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