stego-cli 0.3.2 → 0.3.4

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.
@@ -1,68 +1,173 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { addCommentToManuscript } from "./add-comment.js";
3
+ import { addCommentToState, clearResolvedInState, deleteCommentInState, ensureNoParseErrors, loadCommentDocumentState, renderStateDocument, replyToCommentInState, serializeLoadedState, setCommentStatusInState, syncAnchorsInState } from "./comment-domain.js";
4
4
  import { CommentsCommandError } from "./errors.js";
5
5
  /** Handles `stego comments ...` command group. */
6
6
  export async function runCommentsCommand(options, cwd) {
7
7
  const [subcommand, manuscriptArg] = options._;
8
- if (subcommand !== "add") {
9
- throw new CommentsCommandError("INVALID_USAGE", 2, "Unknown comments subcommand. Use: stego comments add <manuscript> [--message <text> | --input <path|->].");
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
10
  }
11
11
  if (!manuscriptArg) {
12
- throw new CommentsCommandError("INVALID_USAGE", 2, "Manuscript path is required. Use: stego comments add <manuscript> ...");
12
+ throw new CommentsCommandError("INVALID_USAGE", 2, "Manuscript path is required. Use: stego comments <subcommand> <manuscript> ...");
13
13
  }
14
14
  const outputFormat = parseOutputFormat(readString(options, "format"));
15
- const messageOption = readString(options, "message");
16
- const inputOption = readString(options, "input");
17
- if (messageOption && inputOption) {
18
- throw new CommentsCommandError("INVALID_USAGE", 2, "Use exactly one payload mode: --message <text> OR --input <path|->.", outputFormat);
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);
19
112
  }
20
- if (!messageOption && !inputOption) {
21
- throw new CommentsCommandError("INVALID_USAGE", 2, "Missing payload. Provide --message <text> or --input <path|->.", outputFormat);
113
+ }
114
+ function runMutation(manuscriptPath, state, outputFormat, run, getComments) {
115
+ let mutationResult;
116
+ try {
117
+ mutationResult = run();
22
118
  }
23
- const payload = inputOption ? readInputPayload(inputOption, cwd, outputFormat) : undefined;
24
- const payloadMessage = coerceNonEmptyString(payload?.message, "Input payload 'message' must be a non-empty string.", outputFormat);
25
- const payloadAuthor = coerceOptionalString(payload?.author, "Input payload 'author' must be a string if provided.", outputFormat);
26
- const payloadRange = payload?.range !== undefined
27
- ? parsePayloadRange(payload.range, outputFormat)
28
- : undefined;
29
- const payloadMeta = payload?.meta !== undefined
30
- ? coerceOptionalObject(payload.meta, "Input payload 'meta' must be an object if provided.", outputFormat)
31
- : undefined;
32
- const optionRange = parseRangeFromOptions(options, outputFormat);
33
- const finalMessage = (messageOption ?? payloadMessage ?? "").trim();
34
- if (!finalMessage) {
35
- throw new CommentsCommandError("INVALID_PAYLOAD", 4, "Comment message cannot be empty.", outputFormat);
119
+ catch (error) {
120
+ const message = error instanceof Error ? error.message : String(error);
121
+ throw new CommentsCommandError("INVALID_PAYLOAD", 4, message, outputFormat);
36
122
  }
37
- const finalAuthor = (readString(options, "author") ?? payloadAuthor ?? "Saurus").trim() || "Saurus";
38
- const finalRange = optionRange ?? payloadRange;
39
- let result;
123
+ const nextComments = getComments(mutationResult);
124
+ const nextText = renderStateDocument(state, nextComments);
40
125
  try {
41
- result = addCommentToManuscript({
42
- manuscriptPath: manuscriptArg,
43
- cwd,
44
- message: finalMessage,
45
- author: finalAuthor,
46
- range: finalRange,
47
- sourceMeta: payloadMeta
48
- });
126
+ fs.writeFileSync(manuscriptPath, nextText, "utf8");
49
127
  }
50
128
  catch (error) {
51
- if (error instanceof CommentsCommandError && outputFormat === "json" && error.outputFormat !== "json") {
52
- throw new CommentsCommandError(error.code, error.exitCode, error.message, outputFormat);
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}`);
53
152
  }
54
- throw error;
55
153
  }
56
- if (outputFormat === "json") {
57
- console.log(JSON.stringify(result, null, 2));
58
- return;
154
+ catch {
155
+ throw new CommentsCommandError("INVALID_USAGE", 2, `Manuscript file not found: ${originalArg}`);
156
+ }
157
+ try {
158
+ return fs.readFileSync(absolutePath, "utf8");
59
159
  }
60
- const relativePath = path.relative(cwd, result.manuscript) || result.manuscript;
61
- if (result.anchor.type === "selection") {
62
- console.log(`Added ${result.commentId} to ${relativePath} (selection anchor).`);
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));
63
168
  return;
64
169
  }
65
- console.log(`Added ${result.commentId} to ${relativePath} (file-level anchor).`);
170
+ console.log(textMessage);
66
171
  }
67
172
  function readString(options, key) {
68
173
  const value = options[key];
@@ -77,6 +182,13 @@ function parseOutputFormat(raw) {
77
182
  }
78
183
  throw new CommentsCommandError("INVALID_USAGE", 2, "Invalid --format value. Use 'text' or 'json'.");
79
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
+ }
80
192
  function readInputPayload(inputPath, cwd, outputFormat) {
81
193
  let rawJson = "";
82
194
  try {
@@ -100,6 +212,160 @@ function readInputPayload(inputPath, cwd, outputFormat) {
100
212
  }
101
213
  return parsed;
102
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
+ }
103
369
  function parseRangeFromOptions(options, outputFormat) {
104
370
  const rawStartLine = readString(options, "start-line");
105
371
  const rawStartCol = readString(options, "start-col");
@@ -115,43 +381,53 @@ function parseRangeFromOptions(options, outputFormat) {
115
381
  throw new CommentsCommandError("INVALID_USAGE", 2, "Range options must include all of --start-line, --start-col, --end-line, --end-col.", outputFormat);
116
382
  }
117
383
  const range = {
118
- startLine: parseNonNegativeInt(rawStartLine, "--start-line", outputFormat),
384
+ startLine: parsePositiveInt(rawStartLine, "--start-line", outputFormat),
119
385
  startCol: parseNonNegativeInt(rawStartCol, "--start-col", outputFormat),
120
- endLine: parseNonNegativeInt(rawEndLine, "--end-line", outputFormat),
386
+ endLine: parsePositiveInt(rawEndLine, "--end-line", outputFormat),
121
387
  endCol: parseNonNegativeInt(rawEndCol, "--end-col", outputFormat)
122
388
  };
123
389
  validateRange(range, outputFormat);
124
390
  return range;
125
391
  }
392
+ function parseCursorLineOption(raw, outputFormat) {
393
+ if (raw === undefined) {
394
+ return undefined;
395
+ }
396
+ return parsePositiveInt(raw, "--cursor-line", outputFormat);
397
+ }
126
398
  function parsePayloadRange(raw, outputFormat) {
127
399
  if (!isPlainObject(raw)) {
128
- throw new CommentsCommandError("INVALID_PAYLOAD", 4, "Input payload 'range' must be an object.", outputFormat);
400
+ throw new CommentsCommandError("INVALID_PAYLOAD", 4, "Input payload range must be an object.", outputFormat);
129
401
  }
130
402
  const parsed = raw;
131
403
  const start = parsed.start;
132
404
  const end = parsed.end;
133
405
  if (!isPlainObject(start) || !isPlainObject(end)) {
134
- throw new CommentsCommandError("INVALID_PAYLOAD", 4, "Input payload 'range' must include 'start' and 'end' objects.", outputFormat);
135
- }
136
- const normalized = {
137
- start: {
138
- line: parseJsonNonNegativeInt(start.line, "range.start.line", outputFormat),
139
- col: parseJsonNonNegativeInt(start.col, "range.start.col", outputFormat)
140
- },
141
- end: {
142
- line: parseJsonNonNegativeInt(end.line, "range.end.line", outputFormat),
143
- col: parseJsonNonNegativeInt(end.col, "range.end.col", outputFormat)
144
- }
145
- };
406
+ throw new CommentsCommandError("INVALID_PAYLOAD", 4, "Input payload range must include start and end objects.", outputFormat);
407
+ }
146
408
  const range = {
147
- startLine: normalized.start.line,
148
- startCol: normalized.start.col,
149
- endLine: normalized.end.line,
150
- endCol: normalized.end.col
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)
151
413
  };
152
414
  validateRange(range, outputFormat);
153
415
  return range;
154
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
+ }
155
431
  function validateRange(range, outputFormat) {
156
432
  const startsBeforeEnd = range.startLine < range.endLine
157
433
  || (range.startLine === range.endLine && range.startCol < range.endCol);
@@ -159,16 +435,32 @@ function validateRange(range, outputFormat) {
159
435
  throw new CommentsCommandError("INVALID_PAYLOAD", 4, "Range end must be after range start.", outputFormat);
160
436
  }
161
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
+ }
162
448
  function parseNonNegativeInt(raw, label, outputFormat) {
163
449
  if (!/^\d+$/.test(raw)) {
164
450
  throw new CommentsCommandError("INVALID_USAGE", 2, `${label} must be a non-negative integer.`, outputFormat);
165
451
  }
166
452
  const value = Number.parseInt(raw, 10);
167
- if (!Number.isFinite(value)) {
453
+ if (!Number.isFinite(value) || value < 0) {
168
454
  throw new CommentsCommandError("INVALID_USAGE", 2, `${label} must be a non-negative integer.`, outputFormat);
169
455
  }
170
456
  return value;
171
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
+ }
172
464
  function parseJsonNonNegativeInt(raw, label, outputFormat) {
173
465
  if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 0) {
174
466
  throw new CommentsCommandError("INVALID_PAYLOAD", 4, `${label} must be a non-negative integer.`, outputFormat);