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.
Files changed (127) hide show
  1. package/.vscode/extensions.json +7 -0
  2. package/README.md +41 -0
  3. package/dist/shared/src/contracts/cli/envelopes.js +19 -0
  4. package/dist/shared/src/contracts/cli/errors.js +14 -0
  5. package/dist/shared/src/contracts/cli/exit-codes.js +15 -0
  6. package/dist/shared/src/contracts/cli/index.js +6 -0
  7. package/dist/shared/src/contracts/cli/metadata.js +1 -0
  8. package/dist/shared/src/contracts/cli/operations.js +1 -0
  9. package/dist/shared/src/domain/comments/anchors.js +1 -0
  10. package/dist/shared/src/domain/comments/index.js +4 -0
  11. package/dist/shared/src/domain/comments/serializer.js +1 -0
  12. package/dist/shared/src/domain/comments/thread-key.js +21 -0
  13. package/dist/shared/src/domain/frontmatter/index.js +3 -0
  14. package/dist/shared/src/domain/frontmatter/parser.js +34 -0
  15. package/dist/shared/src/domain/frontmatter/serializer.js +32 -0
  16. package/dist/shared/src/domain/frontmatter/validators.js +47 -0
  17. package/dist/shared/src/domain/project/index.js +4 -0
  18. package/dist/shared/src/domain/stages/index.js +20 -0
  19. package/dist/shared/src/index.js +6 -0
  20. package/dist/shared/src/utils/guards.js +6 -0
  21. package/dist/shared/src/utils/index.js +3 -0
  22. package/dist/shared/src/utils/invariant.js +5 -0
  23. package/dist/shared/src/utils/result.js +6 -0
  24. package/dist/stego-cli/src/app/cli-version.js +32 -0
  25. package/dist/stego-cli/src/app/command-context.js +1 -0
  26. package/dist/stego-cli/src/app/command-registry.js +121 -0
  27. package/dist/stego-cli/src/app/create-cli-app.js +42 -0
  28. package/dist/stego-cli/src/app/error-boundary.js +42 -0
  29. package/dist/stego-cli/src/app/index.js +6 -0
  30. package/dist/stego-cli/src/app/output-renderer.js +6 -0
  31. package/dist/stego-cli/src/main.js +14 -0
  32. package/dist/stego-cli/src/modules/comments/application/comment-operations.js +457 -0
  33. package/dist/stego-cli/src/modules/comments/commands/comments-add.js +40 -0
  34. package/dist/stego-cli/src/modules/comments/commands/comments-clear-resolved.js +32 -0
  35. package/dist/stego-cli/src/modules/comments/commands/comments-delete.js +33 -0
  36. package/dist/stego-cli/src/modules/comments/commands/comments-read.js +32 -0
  37. package/dist/stego-cli/src/modules/comments/commands/comments-reply.js +36 -0
  38. package/dist/stego-cli/src/modules/comments/commands/comments-set-status.js +35 -0
  39. package/dist/stego-cli/src/modules/comments/commands/comments-sync-anchors.js +33 -0
  40. package/dist/stego-cli/src/modules/comments/domain/comment-policy.js +14 -0
  41. package/dist/stego-cli/src/modules/comments/index.js +20 -0
  42. package/dist/stego-cli/src/modules/comments/infra/comments-repo.js +68 -0
  43. package/dist/stego-cli/src/modules/comments/types.js +1 -0
  44. package/dist/stego-cli/src/modules/compile/application/compile-manuscript.js +16 -0
  45. package/dist/stego-cli/src/modules/compile/commands/build.js +51 -0
  46. package/dist/stego-cli/src/modules/compile/domain/compile-structure.js +105 -0
  47. package/dist/stego-cli/src/modules/compile/index.js +8 -0
  48. package/dist/stego-cli/src/modules/compile/infra/dist-writer.js +8 -0
  49. package/dist/stego-cli/src/modules/compile/types.js +1 -0
  50. package/dist/stego-cli/src/modules/export/application/run-export.js +29 -0
  51. package/dist/stego-cli/src/modules/export/commands/export.js +61 -0
  52. package/dist/stego-cli/src/modules/export/domain/exporter.js +6 -0
  53. package/dist/stego-cli/src/modules/export/index.js +8 -0
  54. package/dist/stego-cli/src/modules/export/types.js +1 -0
  55. package/dist/stego-cli/src/modules/index.js +22 -0
  56. package/dist/stego-cli/src/modules/manuscript/application/create-manuscript.js +107 -0
  57. package/dist/stego-cli/src/modules/manuscript/application/order-inference.js +56 -0
  58. package/dist/stego-cli/src/modules/manuscript/commands/new-manuscript.js +54 -0
  59. package/dist/stego-cli/src/modules/manuscript/domain/manuscript.js +1 -0
  60. package/dist/stego-cli/src/modules/manuscript/index.js +9 -0
  61. package/dist/stego-cli/src/modules/manuscript/infra/manuscript-repo.js +39 -0
  62. package/dist/stego-cli/src/modules/manuscript/types.js +1 -0
  63. package/dist/stego-cli/src/modules/metadata/application/apply-metadata.js +45 -0
  64. package/dist/stego-cli/src/modules/metadata/application/read-metadata.js +18 -0
  65. package/dist/stego-cli/src/modules/metadata/commands/metadata-apply.js +38 -0
  66. package/dist/stego-cli/src/modules/metadata/commands/metadata-read.js +33 -0
  67. package/dist/stego-cli/src/modules/metadata/domain/metadata.js +1 -0
  68. package/dist/stego-cli/src/modules/metadata/index.js +11 -0
  69. package/dist/stego-cli/src/modules/metadata/infra/metadata-repo.js +68 -0
  70. package/dist/stego-cli/src/modules/metadata/types.js +1 -0
  71. package/dist/stego-cli/src/modules/project/application/create-project.js +203 -0
  72. package/dist/stego-cli/src/modules/project/application/infer-project.js +72 -0
  73. package/dist/stego-cli/src/modules/project/commands/new-project.js +86 -0
  74. package/dist/stego-cli/src/modules/project/domain/project.js +1 -0
  75. package/dist/stego-cli/src/modules/project/index.js +9 -0
  76. package/dist/stego-cli/src/modules/project/infra/project-repo.js +27 -0
  77. package/dist/stego-cli/src/modules/project/types.js +1 -0
  78. package/dist/stego-cli/src/modules/quality/application/inspect-project.js +603 -0
  79. package/dist/stego-cli/src/modules/quality/application/lint-runner.js +313 -0
  80. package/dist/stego-cli/src/modules/quality/application/stage-check.js +87 -0
  81. package/dist/stego-cli/src/modules/quality/commands/check-stage.js +54 -0
  82. package/dist/stego-cli/src/modules/quality/commands/lint.js +51 -0
  83. package/dist/stego-cli/src/modules/quality/commands/validate.js +50 -0
  84. package/dist/stego-cli/src/modules/quality/domain/issues.js +1 -0
  85. package/dist/stego-cli/src/modules/quality/domain/policies.js +1 -0
  86. package/dist/stego-cli/src/modules/quality/index.js +14 -0
  87. package/dist/stego-cli/src/modules/quality/infra/cspell-adapter.js +1 -0
  88. package/dist/stego-cli/src/modules/quality/infra/markdownlint-adapter.js +1 -0
  89. package/dist/stego-cli/src/modules/quality/types.js +1 -0
  90. package/dist/stego-cli/src/modules/scaffold/application/scaffold-workspace.js +307 -0
  91. package/dist/stego-cli/src/modules/scaffold/commands/init.js +34 -0
  92. package/dist/stego-cli/src/modules/scaffold/domain/templates.js +182 -0
  93. package/dist/stego-cli/src/modules/scaffold/index.js +8 -0
  94. package/dist/stego-cli/src/modules/scaffold/infra/template-repo.js +33 -0
  95. package/dist/stego-cli/src/modules/scaffold/types.js +1 -0
  96. package/dist/stego-cli/src/modules/spine/application/create-category.js +14 -0
  97. package/dist/stego-cli/src/modules/spine/application/create-entry.js +9 -0
  98. package/dist/stego-cli/src/modules/spine/application/read-catalog.js +13 -0
  99. package/dist/stego-cli/src/modules/spine/commands/spine-deprecated-aliases.js +33 -0
  100. package/dist/stego-cli/src/modules/spine/commands/spine-new-category.js +64 -0
  101. package/dist/stego-cli/src/modules/spine/commands/spine-new-entry.js +65 -0
  102. package/dist/stego-cli/src/modules/spine/commands/spine-read.js +49 -0
  103. package/dist/{spine/spine-domain.js → stego-cli/src/modules/spine/domain/spine.js} +13 -7
  104. package/dist/stego-cli/src/modules/spine/index.js +16 -0
  105. package/dist/stego-cli/src/modules/spine/infra/spine-repo.js +46 -0
  106. package/dist/stego-cli/src/modules/spine/types.js +1 -0
  107. package/dist/stego-cli/src/modules/workspace/application/discover-projects.js +18 -0
  108. package/dist/stego-cli/src/modules/workspace/application/resolve-workspace.js +73 -0
  109. package/dist/stego-cli/src/modules/workspace/commands/list-projects.js +40 -0
  110. package/dist/stego-cli/src/modules/workspace/index.js +9 -0
  111. package/dist/stego-cli/src/modules/workspace/infra/workspace-repo.js +37 -0
  112. package/dist/stego-cli/src/modules/workspace/types.js +1 -0
  113. package/dist/stego-cli/src/platform/clock.js +3 -0
  114. package/dist/stego-cli/src/platform/fs.js +13 -0
  115. package/dist/stego-cli/src/platform/index.js +3 -0
  116. package/dist/stego-cli/src/platform/temp-files.js +6 -0
  117. package/package.json +20 -11
  118. package/dist/comments/comments-command.js +0 -499
  119. package/dist/comments/errors.js +0 -20
  120. package/dist/metadata/metadata-command.js +0 -127
  121. package/dist/metadata/metadata-domain.js +0 -209
  122. package/dist/spine/spine-command.js +0 -129
  123. package/dist/stego-cli.js +0 -2107
  124. /package/dist/{exporters/exporter-types.js → shared/src/contracts/cli/comments.js} +0 -0
  125. /package/dist/{comments/comment-domain.js → shared/src/domain/comments/parser.js} +0 -0
  126. /package/dist/{exporters → stego-cli/src/modules/export/infra}/markdown-exporter.js +0 -0
  127. /package/dist/{exporters → stego-cli/src/modules/export/infra}/pandoc-exporter.js +0 -0
@@ -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 { executeCommentsOperation } from "../application/comment-operations.js";
4
+ import { parseCommentsOutputFormat } from "../infra/comments-repo.js";
5
+ export function registerCommentsSyncAnchorsCommand(registry) {
6
+ registry.register({
7
+ name: "comments sync-anchors <manuscript>",
8
+ description: "Sync comment anchors",
9
+ allowUnknownOptions: true,
10
+ options: [
11
+ { flags: "--input <path>", description: "JSON payload path or '-'" },
12
+ { flags: "--format <format>", description: "text|json" }
13
+ ],
14
+ action: (context) => {
15
+ const manuscriptArg = context.positionals[0];
16
+ if (!manuscriptArg) {
17
+ throw new CliError("INVALID_USAGE", "Manuscript path is required. Use: stego comments sync-anchors <manuscript> --input <path|->.");
18
+ }
19
+ const outputFormat = parseCommentsOutputFormat(context.options.format);
20
+ const result = executeCommentsOperation({
21
+ subcommand: "sync-anchors",
22
+ cwd: context.cwd,
23
+ manuscriptArg,
24
+ options: context.options
25
+ });
26
+ if (outputFormat === "json") {
27
+ writeJson(result.payload);
28
+ return;
29
+ }
30
+ writeText(result.textMessage);
31
+ }
32
+ });
33
+ }
@@ -0,0 +1,14 @@
1
+ export const COMMENT_ID_PATTERN = /^CMT-\d{4,}$/;
2
+ export function normalizeCommentId(value) {
3
+ return value.trim().toUpperCase();
4
+ }
5
+ export function isValidCommentId(value) {
6
+ return COMMENT_ID_PATTERN.test(normalizeCommentId(value));
7
+ }
8
+ export function normalizeCommentStatus(value) {
9
+ const normalized = value.trim().toLowerCase();
10
+ if (normalized === "open" || normalized === "resolved") {
11
+ return normalized;
12
+ }
13
+ return null;
14
+ }
@@ -0,0 +1,20 @@
1
+ import { registerCommentsReadCommand } from "./commands/comments-read.js";
2
+ import { registerCommentsAddCommand } from "./commands/comments-add.js";
3
+ import { registerCommentsReplyCommand } from "./commands/comments-reply.js";
4
+ import { registerCommentsSetStatusCommand } from "./commands/comments-set-status.js";
5
+ import { registerCommentsDeleteCommand } from "./commands/comments-delete.js";
6
+ import { registerCommentsClearResolvedCommand } from "./commands/comments-clear-resolved.js";
7
+ import { registerCommentsSyncAnchorsCommand } from "./commands/comments-sync-anchors.js";
8
+ export const commentsModule = {
9
+ registerCommands(registry) {
10
+ registerCommentsReadCommand(registry);
11
+ registerCommentsAddCommand(registry);
12
+ registerCommentsReplyCommand(registry);
13
+ registerCommentsSetStatusCommand(registry);
14
+ registerCommentsDeleteCommand(registry);
15
+ registerCommentsClearResolvedCommand(registry);
16
+ registerCommentsSyncAnchorsCommand(registry);
17
+ }
18
+ };
19
+ export * from "./types.js";
20
+ export * from "./application/comment-operations.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 parseCommentsOutputFormat(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 resolveManuscriptPath(cwd, manuscriptArg) {
14
+ return path.resolve(cwd, manuscriptArg);
15
+ }
16
+ export function readManuscript(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", `Manuscript 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 manuscript: ${message}`);
32
+ }
33
+ }
34
+ export function writeManuscript(absolutePath, raw) {
35
+ try {
36
+ fs.writeFileSync(absolutePath, raw, "utf8");
37
+ }
38
+ catch (error) {
39
+ const message = error instanceof Error ? error.message : String(error);
40
+ throw new CliError("WRITE_FAILURE", `Failed to update manuscript: ${message}`);
41
+ }
42
+ }
43
+ export function readJsonPayload(inputPath, cwd) {
44
+ let rawJson = "";
45
+ try {
46
+ rawJson = 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(rawJson);
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,16 @@
1
+ import { nowIsoString } from "../../../platform/clock.js";
2
+ import { renderCompiledManuscript } from "../domain/compile-structure.js";
3
+ import { writeCompiledOutput } from "../infra/dist-writer.js";
4
+ export function compileManuscript(input) {
5
+ const markdown = renderCompiledManuscript({
6
+ generatedAt: nowIsoString(),
7
+ projectId: input.project.id,
8
+ title: input.project.meta.title || input.project.id,
9
+ subtitle: input.project.meta.subtitle,
10
+ author: input.project.meta.author,
11
+ chapters: input.chapters,
12
+ compileStructureLevels: input.compileStructureLevels
13
+ });
14
+ const outputPath = writeCompiledOutput(input.project.distDir, input.project.id, markdown);
15
+ return { outputPath };
16
+ }
@@ -0,0 +1,51 @@
1
+ import { writeText } from "../../../app/output-renderer.js";
2
+ import { resolveProjectContext } from "../../project/index.js";
3
+ import { inspectProject, formatIssues, issueHasErrors } from "../../quality/index.js";
4
+ import { resolveWorkspaceContext } from "../../workspace/index.js";
5
+ import { compileManuscript } from "../application/compile-manuscript.js";
6
+ export function registerBuildCommand(registry) {
7
+ registry.register({
8
+ name: "build",
9
+ description: "Compile manuscript output",
10
+ allowUnknownOptions: true,
11
+ options: [
12
+ { flags: "--project <project-id>", description: "Project id" },
13
+ { flags: "--root <path>", description: "Workspace root path" }
14
+ ],
15
+ action: (context) => {
16
+ const project = resolveProjectContext({
17
+ workspace: resolveWorkspaceContext({
18
+ cwd: context.cwd,
19
+ rootOption: readStringOption(context.options, "root")
20
+ }),
21
+ cwd: context.cwd,
22
+ env: context.env,
23
+ explicitProjectId: readStringOption(context.options, "project")
24
+ });
25
+ const report = inspectProject(project);
26
+ for (const line of formatIssues(report.issues)) {
27
+ writeText(line);
28
+ }
29
+ if (issueHasErrors(report.issues)) {
30
+ process.exitCode = 1;
31
+ return;
32
+ }
33
+ const result = compileManuscript({
34
+ project,
35
+ chapters: report.chapters,
36
+ compileStructureLevels: report.compileStructureLevels
37
+ });
38
+ writeText(`Build output: ${result.outputPath}`);
39
+ }
40
+ });
41
+ }
42
+ function readStringOption(options, key) {
43
+ const value = options[key];
44
+ if (typeof value === "string") {
45
+ return value;
46
+ }
47
+ if (typeof value === "number") {
48
+ return String(value);
49
+ }
50
+ return undefined;
51
+ }
@@ -0,0 +1,105 @@
1
+ export function renderCompiledManuscript(input) {
2
+ const tocEntries = [];
3
+ const previousGroupValues = new Map();
4
+ const previousGroupTitles = new Map();
5
+ const entryHeadingLevel = Math.min(6, 2 + input.compileStructureLevels.length);
6
+ const lines = [];
7
+ lines.push(`<!-- generated: ${input.generatedAt} -->`);
8
+ lines.push("");
9
+ lines.push(`# ${input.title}`);
10
+ lines.push("");
11
+ if (input.subtitle) {
12
+ lines.push(`_${input.subtitle}_`);
13
+ lines.push("");
14
+ }
15
+ if (input.author) {
16
+ lines.push(`Author: ${input.author}`);
17
+ lines.push("");
18
+ }
19
+ lines.push(`Generated: ${input.generatedAt}`);
20
+ lines.push("");
21
+ lines.push("## Table of Contents");
22
+ lines.push("");
23
+ if (input.compileStructureLevels.length === 0) {
24
+ lines.push(`- [Manuscript](#${slugify("Manuscript")})`);
25
+ }
26
+ lines.push("");
27
+ for (let chapterIndex = 0; chapterIndex < input.chapters.length; chapterIndex += 1) {
28
+ const chapter = input.chapters[chapterIndex];
29
+ let insertedBreakForEntry = false;
30
+ const levelChanged = [];
31
+ for (let levelIndex = 0; levelIndex < input.compileStructureLevels.length; levelIndex += 1) {
32
+ const level = input.compileStructureLevels[levelIndex];
33
+ const explicitValue = chapter.groupValues[level.key];
34
+ const previousValue = previousGroupValues.get(level.key);
35
+ const currentValue = explicitValue ?? previousValue;
36
+ const explicitTitle = level.titleKey ? toScalarMetadataString(chapter.metadata[level.titleKey]) : undefined;
37
+ const previousTitle = previousGroupTitles.get(level.key);
38
+ const currentTitle = explicitTitle ?? previousTitle;
39
+ const parentChanged = levelIndex > 0 && levelChanged[levelIndex - 1] === true;
40
+ const changed = parentChanged || currentValue !== previousValue;
41
+ levelChanged.push(changed);
42
+ if (!changed || !currentValue) {
43
+ previousGroupValues.set(level.key, currentValue);
44
+ previousGroupTitles.set(level.key, currentTitle);
45
+ continue;
46
+ }
47
+ if (level.pageBreak === "between-groups" && chapterIndex > 0 && !insertedBreakForEntry) {
48
+ lines.push("\\newpage");
49
+ lines.push("");
50
+ insertedBreakForEntry = true;
51
+ }
52
+ if (level.injectHeading) {
53
+ const heading = formatCompileStructureHeading(level, currentValue, currentTitle);
54
+ tocEntries.push({ level: levelIndex, heading });
55
+ const headingLevel = Math.min(6, 2 + levelIndex);
56
+ lines.push(`${"#".repeat(headingLevel)} ${heading}`);
57
+ lines.push("");
58
+ }
59
+ previousGroupValues.set(level.key, currentValue);
60
+ previousGroupTitles.set(level.key, currentTitle);
61
+ }
62
+ lines.push(`${"#".repeat(entryHeadingLevel)} ${chapter.title}`);
63
+ lines.push("");
64
+ lines.push(`<!-- source: ${chapter.relativePath} | order: ${chapter.order} | status: ${chapter.status} -->`);
65
+ lines.push("");
66
+ lines.push(chapter.body.trim());
67
+ lines.push("");
68
+ }
69
+ if (tocEntries.length > 0) {
70
+ const tocStart = lines.indexOf("## Table of Contents");
71
+ if (tocStart >= 0) {
72
+ const insertAt = tocStart + 2;
73
+ const tocLines = tocEntries.map((entry) => `${" ".repeat(entry.level)}- [${entry.heading}](#${slugify(entry.heading)})`);
74
+ lines.splice(insertAt, 0, ...tocLines);
75
+ }
76
+ }
77
+ return `${lines.join("\n")}\n`;
78
+ }
79
+ function formatCompileStructureHeading(level, value, title) {
80
+ const resolvedTitle = title || "";
81
+ if (!resolvedTitle && level.headingTemplate === "{label} {value}: {title}") {
82
+ return `${level.label} ${value}`;
83
+ }
84
+ return level.headingTemplate
85
+ .replaceAll("{label}", level.label)
86
+ .replaceAll("{value}", value)
87
+ .replaceAll("{title}", resolvedTitle)
88
+ .replace(/\s+/g, " ")
89
+ .replace(/:\s*$/, "")
90
+ .trim();
91
+ }
92
+ function toScalarMetadataString(rawValue) {
93
+ if (rawValue == null || rawValue === "" || Array.isArray(rawValue)) {
94
+ return undefined;
95
+ }
96
+ const normalized = String(rawValue).trim();
97
+ return normalized.length > 0 ? normalized : undefined;
98
+ }
99
+ function slugify(value) {
100
+ return value
101
+ .toLowerCase()
102
+ .replace(/[^a-z0-9\s-]/g, "")
103
+ .trim()
104
+ .replace(/\s+/g, "-");
105
+ }
@@ -0,0 +1,8 @@
1
+ import { registerBuildCommand } from "./commands/build.js";
2
+ export const compileModule = {
3
+ registerCommands(registry) {
4
+ registerBuildCommand(registry);
5
+ }
6
+ };
7
+ export * from "./types.js";
8
+ export * from "./application/compile-manuscript.js";
@@ -0,0 +1,8 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ export function writeCompiledOutput(distDir, projectId, markdown) {
4
+ fs.mkdirSync(distDir, { recursive: true });
5
+ const outputPath = path.join(distDir, `${projectId}.md`);
6
+ fs.writeFileSync(outputPath, markdown, "utf8");
7
+ return outputPath;
8
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,29 @@
1
+ import path from "node:path";
2
+ import { parseExportFormat } from "../domain/exporter.js";
3
+ import { markdownExporter } from "../infra/markdown-exporter.js";
4
+ import { createPandocExporter } from "../infra/pandoc-exporter.js";
5
+ const exporters = {
6
+ md: markdownExporter,
7
+ docx: createPandocExporter("docx"),
8
+ pdf: createPandocExporter("pdf"),
9
+ epub: createPandocExporter("epub")
10
+ };
11
+ export function runExport(input) {
12
+ const format = parseExportFormat(input.format.toLowerCase());
13
+ const exporter = exporters[format];
14
+ const targetPath = input.explicitOutputPath
15
+ || path.join(input.project.distDir, "exports", `${input.project.id}.${format}`);
16
+ const outputPath = path.resolve(input.project.workspace.repoRoot, targetPath);
17
+ const capability = exporter.canRun();
18
+ if (!capability.ok) {
19
+ throw new Error(capability.reason || `Exporter '${exporter.id}' cannot run.`);
20
+ }
21
+ exporter.run({
22
+ inputPath: input.inputPath,
23
+ outputPath
24
+ });
25
+ return {
26
+ outputPath,
27
+ format
28
+ };
29
+ }
@@ -0,0 +1,61 @@
1
+ import { writeText } from "../../../app/output-renderer.js";
2
+ import { compileManuscript } from "../../compile/index.js";
3
+ import { resolveProjectContext } from "../../project/index.js";
4
+ import { formatIssues, inspectProject, issueHasErrors } from "../../quality/index.js";
5
+ import { resolveWorkspaceContext } from "../../workspace/index.js";
6
+ import { runExport } from "../application/run-export.js";
7
+ export function registerExportCommand(registry) {
8
+ registry.register({
9
+ name: "export",
10
+ description: "Export manuscript formats",
11
+ allowUnknownOptions: true,
12
+ options: [
13
+ { flags: "--project <project-id>", description: "Project id" },
14
+ { flags: "--format <format>", description: "md|docx|pdf|epub" },
15
+ { flags: "--output <path>", description: "Explicit output path" },
16
+ { flags: "--root <path>", description: "Workspace root path" }
17
+ ],
18
+ action: (context) => {
19
+ const project = resolveProjectContext({
20
+ workspace: resolveWorkspaceContext({
21
+ cwd: context.cwd,
22
+ rootOption: readStringOption(context.options, "root")
23
+ }),
24
+ cwd: context.cwd,
25
+ env: context.env,
26
+ explicitProjectId: readStringOption(context.options, "project")
27
+ });
28
+ const format = (readStringOption(context.options, "format") || "md").toLowerCase();
29
+ const report = inspectProject(project);
30
+ for (const line of formatIssues(report.issues)) {
31
+ writeText(line);
32
+ }
33
+ if (issueHasErrors(report.issues)) {
34
+ process.exitCode = 1;
35
+ return;
36
+ }
37
+ const compiled = compileManuscript({
38
+ project,
39
+ chapters: report.chapters,
40
+ compileStructureLevels: report.compileStructureLevels
41
+ });
42
+ const exported = runExport({
43
+ project,
44
+ format,
45
+ inputPath: compiled.outputPath,
46
+ explicitOutputPath: readStringOption(context.options, "output")
47
+ });
48
+ writeText(`Export output: ${exported.outputPath}`);
49
+ }
50
+ });
51
+ }
52
+ function readStringOption(options, key) {
53
+ const value = options[key];
54
+ if (typeof value === "string") {
55
+ return value;
56
+ }
57
+ if (typeof value === "number") {
58
+ return String(value);
59
+ }
60
+ return undefined;
61
+ }
@@ -0,0 +1,6 @@
1
+ export function parseExportFormat(value) {
2
+ if (value === "md" || value === "docx" || value === "pdf" || value === "epub") {
3
+ return value;
4
+ }
5
+ throw new Error(`Unsupported export format '${value}'. Use md, docx, pdf, or epub.`);
6
+ }
@@ -0,0 +1,8 @@
1
+ import { registerExportCommand } from "./commands/export.js";
2
+ export const exportModule = {
3
+ registerCommands(registry) {
4
+ registerExportCommand(registry);
5
+ }
6
+ };
7
+ export * from "./types.js";
8
+ export * from "./application/run-export.js";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ import { workspaceModule } from "./workspace/index.js";
2
+ import { projectModule } from "./project/index.js";
3
+ import { scaffoldModule } from "./scaffold/index.js";
4
+ import { manuscriptModule } from "./manuscript/index.js";
5
+ import { spineModule } from "./spine/index.js";
6
+ import { metadataModule } from "./metadata/index.js";
7
+ import { commentsModule } from "./comments/index.js";
8
+ import { qualityModule } from "./quality/index.js";
9
+ import { compileModule } from "./compile/index.js";
10
+ import { exportModule } from "./export/index.js";
11
+ export const coreModules = [
12
+ workspaceModule,
13
+ projectModule,
14
+ scaffoldModule,
15
+ manuscriptModule,
16
+ spineModule,
17
+ metadataModule,
18
+ commentsModule,
19
+ qualityModule,
20
+ compileModule,
21
+ exportModule
22
+ ];
@@ -0,0 +1,107 @@
1
+ import path from "node:path";
2
+ import { fileExists, ensureManuscriptDir, joinPath, listManuscriptOrderEntries, writeTextFile } from "../infra/manuscript-repo.js";
3
+ import { inferNextManuscriptPrefix, parseManuscriptPrefix, parseOrderFromManuscriptFilename, parseRequestedManuscriptFilename } from "./order-inference.js";
4
+ const DEFAULT_NEW_MANUSCRIPT_SLUG = "new-document";
5
+ export function createNewManuscript(input) {
6
+ const project = input.project;
7
+ ensureManuscriptDir(project.manuscriptDir);
8
+ const requiredMetadataState = resolveRequiredMetadata(project);
9
+ if (requiredMetadataState.errors.length > 0) {
10
+ throw new Error(`Unable to resolve required metadata for project '${project.id}': ${requiredMetadataState.errors.join(" ")}`);
11
+ }
12
+ const existingEntries = listManuscriptOrderEntries(project.manuscriptDir);
13
+ const explicitPrefix = parseManuscriptPrefix(input.requestedPrefixRaw);
14
+ const requestedFilename = parseRequestedManuscriptFilename(input.requestedFilenameRaw);
15
+ if (requestedFilename && explicitPrefix != null) {
16
+ throw new Error("Options --filename and --i/-i cannot be used together.");
17
+ }
18
+ let filename;
19
+ if (requestedFilename) {
20
+ const requestedOrder = parseOrderFromManuscriptFilename(requestedFilename);
21
+ if (requestedOrder != null) {
22
+ const collision = existingEntries.find((entry) => entry.order === requestedOrder);
23
+ if (collision) {
24
+ throw new Error(`Manuscript prefix '${requestedOrder}' is already used by '${collision.filename}'. Choose a different filename prefix.`);
25
+ }
26
+ }
27
+ filename = requestedFilename;
28
+ }
29
+ else {
30
+ const nextPrefix = explicitPrefix ?? inferNextManuscriptPrefix(existingEntries);
31
+ const collision = existingEntries.find((entry) => entry.order === nextPrefix);
32
+ if (collision) {
33
+ throw new Error(`Manuscript prefix '${nextPrefix}' is already used by '${collision.filename}'. Re-run with --i <number> to choose an unused prefix.`);
34
+ }
35
+ filename = `${nextPrefix}-${DEFAULT_NEW_MANUSCRIPT_SLUG}.md`;
36
+ }
37
+ const manuscriptPath = joinPath(project.manuscriptDir, filename);
38
+ if (fileExists(manuscriptPath)) {
39
+ throw new Error(`Manuscript already exists: ${filename}`);
40
+ }
41
+ writeTextFile(manuscriptPath, renderNewManuscriptTemplate(requiredMetadataState.requiredMetadata));
42
+ return {
43
+ projectId: project.id,
44
+ filePath: path.relative(project.workspace.repoRoot, manuscriptPath)
45
+ };
46
+ }
47
+ export function parseManuscriptOutputFormat(raw) {
48
+ if (!raw || raw === "text") {
49
+ return "text";
50
+ }
51
+ if (raw === "json") {
52
+ return "json";
53
+ }
54
+ throw new Error("Invalid --format value. Use 'text' or 'json'.");
55
+ }
56
+ function resolveRequiredMetadata(project) {
57
+ const raw = project.meta.requiredMetadata;
58
+ if (raw == null) {
59
+ return {
60
+ requiredMetadata: project.workspace.config.requiredMetadata,
61
+ errors: []
62
+ };
63
+ }
64
+ if (!Array.isArray(raw)) {
65
+ return {
66
+ requiredMetadata: project.workspace.config.requiredMetadata,
67
+ errors: ["Project 'requiredMetadata' must be an array of metadata keys."]
68
+ };
69
+ }
70
+ const requiredMetadata = [];
71
+ const seen = new Set();
72
+ const errors = [];
73
+ for (const [index, entry] of raw.entries()) {
74
+ if (typeof entry !== "string") {
75
+ errors.push(`Project 'requiredMetadata' entry at index ${index} must be a string.`);
76
+ continue;
77
+ }
78
+ const key = entry.trim();
79
+ if (!key) {
80
+ errors.push(`Project 'requiredMetadata' entry at index ${index} cannot be empty.`);
81
+ continue;
82
+ }
83
+ if (seen.has(key)) {
84
+ continue;
85
+ }
86
+ seen.add(key);
87
+ requiredMetadata.push(key);
88
+ }
89
+ return {
90
+ requiredMetadata,
91
+ errors
92
+ };
93
+ }
94
+ function renderNewManuscriptTemplate(requiredMetadata) {
95
+ const lines = ["---", "status: draft"];
96
+ const seenKeys = new Set(["status"]);
97
+ for (const key of requiredMetadata) {
98
+ const normalized = key.trim();
99
+ if (!normalized || seenKeys.has(normalized)) {
100
+ continue;
101
+ }
102
+ seenKeys.add(normalized);
103
+ lines.push(`${normalized}:`);
104
+ }
105
+ lines.push("---", "");
106
+ return `${lines.join("\n")}\n`;
107
+ }
@@ -0,0 +1,56 @@
1
+ export function parseManuscriptPrefix(raw) {
2
+ if (raw == null) {
3
+ return undefined;
4
+ }
5
+ const normalized = raw.trim();
6
+ if (!normalized) {
7
+ throw new Error("Option --i/-i requires a numeric value.");
8
+ }
9
+ if (!/^\d+$/.test(normalized)) {
10
+ throw new Error(`Invalid manuscript prefix '${raw}'. Use a non-negative integer.`);
11
+ }
12
+ const parsed = Number(normalized);
13
+ if (!Number.isSafeInteger(parsed)) {
14
+ throw new Error(`Invalid manuscript prefix '${raw}'. Use a smaller integer value.`);
15
+ }
16
+ return parsed;
17
+ }
18
+ export function parseRequestedManuscriptFilename(raw) {
19
+ if (raw == null) {
20
+ return undefined;
21
+ }
22
+ const normalized = raw.trim();
23
+ if (!normalized) {
24
+ throw new Error("Option --filename requires a value.");
25
+ }
26
+ if (/[\\/]/.test(normalized)) {
27
+ throw new Error(`Invalid filename '${raw}'. Use a filename only (no directory separators).`);
28
+ }
29
+ const withExtension = normalized.toLowerCase().endsWith(".md")
30
+ ? normalized
31
+ : `${normalized}.md`;
32
+ const stem = withExtension.slice(0, -3).trim();
33
+ if (!stem) {
34
+ throw new Error(`Invalid filename '${raw}'.`);
35
+ }
36
+ return withExtension;
37
+ }
38
+ export function parseOrderFromManuscriptFilename(filename) {
39
+ const match = filename.match(/^(\d+)[-_]/);
40
+ if (!match) {
41
+ return undefined;
42
+ }
43
+ return Number(match[1]);
44
+ }
45
+ export function inferNextManuscriptPrefix(entries) {
46
+ if (entries.length === 0) {
47
+ return 100;
48
+ }
49
+ if (entries.length === 1) {
50
+ return entries[0].order + 100;
51
+ }
52
+ const previous = entries[entries.length - 2].order;
53
+ const latest = entries[entries.length - 1].order;
54
+ const step = latest - previous;
55
+ return latest + (step > 0 ? step : 1);
56
+ }