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