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,64 @@
1
+ import { CliError } from "../../../../../shared/src/contracts/cli/index.js";
2
+ import { writeJson, writeText } from "../../../app/output-renderer.js";
3
+ import { resolveProjectContext } from "../../project/index.js";
4
+ import { resolveWorkspaceContext } from "../../workspace/index.js";
5
+ import { createSpineCategoryForProject } from "../application/create-category.js";
6
+ import { parseSpineOutputFormat } from "../infra/spine-repo.js";
7
+ export function registerSpineNewCategoryCommand(registry) {
8
+ registry.register({
9
+ name: "spine new-category",
10
+ description: "Create a new spine category",
11
+ allowUnknownOptions: true,
12
+ options: [
13
+ { flags: "--project <project-id>", description: "Project id" },
14
+ { flags: "--key <category>", description: "Category key" },
15
+ { flags: "--label <label>", description: "Category display label" },
16
+ { flags: "--require-metadata", description: "Append key to required metadata" },
17
+ { flags: "--format <format>", description: "text|json" },
18
+ { flags: "--root <path>", description: "Workspace root path" }
19
+ ],
20
+ action: (context) => {
21
+ const key = readStringOption(context.options, "key");
22
+ if (!key) {
23
+ throw new CliError("INVALID_USAGE", "--key is required for `stego spine new-category`.");
24
+ }
25
+ const outputFormat = parseSpineOutputFormat(readOption(context.options, "format"));
26
+ const project = resolveProjectContext({
27
+ workspace: resolveWorkspaceContext({
28
+ cwd: context.cwd,
29
+ rootOption: readStringOption(context.options, "root")
30
+ }),
31
+ cwd: context.cwd,
32
+ env: context.env,
33
+ explicitProjectId: readStringOption(context.options, "project")
34
+ });
35
+ const result = createSpineCategoryForProject({
36
+ project,
37
+ key,
38
+ label: readStringOption(context.options, "label"),
39
+ requireMetadata: readBooleanOption(context.options, "requireMetadata")
40
+ });
41
+ if (outputFormat === "json") {
42
+ writeJson(result);
43
+ return;
44
+ }
45
+ writeText(`Created spine category '${result.result.key}' (${result.result.metadataPath}).`);
46
+ }
47
+ });
48
+ }
49
+ function readOption(options, key) {
50
+ return options[key];
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
+ }
62
+ function readBooleanOption(options, key) {
63
+ return options[key] === true;
64
+ }
@@ -0,0 +1,65 @@
1
+ import { CliError } from "../../../../../shared/src/contracts/cli/index.js";
2
+ import { writeJson, writeText } from "../../../app/output-renderer.js";
3
+ import { resolveProjectContext } from "../../project/index.js";
4
+ import { resolveWorkspaceContext } from "../../workspace/index.js";
5
+ import { createSpineEntryForProject } from "../application/create-entry.js";
6
+ import { parseSpineOutputFormat } from "../infra/spine-repo.js";
7
+ export function registerSpineNewEntryCommand(registry) {
8
+ registry.register({
9
+ name: "spine new",
10
+ description: "Create a new spine entry",
11
+ allowUnknownOptions: true,
12
+ options: [
13
+ { flags: "--project <project-id>", description: "Project id" },
14
+ { flags: "--category <category>", description: "Category key" },
15
+ { flags: "--filename <path>", description: "Relative entry path" },
16
+ { flags: "--format <format>", description: "text|json" },
17
+ { flags: "--root <path>", description: "Workspace root path" }
18
+ ],
19
+ action: (context) => {
20
+ if (hasOption(context.options, "entry")) {
21
+ throw new Error("Unknown option '--entry' for `stego spine new`. Use `--filename`.");
22
+ }
23
+ const category = readStringOption(context.options, "category");
24
+ if (!category) {
25
+ throw new CliError("INVALID_USAGE", "--category is required for `stego spine new`.");
26
+ }
27
+ const outputFormat = parseSpineOutputFormat(readOption(context.options, "format"));
28
+ const project = resolveProjectContext({
29
+ workspace: resolveWorkspaceContext({
30
+ cwd: context.cwd,
31
+ rootOption: readStringOption(context.options, "root")
32
+ }),
33
+ cwd: context.cwd,
34
+ env: context.env,
35
+ explicitProjectId: readStringOption(context.options, "project")
36
+ });
37
+ const result = createSpineEntryForProject({
38
+ project,
39
+ category,
40
+ filename: readStringOption(context.options, "filename")
41
+ });
42
+ if (outputFormat === "json") {
43
+ writeJson(result);
44
+ return;
45
+ }
46
+ writeText(`Created spine entry: ${result.result.filePath}`);
47
+ }
48
+ });
49
+ }
50
+ function readOption(options, key) {
51
+ return options[key];
52
+ }
53
+ function readStringOption(options, key) {
54
+ const value = options[key];
55
+ if (typeof value === "string") {
56
+ return value;
57
+ }
58
+ if (typeof value === "number") {
59
+ return String(value);
60
+ }
61
+ return undefined;
62
+ }
63
+ function hasOption(options, key) {
64
+ return Object.hasOwn(options, key);
65
+ }
@@ -0,0 +1,49 @@
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 { readSpineCatalogForProject } from "../application/read-catalog.js";
5
+ import { parseSpineOutputFormat } from "../infra/spine-repo.js";
6
+ export function registerSpineReadCommand(registry) {
7
+ registry.register({
8
+ name: "spine read",
9
+ description: "Read spine catalog",
10
+ allowUnknownOptions: true,
11
+ options: [
12
+ { flags: "--project <project-id>", description: "Project id" },
13
+ { flags: "--format <format>", description: "text|json" },
14
+ { flags: "--root <path>", description: "Workspace root path" }
15
+ ],
16
+ action: (context) => {
17
+ const outputFormat = parseSpineOutputFormat(readOption(context.options, "format"));
18
+ const project = resolveProjectContext({
19
+ workspace: resolveWorkspaceContext({
20
+ cwd: context.cwd,
21
+ rootOption: readStringOption(context.options, "root")
22
+ }),
23
+ cwd: context.cwd,
24
+ env: context.env,
25
+ explicitProjectId: readStringOption(context.options, "project")
26
+ });
27
+ const result = readSpineCatalogForProject(project);
28
+ if (outputFormat === "json") {
29
+ writeJson(result);
30
+ return;
31
+ }
32
+ const entryCount = result.state.categories.reduce((sum, category) => sum + category.entries.length, 0);
33
+ writeText(`Spine categories: ${result.state.categories.length}. Entries: ${entryCount}.`);
34
+ }
35
+ });
36
+ }
37
+ function readOption(options, key) {
38
+ return options[key];
39
+ }
40
+ function readStringOption(options, key) {
41
+ const value = options[key];
42
+ if (typeof value === "string") {
43
+ return value;
44
+ }
45
+ if (typeof value === "number") {
46
+ return String(value);
47
+ }
48
+ return undefined;
49
+ }
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { deriveDefaultLabelFromFilename, parseMarkdownDocument, serializeMarkdownDocument } from "../metadata/metadata-domain.js";
3
+ import { parseMarkdownDocument, serializeMarkdownDocument } from "../../../../../shared/src/domain/frontmatter/index.js";
4
4
  const CATEGORY_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;
5
5
  export function readSpineCatalog(projectRoot, spineDir) {
6
6
  const absoluteSpineDir = path.resolve(spineDir);
@@ -28,10 +28,7 @@ export function readSpineCatalog(projectRoot, spineDir) {
28
28
  const knownKeys = new Set();
29
29
  for (const entryFile of entryFiles) {
30
30
  const key = toEntryKey(categoryDir, entryFile);
31
- if (!key) {
32
- continue;
33
- }
34
- if (knownKeys.has(key)) {
31
+ if (!key || knownKeys.has(key)) {
35
32
  continue;
36
33
  }
37
34
  knownKeys.add(key);
@@ -72,11 +69,10 @@ export function createSpineCategory(projectRoot, spineDir, keyRaw, labelRaw, req
72
69
  const label = (labelRaw || "").trim() || toDisplayLabel(key);
73
70
  const metadataPath = path.join(categoryDir, "_category.md");
74
71
  if (!fs.existsSync(metadataPath)) {
75
- const frontmatter = { label };
76
72
  const rendered = serializeMarkdownDocument({
77
73
  lineEnding: "\n",
78
74
  hasFrontmatter: true,
79
- frontmatter,
75
+ frontmatter: { label },
80
76
  body: `# ${label}\n\n`
81
77
  });
82
78
  fs.writeFileSync(metadataPath, rendered, "utf8");
@@ -272,3 +268,13 @@ function firstContentLine(body, heading) {
272
268
  }
273
269
  return "";
274
270
  }
271
+ function deriveDefaultLabelFromFilename(filePath) {
272
+ const basename = path.basename(filePath, path.extname(filePath));
273
+ const normalized = basename
274
+ .replace(/[_-]+/g, " ")
275
+ .trim();
276
+ if (!normalized) {
277
+ return "New Entry";
278
+ }
279
+ return normalized.replace(/\b\w/g, (part) => part.toUpperCase());
280
+ }
@@ -0,0 +1,16 @@
1
+ import { registerSpineReadCommand } from "./commands/spine-read.js";
2
+ import { registerSpineNewCategoryCommand } from "./commands/spine-new-category.js";
3
+ import { registerSpineNewEntryCommand } from "./commands/spine-new-entry.js";
4
+ import { registerSpineDeprecatedAliases } from "./commands/spine-deprecated-aliases.js";
5
+ export const spineModule = {
6
+ registerCommands(registry) {
7
+ registerSpineReadCommand(registry);
8
+ registerSpineNewCategoryCommand(registry);
9
+ registerSpineNewEntryCommand(registry);
10
+ registerSpineDeprecatedAliases(registry);
11
+ }
12
+ };
13
+ export * from "./types.js";
14
+ export * from "./application/read-catalog.js";
15
+ export * from "./application/create-category.js";
16
+ export * from "./application/create-entry.js";
@@ -0,0 +1,46 @@
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 parseSpineOutputFormat(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 readRequiredMetadata(projectMeta) {
14
+ const rawValue = projectMeta.requiredMetadata;
15
+ if (!Array.isArray(rawValue)) {
16
+ return [];
17
+ }
18
+ const seen = new Set();
19
+ const keys = [];
20
+ for (const candidate of rawValue) {
21
+ if (typeof candidate !== "string") {
22
+ continue;
23
+ }
24
+ const normalized = candidate.trim();
25
+ if (!normalized || seen.has(normalized)) {
26
+ continue;
27
+ }
28
+ seen.add(normalized);
29
+ keys.push(normalized);
30
+ }
31
+ return keys;
32
+ }
33
+ export function writeRequiredMetadata(projectRoot, projectMeta, requiredMetadata) {
34
+ const projectJsonPath = path.join(projectRoot, "stego-project.json");
35
+ const next = {
36
+ ...projectMeta,
37
+ requiredMetadata
38
+ };
39
+ try {
40
+ fs.writeFileSync(projectJsonPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
41
+ }
42
+ catch (error) {
43
+ const message = error instanceof Error ? error.message : String(error);
44
+ throw new CliError("WRITE_FAILURE", `Failed to update ${projectJsonPath}: ${message}`);
45
+ }
46
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,18 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ export function discoverWorkspaceProjects(workspace) {
4
+ const projectsRoot = path.join(workspace.repoRoot, workspace.config.projectsDir);
5
+ if (!fs.existsSync(projectsRoot)) {
6
+ return [];
7
+ }
8
+ return fs.readdirSync(projectsRoot, { withFileTypes: true })
9
+ .filter((entry) => entry.isDirectory())
10
+ .map((entry) => entry.name)
11
+ .filter((projectId) => fs.existsSync(path.join(projectsRoot, projectId, "stego-project.json")))
12
+ .sort((left, right) => left.localeCompare(right))
13
+ .map((projectId) => ({
14
+ id: projectId,
15
+ root: path.join(projectsRoot, projectId),
16
+ projectJsonPath: path.join(projectsRoot, projectId, "stego-project.json")
17
+ }));
18
+ }
@@ -0,0 +1,73 @@
1
+ import path from "node:path";
2
+ import { CliError } from "../../../../../shared/src/contracts/cli/index.js";
3
+ import { ROOT_CONFIG_FILENAME } from "../types.js";
4
+ import { findNearestFileUpward, isDirectory, pathExists, readJsonFile } from "../infra/workspace-repo.js";
5
+ export function resolveWorkspaceContext(input) {
6
+ if (input.rootOption) {
7
+ return resolveFromExplicitRoot(input.cwd, input.rootOption);
8
+ }
9
+ const discoveredConfigPath = findNearestFileUpward(input.cwd, ROOT_CONFIG_FILENAME);
10
+ if (!discoveredConfigPath) {
11
+ const legacyConfigPath = findNearestFileUpward(input.cwd, "writing.config.json");
12
+ if (legacyConfigPath) {
13
+ throw new CliError("WORKSPACE_NOT_FOUND", `Found legacy '${path.basename(legacyConfigPath)}' at '${path.dirname(legacyConfigPath)}'. Rename it to '${ROOT_CONFIG_FILENAME}'.`);
14
+ }
15
+ throw new CliError("WORKSPACE_NOT_FOUND", `No Stego workspace found from '${input.cwd}'. Run 'stego init' or pass --root <path>.`);
16
+ }
17
+ const repoRoot = path.dirname(discoveredConfigPath);
18
+ const config = parseWorkspaceConfig(discoveredConfigPath);
19
+ return {
20
+ repoRoot,
21
+ configPath: discoveredConfigPath,
22
+ config
23
+ };
24
+ }
25
+ function resolveFromExplicitRoot(cwd, rootOption) {
26
+ const explicitRoot = path.resolve(cwd, rootOption);
27
+ if (!pathExists(explicitRoot) || !isDirectory(explicitRoot)) {
28
+ throw new CliError("WORKSPACE_NOT_FOUND", `Workspace root does not exist or is not a directory: ${explicitRoot}`);
29
+ }
30
+ const explicitConfigPath = path.join(explicitRoot, ROOT_CONFIG_FILENAME);
31
+ if (!pathExists(explicitConfigPath)) {
32
+ const legacyConfigPath = path.join(explicitRoot, "writing.config.json");
33
+ if (pathExists(legacyConfigPath)) {
34
+ throw new CliError("WORKSPACE_NOT_FOUND", `Found legacy 'writing.config.json' at '${explicitRoot}'. Rename it to '${ROOT_CONFIG_FILENAME}'.`);
35
+ }
36
+ throw new CliError("WORKSPACE_NOT_FOUND", `No Stego workspace found at '${explicitRoot}'. Expected '${ROOT_CONFIG_FILENAME}'.`);
37
+ }
38
+ return {
39
+ repoRoot: explicitRoot,
40
+ configPath: explicitConfigPath,
41
+ config: parseWorkspaceConfig(explicitConfigPath)
42
+ };
43
+ }
44
+ function parseWorkspaceConfig(configPath) {
45
+ let parsed;
46
+ try {
47
+ parsed = readJsonFile(configPath);
48
+ }
49
+ catch (error) {
50
+ const message = error instanceof Error ? error.message : String(error);
51
+ throw new CliError("INVALID_CONFIGURATION", `Invalid JSON at ${configPath}: ${message}`);
52
+ }
53
+ if (!isWorkspaceConfig(parsed)) {
54
+ throw new CliError("INVALID_CONFIGURATION", `Invalid Stego workspace config at ${configPath}.`);
55
+ }
56
+ return parsed;
57
+ }
58
+ function isWorkspaceConfig(value) {
59
+ if (!isPlainObject(value)) {
60
+ return false;
61
+ }
62
+ return typeof value.projectsDir === "string"
63
+ && typeof value.chapterDir === "string"
64
+ && typeof value.spineDir === "string"
65
+ && typeof value.notesDir === "string"
66
+ && typeof value.distDir === "string"
67
+ && Array.isArray(value.requiredMetadata)
68
+ && Array.isArray(value.allowedStatuses)
69
+ && isPlainObject(value.stagePolicies);
70
+ }
71
+ function isPlainObject(value) {
72
+ return typeof value === "object" && value !== null && !Array.isArray(value);
73
+ }
@@ -0,0 +1,40 @@
1
+ import { writeText } from "../../../app/output-renderer.js";
2
+ import { discoverWorkspaceProjects } from "../application/discover-projects.js";
3
+ import { resolveWorkspaceContext } from "../application/resolve-workspace.js";
4
+ export function registerListProjectsCommand(registry) {
5
+ registry.register({
6
+ name: "list-projects",
7
+ description: "List projects in the workspace",
8
+ options: [
9
+ {
10
+ flags: "--root <path>",
11
+ description: "Workspace root path"
12
+ }
13
+ ],
14
+ action: (context) => {
15
+ const workspace = resolveWorkspaceContext({
16
+ cwd: context.cwd,
17
+ rootOption: readStringOption(context.options, "root")
18
+ });
19
+ const projects = discoverWorkspaceProjects(workspace);
20
+ if (projects.length === 0) {
21
+ writeText("No projects found.");
22
+ return;
23
+ }
24
+ writeText("Projects:");
25
+ for (const project of projects) {
26
+ writeText(`- ${project.id}`);
27
+ }
28
+ }
29
+ });
30
+ }
31
+ function readStringOption(options, key) {
32
+ const value = options[key];
33
+ if (typeof value === "string") {
34
+ return value;
35
+ }
36
+ if (typeof value === "number") {
37
+ return String(value);
38
+ }
39
+ return undefined;
40
+ }
@@ -0,0 +1,9 @@
1
+ import { registerListProjectsCommand } from "./commands/list-projects.js";
2
+ export const workspaceModule = {
3
+ registerCommands(registry) {
4
+ registerListProjectsCommand(registry);
5
+ }
6
+ };
7
+ export * from "./types.js";
8
+ export * from "./application/resolve-workspace.js";
9
+ export * from "./application/discover-projects.js";
@@ -0,0 +1,37 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ export function pathExists(value) {
4
+ return fs.existsSync(value);
5
+ }
6
+ export function isDirectory(value) {
7
+ try {
8
+ return fs.statSync(value).isDirectory();
9
+ }
10
+ catch {
11
+ return false;
12
+ }
13
+ }
14
+ export function readJsonFile(filePath) {
15
+ const raw = fs.readFileSync(filePath, "utf8");
16
+ return JSON.parse(raw);
17
+ }
18
+ export function findNearestFileUpward(startPath, filename) {
19
+ let current = path.resolve(startPath);
20
+ if (!pathExists(current)) {
21
+ return null;
22
+ }
23
+ if (!isDirectory(current)) {
24
+ current = path.dirname(current);
25
+ }
26
+ while (true) {
27
+ const candidate = path.join(current, filename);
28
+ if (pathExists(candidate)) {
29
+ return candidate;
30
+ }
31
+ const parent = path.dirname(current);
32
+ if (parent === current) {
33
+ return null;
34
+ }
35
+ current = parent;
36
+ }
37
+ }
@@ -0,0 +1 @@
1
+ export const ROOT_CONFIG_FILENAME = "stego.config.json";
@@ -0,0 +1,3 @@
1
+ export function nowIsoString() {
2
+ return new Date().toISOString();
3
+ }
@@ -0,0 +1,13 @@
1
+ import fs from "node:fs";
2
+ export const fileSystem = {
3
+ existsSync: fs.existsSync,
4
+ readFileSync: fs.readFileSync,
5
+ writeFileSync: fs.writeFileSync,
6
+ mkdirSync: fs.mkdirSync,
7
+ readdirSync: fs.readdirSync,
8
+ statSync: fs.statSync,
9
+ cpSync: fs.cpSync,
10
+ copyFileSync: fs.copyFileSync,
11
+ mkdtempSync: fs.mkdtempSync,
12
+ rmSync: fs.rmSync
13
+ };
@@ -0,0 +1,3 @@
1
+ export * from "./clock.js";
2
+ export * from "./fs.js";
3
+ export * from "./temp-files.js";
@@ -0,0 +1,6 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import { fileSystem } from "./fs.js";
4
+ export function makeTempDir(prefix) {
5
+ return fileSystem.mkdtempSync(path.join(os.tmpdir(), prefix));
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stego-cli",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "description": "Installable CLI for the Stego writing monorepo workflow.",
6
6
  "license": "Apache-2.0",
@@ -14,7 +14,7 @@
14
14
  },
15
15
  "homepage": "https://github.com/matt-gold/stego/tree/main/packages/stego-cli#readme",
16
16
  "bin": {
17
- "stego": "dist/stego-cli.js"
17
+ "stego": "dist/stego-cli/src/main.js"
18
18
  },
19
19
  "engines": {
20
20
  "node": ">=20"
@@ -41,25 +41,34 @@
41
41
  "pack:dry-run": "npm pack --dry-run",
42
42
  "changeset": "changeset",
43
43
  "release:version": "changeset version",
44
- "list-projects": "node --experimental-strip-types tools/stego-cli.ts list-projects",
45
- "new-project": "node --experimental-strip-types tools/stego-cli.ts new-project",
46
- "new": "node --experimental-strip-types tools/stego-cli.ts new",
47
- "lint": "node --experimental-strip-types tools/stego-cli.ts lint",
48
- "validate": "node --experimental-strip-types tools/stego-cli.ts validate",
49
- "build": "node --experimental-strip-types tools/stego-cli.ts build",
50
- "check-stage": "node --experimental-strip-types tools/stego-cli.ts check-stage",
51
- "export": "node --experimental-strip-types tools/stego-cli.ts export",
44
+ "list-projects": "node --experimental-strip-types src/main.ts list-projects",
45
+ "new-project": "node --experimental-strip-types src/main.ts new-project",
46
+ "new": "node --experimental-strip-types src/main.ts new",
47
+ "lint": "node --experimental-strip-types src/main.ts lint",
48
+ "validate": "node --experimental-strip-types src/main.ts validate",
49
+ "build": "node --experimental-strip-types src/main.ts build",
50
+ "check-stage": "node --experimental-strip-types src/main.ts check-stage",
51
+ "export": "node --experimental-strip-types src/main.ts export",
52
52
  "test:compile-structure": "node --test tools/test/compile-structure.test.mjs",
53
53
  "test:comments": "node --test tools/test/comments-add.test.mjs",
54
+ "test:cli-core": "node --test tools/test/cli-core.test.mjs",
54
55
  "test:spine-v2": "node --test tools/test/spine-v2.test.mjs",
55
- "test": "npm run test:compile-structure && npm run test:comments && npm run test:spine-v2"
56
+ "test:workspace-project": "node --test tools/test/workspace-project.test.mjs",
57
+ "test": "npm run test:compile-structure && npm run test:comments && npm run test:cli-core && npm run test:spine-v2 && npm run test:workspace-project && npm run check:module-apis && npm run check:boundaries",
58
+ "check:boundaries": "node scripts/check-module-boundaries.mjs",
59
+ "check:module-apis": "node scripts/check-module-apis.mjs"
56
60
  },
57
61
  "devDependencies": {
58
62
  "@changesets/cli": "^2.29.8",
63
+ "@types/js-yaml": "^4.0.9",
59
64
  "@types/node": "^25.2.3",
60
65
  "cspell": "^9.6.4",
61
66
  "markdownlint-cli": "^0.47.0",
62
67
  "pandoc": "^0.2.0",
63
68
  "typescript": "^5.9.3"
69
+ },
70
+ "dependencies": {
71
+ "cac": "^7.0.0",
72
+ "js-yaml": "^4.1.1"
64
73
  }
65
74
  }