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,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,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
+ }