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.
- package/dist/comments/comment-domain.js +919 -0
- package/dist/comments/comments-command.js +356 -64
- package/dist/stego-cli.js +19 -241
- package/package.json +1 -1
- package/dist/comments/add-comment.js +0 -182
|
@@ -1,68 +1,173 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
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
|
|
9
|
-
throw new CommentsCommandError("INVALID_USAGE", 2, "
|
|
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
|
|
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
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
21
|
-
|
|
113
|
+
}
|
|
114
|
+
function runMutation(manuscriptPath, state, outputFormat, run, getComments) {
|
|
115
|
+
let mutationResult;
|
|
116
|
+
try {
|
|
117
|
+
mutationResult = run();
|
|
22
118
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
38
|
-
const
|
|
39
|
-
let result;
|
|
123
|
+
const nextComments = getComments(mutationResult);
|
|
124
|
+
const nextText = renderStateDocument(state, nextComments);
|
|
40
125
|
try {
|
|
41
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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(
|
|
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:
|
|
384
|
+
startLine: parsePositiveInt(rawStartLine, "--start-line", outputFormat),
|
|
119
385
|
startCol: parseNonNegativeInt(rawStartCol, "--start-col", outputFormat),
|
|
120
|
-
endLine:
|
|
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
|
|
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
|
|
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:
|
|
148
|
-
startCol:
|
|
149
|
-
endLine:
|
|
150
|
-
endCol:
|
|
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);
|