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
@@ -1,499 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { addCommentToState, clearResolvedInState, deleteCommentInState, ensureNoParseErrors, loadCommentDocumentState, renderStateDocument, replyToCommentInState, serializeLoadedState, setCommentStatusInState, syncAnchorsInState } from "./comment-domain.js";
4
- import { CommentsCommandError } from "./errors.js";
5
- /** Handles `stego comments ...` command group. */
6
- export async function runCommentsCommand(options, cwd) {
7
- const [subcommand, manuscriptArg] = options._;
8
- if (!subcommand) {
9
- throw new CommentsCommandError("INVALID_USAGE", 2, "Comments subcommand is required. Use: read, add, reply, set-status, delete, clear-resolved, sync-anchors.");
10
- }
11
- if (!manuscriptArg) {
12
- throw new CommentsCommandError("INVALID_USAGE", 2, "Manuscript path is required. Use: stego comments <subcommand> <manuscript> ...");
13
- }
14
- const outputFormat = parseOutputFormat(readString(options, "format"));
15
- const manuscriptPath = path.resolve(cwd, manuscriptArg);
16
- const raw = readManuscript(manuscriptPath, manuscriptArg);
17
- let state = loadCommentDocumentState(raw);
18
- switch (subcommand) {
19
- case "read": {
20
- emitSuccess({
21
- operation: "read",
22
- manuscript: manuscriptPath,
23
- state: serializeLoadedState(state)
24
- }, outputFormat, `Read comments for ${path.relative(cwd, manuscriptPath) || manuscriptPath}.`);
25
- return;
26
- }
27
- case "add": {
28
- ensureMutableState(state, outputFormat);
29
- const addInput = resolveAddInput(options, cwd, outputFormat);
30
- const result = runMutation(manuscriptPath, state, outputFormat, () => addCommentToState(raw, state, addInput), (mutationResult) => mutationResult.comments);
31
- state = result.state;
32
- emitSuccess({
33
- operation: "add",
34
- manuscript: manuscriptPath,
35
- commentId: result.meta.commentId,
36
- state: serializeLoadedState(state)
37
- }, outputFormat, `Added ${result.meta.commentId} to ${path.relative(cwd, manuscriptPath) || manuscriptPath}.`);
38
- return;
39
- }
40
- case "reply": {
41
- ensureMutableState(state, outputFormat);
42
- const commentId = requireCommentId(options, outputFormat);
43
- const { message, author } = resolveReplyInput(options, cwd, outputFormat);
44
- const result = runMutation(manuscriptPath, state, outputFormat, () => replyToCommentInState(state, { commentId, message, author }), (mutationResult) => mutationResult.comments);
45
- state = result.state;
46
- emitSuccess({
47
- operation: "reply",
48
- manuscript: manuscriptPath,
49
- commentId: result.meta.commentId,
50
- state: serializeLoadedState(state)
51
- }, outputFormat, `Added reply ${result.meta.commentId} in ${path.relative(cwd, manuscriptPath) || manuscriptPath}.`);
52
- return;
53
- }
54
- case "set-status": {
55
- ensureMutableState(state, outputFormat);
56
- const commentId = requireCommentId(options, outputFormat);
57
- const status = parseStatus(readString(options, "status"), outputFormat);
58
- const thread = options.thread === true;
59
- const result = runMutation(manuscriptPath, state, outputFormat, () => setCommentStatusInState(state, { commentId, status, thread }), (mutationResult) => mutationResult.comments);
60
- state = result.state;
61
- emitSuccess({
62
- operation: "set-status",
63
- manuscript: manuscriptPath,
64
- status,
65
- changedIds: result.meta.changedIds,
66
- state: serializeLoadedState(state)
67
- }, outputFormat, `Updated ${result.meta.changedIds.length} comment(s) to '${status}'.`);
68
- return;
69
- }
70
- case "delete": {
71
- ensureMutableState(state, outputFormat);
72
- const commentId = requireCommentId(options, outputFormat);
73
- const result = runMutation(manuscriptPath, state, outputFormat, () => deleteCommentInState(state, commentId), (mutationResult) => mutationResult.comments);
74
- state = result.state;
75
- emitSuccess({
76
- operation: "delete",
77
- manuscript: manuscriptPath,
78
- removed: result.meta.removed,
79
- state: serializeLoadedState(state)
80
- }, outputFormat, `Deleted ${result.meta.removed} comment(s).`);
81
- return;
82
- }
83
- case "clear-resolved": {
84
- ensureMutableState(state, outputFormat);
85
- const result = runMutation(manuscriptPath, state, outputFormat, () => clearResolvedInState(state), (mutationResult) => mutationResult.comments);
86
- state = result.state;
87
- emitSuccess({
88
- operation: "clear-resolved",
89
- manuscript: manuscriptPath,
90
- removed: result.meta.removed,
91
- state: serializeLoadedState(state)
92
- }, outputFormat, `Cleared ${result.meta.removed} resolved comment(s).`);
93
- return;
94
- }
95
- case "sync-anchors": {
96
- ensureMutableState(state, outputFormat);
97
- const payload = readInputPayload(requireInputPath(options, outputFormat), cwd, outputFormat);
98
- const syncInput = parseSyncInput(payload, outputFormat);
99
- const result = runMutation(manuscriptPath, state, outputFormat, () => syncAnchorsInState(raw, state, syncInput), (mutationResult) => mutationResult.comments);
100
- state = result.state;
101
- emitSuccess({
102
- operation: "sync-anchors",
103
- manuscript: manuscriptPath,
104
- updatedCount: result.meta.updatedCount,
105
- deletedCount: result.meta.deletedCount,
106
- state: serializeLoadedState(state)
107
- }, outputFormat, `Synced anchors (${result.meta.updatedCount} updated, ${result.meta.deletedCount} deleted).`);
108
- return;
109
- }
110
- default:
111
- throw new CommentsCommandError("INVALID_USAGE", 2, `Unknown comments subcommand '${subcommand}'. Use: read, add, reply, set-status, delete, clear-resolved, sync-anchors.`, outputFormat);
112
- }
113
- }
114
- function runMutation(manuscriptPath, state, outputFormat, run, getComments) {
115
- let mutationResult;
116
- try {
117
- mutationResult = run();
118
- }
119
- catch (error) {
120
- const message = error instanceof Error ? error.message : String(error);
121
- throw new CommentsCommandError("INVALID_PAYLOAD", 4, message, outputFormat);
122
- }
123
- const nextComments = getComments(mutationResult);
124
- const nextText = renderStateDocument(state, nextComments);
125
- try {
126
- fs.writeFileSync(manuscriptPath, nextText, "utf8");
127
- }
128
- catch (error) {
129
- const message = error instanceof Error ? error.message : String(error);
130
- throw new CommentsCommandError("WRITE_FAILURE", 6, `Failed to update manuscript: ${message}`, outputFormat);
131
- }
132
- const nextState = loadCommentDocumentState(nextText);
133
- return {
134
- meta: mutationResult,
135
- state: nextState
136
- };
137
- }
138
- function ensureMutableState(state, outputFormat) {
139
- try {
140
- ensureNoParseErrors(state);
141
- }
142
- catch (error) {
143
- const message = error instanceof Error ? error.message : String(error);
144
- throw new CommentsCommandError("COMMENT_APPENDIX_INVALID", 5, message, outputFormat);
145
- }
146
- }
147
- function readManuscript(absolutePath, originalArg) {
148
- try {
149
- const stat = fs.statSync(absolutePath);
150
- if (!stat.isFile()) {
151
- throw new CommentsCommandError("INVALID_USAGE", 2, `Manuscript file not found: ${originalArg}`);
152
- }
153
- }
154
- catch {
155
- throw new CommentsCommandError("INVALID_USAGE", 2, `Manuscript file not found: ${originalArg}`);
156
- }
157
- try {
158
- return fs.readFileSync(absolutePath, "utf8");
159
- }
160
- catch (error) {
161
- const message = error instanceof Error ? error.message : String(error);
162
- throw new CommentsCommandError("INVALID_USAGE", 2, `Unable to read manuscript: ${message}`);
163
- }
164
- }
165
- function emitSuccess(payload, outputFormat, textMessage) {
166
- if (outputFormat === "json") {
167
- console.log(JSON.stringify({ ok: true, ...payload }, null, 2));
168
- return;
169
- }
170
- console.log(textMessage);
171
- }
172
- function readString(options, key) {
173
- const value = options[key];
174
- return typeof value === "string" ? value : undefined;
175
- }
176
- function parseOutputFormat(raw) {
177
- if (!raw || raw === "text") {
178
- return "text";
179
- }
180
- if (raw === "json") {
181
- return "json";
182
- }
183
- throw new CommentsCommandError("INVALID_USAGE", 2, "Invalid --format value. Use 'text' or 'json'.");
184
- }
185
- function requireInputPath(options, outputFormat) {
186
- const value = readString(options, "input");
187
- if (!value) {
188
- throw new CommentsCommandError("INVALID_USAGE", 2, "--input <path|-> is required for this command.", outputFormat);
189
- }
190
- return value;
191
- }
192
- function readInputPayload(inputPath, cwd, outputFormat) {
193
- let rawJson = "";
194
- try {
195
- rawJson = inputPath === "-"
196
- ? fs.readFileSync(0, "utf8")
197
- : fs.readFileSync(path.resolve(cwd, inputPath), "utf8");
198
- }
199
- catch (error) {
200
- const message = error instanceof Error ? error.message : String(error);
201
- throw new CommentsCommandError("INVALID_PAYLOAD", 4, `Unable to read input payload: ${message}`, outputFormat);
202
- }
203
- let parsed;
204
- try {
205
- parsed = JSON.parse(rawJson);
206
- }
207
- catch {
208
- throw new CommentsCommandError("INVALID_PAYLOAD", 4, "Input payload is not valid JSON.", outputFormat);
209
- }
210
- if (!isPlainObject(parsed)) {
211
- throw new CommentsCommandError("INVALID_PAYLOAD", 4, "Input payload must be a JSON object.", outputFormat);
212
- }
213
- return parsed;
214
- }
215
- function resolveAddInput(options, cwd, outputFormat) {
216
- const messageOption = readString(options, "message");
217
- const inputOption = readString(options, "input");
218
- if (messageOption && inputOption) {
219
- throw new CommentsCommandError("INVALID_USAGE", 2, "Use exactly one payload mode: --message <text> OR --input <path|->.", outputFormat);
220
- }
221
- if (!messageOption && !inputOption) {
222
- throw new CommentsCommandError("INVALID_USAGE", 2, "Missing payload. Provide --message <text> or --input <path|->.", outputFormat);
223
- }
224
- const payload = inputOption ? readInputPayload(inputOption, cwd, outputFormat) : undefined;
225
- const payloadMessage = coerceNonEmptyString(payload?.message, "Input payload 'message' must be a non-empty string.", outputFormat);
226
- const payloadAuthor = coerceOptionalString(payload?.author, "Input payload 'author' must be a string if provided.", outputFormat);
227
- const payloadMeta = payload?.meta !== undefined
228
- ? coerceOptionalObject(payload.meta, "Input payload 'meta' must be an object if provided.", outputFormat)
229
- : undefined;
230
- const optionRange = parseRangeFromOptions(options, outputFormat);
231
- const optionCursorLine = parseCursorLineOption(readString(options, "cursor-line"), outputFormat);
232
- const payloadRange = payload?.range !== undefined
233
- ? parsePayloadRange(payload.range, outputFormat)
234
- : undefined;
235
- const payloadAnchor = payload?.anchor !== undefined
236
- ? parseAnchorPayload(payload.anchor, outputFormat)
237
- : undefined;
238
- const finalMessage = (messageOption ?? payloadMessage ?? "").trim();
239
- if (!finalMessage) {
240
- throw new CommentsCommandError("INVALID_PAYLOAD", 4, "Comment message cannot be empty.", outputFormat);
241
- }
242
- const finalAuthor = (readString(options, "author") ?? payloadAuthor ?? "Saurus").trim() || "Saurus";
243
- const anchor = {
244
- range: optionRange ?? payloadAnchor?.range ?? payloadRange,
245
- cursorLine: optionCursorLine ?? payloadAnchor?.cursorLine,
246
- excerpt: payloadAnchor?.excerpt
247
- };
248
- const resolvedAnchor = anchor.range || anchor.cursorLine !== undefined || anchor.excerpt
249
- ? anchor
250
- : undefined;
251
- return {
252
- message: finalMessage,
253
- author: finalAuthor,
254
- anchor: resolvedAnchor,
255
- meta: payloadMeta
256
- };
257
- }
258
- function resolveReplyInput(options, cwd, outputFormat) {
259
- const messageOption = readString(options, "message");
260
- const inputOption = readString(options, "input");
261
- if (messageOption && inputOption) {
262
- throw new CommentsCommandError("INVALID_USAGE", 2, "Use exactly one payload mode: --message <text> OR --input <path|->.", outputFormat);
263
- }
264
- if (!messageOption && !inputOption) {
265
- throw new CommentsCommandError("INVALID_USAGE", 2, "Missing payload. Provide --message <text> or --input <path|->.", outputFormat);
266
- }
267
- const payload = inputOption ? readInputPayload(inputOption, cwd, outputFormat) : undefined;
268
- const payloadMessage = coerceNonEmptyString(payload?.message, "Input payload 'message' must be a non-empty string.", outputFormat);
269
- const payloadAuthor = coerceOptionalString(payload?.author, "Input payload 'author' must be a string if provided.", outputFormat);
270
- const finalMessage = (messageOption ?? payloadMessage ?? "").trim();
271
- if (!finalMessage) {
272
- throw new CommentsCommandError("INVALID_PAYLOAD", 4, "Reply cannot be empty.", outputFormat);
273
- }
274
- const finalAuthor = (readString(options, "author") ?? payloadAuthor ?? "Saurus").trim() || "Saurus";
275
- return {
276
- message: finalMessage,
277
- author: finalAuthor
278
- };
279
- }
280
- function parseSyncInput(payload, outputFormat) {
281
- const candidate = payload;
282
- const updates = candidate.updates === undefined
283
- ? []
284
- : parseAnchorUpdates(candidate.updates, outputFormat);
285
- const deleteIdsRaw = candidate.delete_ids ?? candidate.deleteIds;
286
- const deleteIds = deleteIdsRaw === undefined
287
- ? []
288
- : parseDeleteIds(deleteIdsRaw, outputFormat);
289
- return {
290
- updates,
291
- deleteIds
292
- };
293
- }
294
- function parseAnchorUpdates(raw, outputFormat) {
295
- if (!Array.isArray(raw)) {
296
- throw new CommentsCommandError("INVALID_PAYLOAD", 4, "Input payload 'updates' must be an array.", outputFormat);
297
- }
298
- const updates = [];
299
- for (const candidate of raw) {
300
- if (!isPlainObject(candidate)) {
301
- throw new CommentsCommandError("INVALID_PAYLOAD", 4, "Each update must be an object.", outputFormat);
302
- }
303
- const id = typeof candidate.id === "string" ? candidate.id.trim().toUpperCase() : "";
304
- if (!/^CMT-\d{4,}$/.test(id)) {
305
- throw new CommentsCommandError("INVALID_PAYLOAD", 4, "Each update.id must be a comment id like CMT-0001.", outputFormat);
306
- }
307
- const start = isPlainObject(candidate.start) ? candidate.start : undefined;
308
- const end = isPlainObject(candidate.end) ? candidate.end : undefined;
309
- if (!start || !end) {
310
- throw new CommentsCommandError("INVALID_PAYLOAD", 4, "Each update must include start and end positions.", outputFormat);
311
- }
312
- const parsed = {
313
- id,
314
- start: {
315
- line: parseJsonPositiveInt(start.line, "updates[].start.line", outputFormat),
316
- col: parseJsonNonNegativeInt(start.col, "updates[].start.col", outputFormat)
317
- },
318
- end: {
319
- line: parseJsonPositiveInt(end.line, "updates[].end.line", outputFormat),
320
- col: parseJsonNonNegativeInt(end.col, "updates[].end.col", outputFormat)
321
- }
322
- };
323
- const valid = parsed.start.line < parsed.end.line
324
- || (parsed.start.line === parsed.end.line && parsed.start.col < parsed.end.col);
325
- if (!valid) {
326
- throw new CommentsCommandError("INVALID_PAYLOAD", 4, "Each update range end must be after start.", outputFormat);
327
- }
328
- updates.push(parsed);
329
- }
330
- return updates;
331
- }
332
- function parseDeleteIds(raw, outputFormat) {
333
- if (!Array.isArray(raw)) {
334
- throw new CommentsCommandError("INVALID_PAYLOAD", 4, "Input payload 'delete_ids' must be an array.", outputFormat);
335
- }
336
- const ids = [];
337
- for (const value of raw) {
338
- if (typeof value !== "string") {
339
- throw new CommentsCommandError("INVALID_PAYLOAD", 4, "delete_ids values must be strings.", outputFormat);
340
- }
341
- const normalized = value.trim().toUpperCase();
342
- if (!/^CMT-\d{4,}$/.test(normalized)) {
343
- throw new CommentsCommandError("INVALID_PAYLOAD", 4, `Invalid delete_ids entry '${value}'.`, outputFormat);
344
- }
345
- ids.push(normalized);
346
- }
347
- return ids;
348
- }
349
- function parseAnchorPayload(raw, outputFormat) {
350
- if (!isPlainObject(raw)) {
351
- throw new CommentsCommandError("INVALID_PAYLOAD", 4, "Input payload 'anchor' must be an object.", outputFormat);
352
- }
353
- const parsed = raw;
354
- const range = parsed.range !== undefined
355
- ? parsePayloadRange(parsed.range, outputFormat)
356
- : undefined;
357
- const cursorLine = parsed.cursor_line !== undefined
358
- ? parseJsonPositiveInt(parsed.cursor_line, "anchor.cursor_line", outputFormat)
359
- : undefined;
360
- const excerpt = parsed.excerpt !== undefined
361
- ? coerceOptionalString(parsed.excerpt, "Input payload 'anchor.excerpt' must be a string if provided.", outputFormat)
362
- : undefined;
363
- return {
364
- range,
365
- cursorLine,
366
- excerpt
367
- };
368
- }
369
- function parseRangeFromOptions(options, outputFormat) {
370
- const rawStartLine = readString(options, "start-line");
371
- const rawStartCol = readString(options, "start-col");
372
- const rawEndLine = readString(options, "end-line");
373
- const rawEndCol = readString(options, "end-col");
374
- const values = [rawStartLine, rawStartCol, rawEndLine, rawEndCol];
375
- const hasAny = values.some((value) => value !== undefined);
376
- const hasAll = values.every((value) => value !== undefined);
377
- if (!hasAny) {
378
- return undefined;
379
- }
380
- if (!hasAll) {
381
- throw new CommentsCommandError("INVALID_USAGE", 2, "Range options must include all of --start-line, --start-col, --end-line, --end-col.", outputFormat);
382
- }
383
- const range = {
384
- startLine: parsePositiveInt(rawStartLine, "--start-line", outputFormat),
385
- startCol: parseNonNegativeInt(rawStartCol, "--start-col", outputFormat),
386
- endLine: parsePositiveInt(rawEndLine, "--end-line", outputFormat),
387
- endCol: parseNonNegativeInt(rawEndCol, "--end-col", outputFormat)
388
- };
389
- validateRange(range, outputFormat);
390
- return range;
391
- }
392
- function parseCursorLineOption(raw, outputFormat) {
393
- if (raw === undefined) {
394
- return undefined;
395
- }
396
- return parsePositiveInt(raw, "--cursor-line", outputFormat);
397
- }
398
- function parsePayloadRange(raw, outputFormat) {
399
- if (!isPlainObject(raw)) {
400
- throw new CommentsCommandError("INVALID_PAYLOAD", 4, "Input payload range must be an object.", outputFormat);
401
- }
402
- const parsed = raw;
403
- const start = parsed.start;
404
- const end = parsed.end;
405
- if (!isPlainObject(start) || !isPlainObject(end)) {
406
- throw new CommentsCommandError("INVALID_PAYLOAD", 4, "Input payload range must include start and end objects.", outputFormat);
407
- }
408
- const range = {
409
- startLine: parseJsonPositiveInt(start.line, "range.start.line", outputFormat),
410
- startCol: parseJsonNonNegativeInt(start.col, "range.start.col", outputFormat),
411
- endLine: parseJsonPositiveInt(end.line, "range.end.line", outputFormat),
412
- endCol: parseJsonNonNegativeInt(end.col, "range.end.col", outputFormat)
413
- };
414
- validateRange(range, outputFormat);
415
- return range;
416
- }
417
- function requireCommentId(options, outputFormat) {
418
- const commentId = readString(options, "comment-id")?.trim().toUpperCase();
419
- if (!commentId || !/^CMT-\d{4,}$/.test(commentId)) {
420
- throw new CommentsCommandError("INVALID_USAGE", 2, "--comment-id CMT-#### is required.", outputFormat);
421
- }
422
- return commentId;
423
- }
424
- function parseStatus(raw, outputFormat) {
425
- const normalized = (raw ?? "").trim().toLowerCase();
426
- if (normalized === "open" || normalized === "resolved") {
427
- return normalized;
428
- }
429
- throw new CommentsCommandError("INVALID_USAGE", 2, "--status must be 'open' or 'resolved'.", outputFormat);
430
- }
431
- function validateRange(range, outputFormat) {
432
- const startsBeforeEnd = range.startLine < range.endLine
433
- || (range.startLine === range.endLine && range.startCol < range.endCol);
434
- if (!startsBeforeEnd) {
435
- throw new CommentsCommandError("INVALID_PAYLOAD", 4, "Range end must be after range start.", outputFormat);
436
- }
437
- }
438
- function parsePositiveInt(raw, label, outputFormat) {
439
- if (!/^\d+$/.test(raw)) {
440
- throw new CommentsCommandError("INVALID_USAGE", 2, `${label} must be a positive integer.`, outputFormat);
441
- }
442
- const value = Number.parseInt(raw, 10);
443
- if (!Number.isFinite(value) || value < 1) {
444
- throw new CommentsCommandError("INVALID_USAGE", 2, `${label} must be a positive integer.`, outputFormat);
445
- }
446
- return value;
447
- }
448
- function parseNonNegativeInt(raw, label, outputFormat) {
449
- if (!/^\d+$/.test(raw)) {
450
- throw new CommentsCommandError("INVALID_USAGE", 2, `${label} must be a non-negative integer.`, outputFormat);
451
- }
452
- const value = Number.parseInt(raw, 10);
453
- if (!Number.isFinite(value) || value < 0) {
454
- throw new CommentsCommandError("INVALID_USAGE", 2, `${label} must be a non-negative integer.`, outputFormat);
455
- }
456
- return value;
457
- }
458
- function parseJsonPositiveInt(raw, label, outputFormat) {
459
- if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 1) {
460
- throw new CommentsCommandError("INVALID_PAYLOAD", 4, `${label} must be a positive integer.`, outputFormat);
461
- }
462
- return raw;
463
- }
464
- function parseJsonNonNegativeInt(raw, label, outputFormat) {
465
- if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 0) {
466
- throw new CommentsCommandError("INVALID_PAYLOAD", 4, `${label} must be a non-negative integer.`, outputFormat);
467
- }
468
- return raw;
469
- }
470
- function coerceNonEmptyString(value, message, outputFormat) {
471
- if (value === undefined) {
472
- return undefined;
473
- }
474
- if (typeof value !== "string" || value.trim().length === 0) {
475
- throw new CommentsCommandError("INVALID_PAYLOAD", 4, message, outputFormat);
476
- }
477
- return value;
478
- }
479
- function coerceOptionalString(value, message, outputFormat) {
480
- if (value === undefined) {
481
- return undefined;
482
- }
483
- if (typeof value !== "string") {
484
- throw new CommentsCommandError("INVALID_PAYLOAD", 4, message, outputFormat);
485
- }
486
- return value;
487
- }
488
- function coerceOptionalObject(value, message, outputFormat) {
489
- if (value === undefined) {
490
- return undefined;
491
- }
492
- if (!isPlainObject(value)) {
493
- throw new CommentsCommandError("INVALID_PAYLOAD", 4, message, outputFormat);
494
- }
495
- return value;
496
- }
497
- function isPlainObject(value) {
498
- return typeof value === "object" && value !== null && !Array.isArray(value);
499
- }
@@ -1,20 +0,0 @@
1
- /** Error raised for machine-facing comments command failures. */
2
- export class CommentsCommandError extends Error {
3
- code;
4
- exitCode;
5
- outputFormat;
6
- constructor(code, exitCode, message, outputFormat = "text") {
7
- super(message);
8
- this.name = "CommentsCommandError";
9
- this.code = code;
10
- this.exitCode = exitCode;
11
- this.outputFormat = outputFormat;
12
- }
13
- toJson() {
14
- return {
15
- ok: false,
16
- code: this.code,
17
- message: this.message
18
- };
19
- }
20
- }
@@ -1,127 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import process from "node:process";
4
- import { normalizeFrontmatterRecord, parseMarkdownDocument, serializeMarkdownDocument } from "./metadata-domain.js";
5
- export async function runMetadataCommand(options, cwd) {
6
- const [subcommand, markdownArg] = options._;
7
- if (!subcommand) {
8
- throw new Error("Metadata subcommand is required. Use: read, apply.");
9
- }
10
- if (!markdownArg) {
11
- throw new Error("Markdown path is required. Use: stego metadata <subcommand> <path>.");
12
- }
13
- const outputFormat = parseOutputFormat(readString(options, "format"));
14
- const absolutePath = path.resolve(cwd, markdownArg);
15
- const raw = readFile(absolutePath, markdownArg);
16
- switch (subcommand) {
17
- case "read": {
18
- const parsed = parseMarkdownDocument(raw);
19
- emit({
20
- ok: true,
21
- operation: "read",
22
- state: {
23
- path: absolutePath,
24
- hasFrontmatter: parsed.hasFrontmatter,
25
- lineEnding: parsed.lineEnding,
26
- frontmatter: parsed.frontmatter,
27
- body: parsed.body
28
- }
29
- }, outputFormat);
30
- return;
31
- }
32
- case "apply": {
33
- const inputPath = requireInputPath(options);
34
- const payload = readInputPayload(inputPath, cwd);
35
- const frontmatter = normalizeFrontmatterRecord(payload.frontmatter);
36
- const body = typeof payload.body === "string" ? payload.body : undefined;
37
- const hasFrontmatter = typeof payload.hasFrontmatter === "boolean" ? payload.hasFrontmatter : undefined;
38
- const existing = parseMarkdownDocument(raw);
39
- const next = {
40
- lineEnding: existing.lineEnding,
41
- hasFrontmatter: hasFrontmatter ?? (existing.hasFrontmatter || Object.keys(frontmatter).length > 0),
42
- frontmatter,
43
- body: body ?? existing.body
44
- };
45
- const nextText = serializeMarkdownDocument(next);
46
- const changed = nextText !== raw;
47
- if (changed) {
48
- fs.writeFileSync(absolutePath, nextText, "utf8");
49
- }
50
- const reparsed = parseMarkdownDocument(nextText);
51
- emit({
52
- ok: true,
53
- operation: "apply",
54
- changed,
55
- state: {
56
- path: absolutePath,
57
- hasFrontmatter: reparsed.hasFrontmatter,
58
- lineEnding: reparsed.lineEnding,
59
- frontmatter: reparsed.frontmatter,
60
- body: reparsed.body
61
- }
62
- }, outputFormat);
63
- return;
64
- }
65
- default:
66
- throw new Error(`Unknown metadata subcommand '${subcommand}'. Use: read, apply.`);
67
- }
68
- }
69
- function emit(payload, format) {
70
- if (format === "json") {
71
- process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
72
- return;
73
- }
74
- if (payload.operation === "read") {
75
- process.stdout.write(`Read metadata for ${payload.state.path} (${Object.keys(payload.state.frontmatter).length} keys).\n`);
76
- return;
77
- }
78
- process.stdout.write(`${payload.changed ? "Updated" : "No changes for"} metadata in ${payload.state.path}.\n`);
79
- }
80
- function parseOutputFormat(raw) {
81
- if (!raw || raw === "text") {
82
- return "text";
83
- }
84
- if (raw === "json") {
85
- return "json";
86
- }
87
- throw new Error("Invalid --format value. Use 'text' or 'json'.");
88
- }
89
- function readString(options, key) {
90
- const value = options[key];
91
- return typeof value === "string" ? value : undefined;
92
- }
93
- function requireInputPath(options) {
94
- const inputPath = readString(options, "input");
95
- if (!inputPath) {
96
- throw new Error("--input <path|-> is required for 'metadata apply'.");
97
- }
98
- return inputPath;
99
- }
100
- function readInputPayload(inputPath, cwd) {
101
- const raw = inputPath === "-"
102
- ? fs.readFileSync(process.stdin.fd, "utf8")
103
- : fs.readFileSync(path.resolve(cwd, inputPath), "utf8");
104
- let parsed;
105
- try {
106
- parsed = JSON.parse(raw);
107
- }
108
- catch {
109
- throw new Error("Input payload is not valid JSON.");
110
- }
111
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
112
- throw new Error("Input payload must be a JSON object.");
113
- }
114
- return parsed;
115
- }
116
- function readFile(filePath, originalArg) {
117
- try {
118
- const stat = fs.statSync(filePath);
119
- if (!stat.isFile()) {
120
- throw new Error();
121
- }
122
- }
123
- catch {
124
- throw new Error(`Markdown file not found: ${originalArg}`);
125
- }
126
- return fs.readFileSync(filePath, "utf8");
127
- }