stego-cli 0.4.1 → 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.
- package/README.md +6 -0
- package/dist/shared/src/contracts/cli/envelopes.js +19 -0
- package/dist/shared/src/contracts/cli/errors.js +14 -0
- package/dist/shared/src/contracts/cli/exit-codes.js +15 -0
- package/dist/shared/src/contracts/cli/index.js +6 -0
- package/dist/shared/src/contracts/cli/metadata.js +1 -0
- package/dist/shared/src/contracts/cli/operations.js +1 -0
- package/dist/shared/src/domain/comments/anchors.js +1 -0
- package/dist/shared/src/domain/comments/index.js +4 -0
- package/dist/shared/src/domain/comments/serializer.js +1 -0
- package/dist/shared/src/domain/comments/thread-key.js +21 -0
- package/dist/shared/src/domain/frontmatter/index.js +3 -0
- package/dist/shared/src/domain/frontmatter/parser.js +34 -0
- package/dist/shared/src/domain/frontmatter/serializer.js +32 -0
- package/dist/shared/src/domain/frontmatter/validators.js +47 -0
- package/dist/shared/src/domain/project/index.js +4 -0
- package/dist/shared/src/domain/stages/index.js +20 -0
- package/dist/shared/src/index.js +6 -0
- package/dist/shared/src/utils/guards.js +6 -0
- package/dist/shared/src/utils/index.js +3 -0
- package/dist/shared/src/utils/invariant.js +5 -0
- package/dist/shared/src/utils/result.js +6 -0
- package/dist/stego-cli/src/app/cli-version.js +32 -0
- package/dist/stego-cli/src/app/command-context.js +1 -0
- package/dist/stego-cli/src/app/command-registry.js +121 -0
- package/dist/stego-cli/src/app/create-cli-app.js +42 -0
- package/dist/stego-cli/src/app/error-boundary.js +42 -0
- package/dist/stego-cli/src/app/index.js +6 -0
- package/dist/stego-cli/src/app/output-renderer.js +6 -0
- package/dist/stego-cli/src/main.js +14 -0
- package/dist/stego-cli/src/modules/comments/application/comment-operations.js +457 -0
- package/dist/stego-cli/src/modules/comments/commands/comments-add.js +40 -0
- package/dist/stego-cli/src/modules/comments/commands/comments-clear-resolved.js +32 -0
- package/dist/stego-cli/src/modules/comments/commands/comments-delete.js +33 -0
- package/dist/stego-cli/src/modules/comments/commands/comments-read.js +32 -0
- package/dist/stego-cli/src/modules/comments/commands/comments-reply.js +36 -0
- package/dist/stego-cli/src/modules/comments/commands/comments-set-status.js +35 -0
- package/dist/stego-cli/src/modules/comments/commands/comments-sync-anchors.js +33 -0
- package/dist/stego-cli/src/modules/comments/domain/comment-policy.js +14 -0
- package/dist/stego-cli/src/modules/comments/index.js +20 -0
- package/dist/stego-cli/src/modules/comments/infra/comments-repo.js +68 -0
- package/dist/stego-cli/src/modules/comments/types.js +1 -0
- package/dist/stego-cli/src/modules/compile/application/compile-manuscript.js +16 -0
- package/dist/stego-cli/src/modules/compile/commands/build.js +51 -0
- package/dist/stego-cli/src/modules/compile/domain/compile-structure.js +105 -0
- package/dist/stego-cli/src/modules/compile/index.js +8 -0
- package/dist/stego-cli/src/modules/compile/infra/dist-writer.js +8 -0
- package/dist/stego-cli/src/modules/compile/types.js +1 -0
- package/dist/stego-cli/src/modules/export/application/run-export.js +29 -0
- package/dist/stego-cli/src/modules/export/commands/export.js +61 -0
- package/dist/stego-cli/src/modules/export/domain/exporter.js +6 -0
- package/dist/stego-cli/src/modules/export/index.js +8 -0
- package/dist/stego-cli/src/modules/export/types.js +1 -0
- package/dist/stego-cli/src/modules/index.js +22 -0
- package/dist/stego-cli/src/modules/manuscript/application/create-manuscript.js +107 -0
- package/dist/stego-cli/src/modules/manuscript/application/order-inference.js +56 -0
- package/dist/stego-cli/src/modules/manuscript/commands/new-manuscript.js +54 -0
- package/dist/stego-cli/src/modules/manuscript/domain/manuscript.js +1 -0
- package/dist/stego-cli/src/modules/manuscript/index.js +9 -0
- package/dist/stego-cli/src/modules/manuscript/infra/manuscript-repo.js +39 -0
- package/dist/stego-cli/src/modules/manuscript/types.js +1 -0
- package/dist/stego-cli/src/modules/metadata/application/apply-metadata.js +45 -0
- package/dist/stego-cli/src/modules/metadata/application/read-metadata.js +18 -0
- package/dist/stego-cli/src/modules/metadata/commands/metadata-apply.js +38 -0
- package/dist/stego-cli/src/modules/metadata/commands/metadata-read.js +33 -0
- package/dist/stego-cli/src/modules/metadata/domain/metadata.js +1 -0
- package/dist/stego-cli/src/modules/metadata/index.js +11 -0
- package/dist/stego-cli/src/modules/metadata/infra/metadata-repo.js +68 -0
- package/dist/stego-cli/src/modules/metadata/types.js +1 -0
- package/dist/stego-cli/src/modules/project/application/create-project.js +203 -0
- package/dist/stego-cli/src/modules/project/application/infer-project.js +72 -0
- package/dist/stego-cli/src/modules/project/commands/new-project.js +86 -0
- package/dist/stego-cli/src/modules/project/domain/project.js +1 -0
- package/dist/stego-cli/src/modules/project/index.js +9 -0
- package/dist/stego-cli/src/modules/project/infra/project-repo.js +27 -0
- package/dist/stego-cli/src/modules/project/types.js +1 -0
- package/dist/stego-cli/src/modules/quality/application/inspect-project.js +603 -0
- package/dist/stego-cli/src/modules/quality/application/lint-runner.js +313 -0
- package/dist/stego-cli/src/modules/quality/application/stage-check.js +87 -0
- package/dist/stego-cli/src/modules/quality/commands/check-stage.js +54 -0
- package/dist/stego-cli/src/modules/quality/commands/lint.js +51 -0
- package/dist/stego-cli/src/modules/quality/commands/validate.js +50 -0
- package/dist/stego-cli/src/modules/quality/domain/issues.js +1 -0
- package/dist/stego-cli/src/modules/quality/domain/policies.js +1 -0
- package/dist/stego-cli/src/modules/quality/index.js +14 -0
- package/dist/stego-cli/src/modules/quality/infra/cspell-adapter.js +1 -0
- package/dist/stego-cli/src/modules/quality/infra/markdownlint-adapter.js +1 -0
- package/dist/stego-cli/src/modules/quality/types.js +1 -0
- package/dist/stego-cli/src/modules/scaffold/application/scaffold-workspace.js +307 -0
- package/dist/stego-cli/src/modules/scaffold/commands/init.js +34 -0
- package/dist/stego-cli/src/modules/scaffold/domain/templates.js +182 -0
- package/dist/stego-cli/src/modules/scaffold/index.js +8 -0
- package/dist/stego-cli/src/modules/scaffold/infra/template-repo.js +33 -0
- package/dist/stego-cli/src/modules/scaffold/types.js +1 -0
- package/dist/stego-cli/src/modules/spine/application/create-category.js +14 -0
- package/dist/stego-cli/src/modules/spine/application/create-entry.js +9 -0
- package/dist/stego-cli/src/modules/spine/application/read-catalog.js +13 -0
- package/dist/stego-cli/src/modules/spine/commands/spine-deprecated-aliases.js +33 -0
- package/dist/stego-cli/src/modules/spine/commands/spine-new-category.js +64 -0
- package/dist/stego-cli/src/modules/spine/commands/spine-new-entry.js +65 -0
- package/dist/stego-cli/src/modules/spine/commands/spine-read.js +49 -0
- package/dist/{spine/spine-domain.js → stego-cli/src/modules/spine/domain/spine.js} +13 -7
- package/dist/stego-cli/src/modules/spine/index.js +16 -0
- package/dist/stego-cli/src/modules/spine/infra/spine-repo.js +46 -0
- package/dist/stego-cli/src/modules/spine/types.js +1 -0
- package/dist/stego-cli/src/modules/workspace/application/discover-projects.js +18 -0
- package/dist/stego-cli/src/modules/workspace/application/resolve-workspace.js +73 -0
- package/dist/stego-cli/src/modules/workspace/commands/list-projects.js +40 -0
- package/dist/stego-cli/src/modules/workspace/index.js +9 -0
- package/dist/stego-cli/src/modules/workspace/infra/workspace-repo.js +37 -0
- package/dist/stego-cli/src/modules/workspace/types.js +1 -0
- package/dist/stego-cli/src/platform/clock.js +3 -0
- package/dist/stego-cli/src/platform/fs.js +13 -0
- package/dist/stego-cli/src/platform/index.js +3 -0
- package/dist/stego-cli/src/platform/temp-files.js +6 -0
- package/package.json +20 -11
- package/dist/comments/comments-command.js +0 -499
- package/dist/comments/errors.js +0 -20
- package/dist/metadata/metadata-command.js +0 -127
- package/dist/metadata/metadata-domain.js +0 -209
- package/dist/spine/spine-command.js +0 -129
- package/dist/stego-cli.js +0 -2264
- /package/dist/{exporters/exporter-types.js → shared/src/contracts/cli/comments.js} +0 -0
- /package/dist/{comments/comment-domain.js → shared/src/domain/comments/parser.js} +0 -0
- /package/dist/{exporters → stego-cli/src/modules/export/infra}/markdown-exporter.js +0 -0
- /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
|
+
}
|