@principal-ai/principal-studio-cli 0.35.3
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 +172 -0
- package/dist/commands/agent-session.d.ts +3 -0
- package/dist/commands/agent-session.d.ts.map +1 -0
- package/dist/commands/agent-session.js +67 -0
- package/dist/commands/agent-sessions.d.ts +11 -0
- package/dist/commands/agent-sessions.d.ts.map +1 -0
- package/dist/commands/agent-sessions.js +39 -0
- package/dist/commands/auxiliary/index.d.ts +3 -0
- package/dist/commands/auxiliary/index.d.ts.map +1 -0
- package/dist/commands/auxiliary/index.js +9 -0
- package/dist/commands/auxiliary/validate.d.ts +3 -0
- package/dist/commands/auxiliary/validate.d.ts.map +1 -0
- package/dist/commands/auxiliary/validate.js +123 -0
- package/dist/commands/collector/check.d.ts +6 -0
- package/dist/commands/collector/check.d.ts.map +1 -0
- package/dist/commands/collector/check.js +153 -0
- package/dist/commands/collector/diagnose.d.ts +6 -0
- package/dist/commands/collector/diagnose.d.ts.map +1 -0
- package/dist/commands/collector/diagnose.js +229 -0
- package/dist/commands/collector/index.d.ts +6 -0
- package/dist/commands/collector/index.d.ts.map +1 -0
- package/dist/commands/collector/index.js +16 -0
- package/dist/commands/collector/status.d.ts +6 -0
- package/dist/commands/collector/status.d.ts.map +1 -0
- package/dist/commands/collector/status.js +77 -0
- package/dist/commands/collector/utils.d.ts +92 -0
- package/dist/commands/collector/utils.d.ts.map +1 -0
- package/dist/commands/collector/utils.js +154 -0
- package/dist/commands/coverage.d.ts +9 -0
- package/dist/commands/coverage.d.ts.map +1 -0
- package/dist/commands/coverage.js +252 -0
- package/dist/commands/create.d.ts +6 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +50 -0
- package/dist/commands/events/index.d.ts +3 -0
- package/dist/commands/events/index.d.ts.map +1 -0
- package/dist/commands/events/index.js +9 -0
- package/dist/commands/events/validate.d.ts +3 -0
- package/dist/commands/events/validate.d.ts.map +1 -0
- package/dist/commands/events/validate.js +124 -0
- package/dist/commands/formats.d.ts +6 -0
- package/dist/commands/formats.d.ts.map +1 -0
- package/dist/commands/formats.js +1118 -0
- package/dist/commands/hooks.d.ts +9 -0
- package/dist/commands/hooks.d.ts.map +1 -0
- package/dist/commands/hooks.js +295 -0
- package/dist/commands/inbox.d.ts +13 -0
- package/dist/commands/inbox.d.ts.map +1 -0
- package/dist/commands/inbox.js +179 -0
- package/dist/commands/init.d.ts +6 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +220 -0
- package/dist/commands/lint.d.ts +11 -0
- package/dist/commands/lint.d.ts.map +1 -0
- package/dist/commands/lint.js +71 -0
- package/dist/commands/list.d.ts +6 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +78 -0
- package/dist/commands/migrate-nodes.d.ts +9 -0
- package/dist/commands/migrate-nodes.d.ts.map +1 -0
- package/dist/commands/migrate-nodes.js +363 -0
- package/dist/commands/migrate-scopes-to-canvas.d.ts +3 -0
- package/dist/commands/migrate-scopes-to-canvas.d.ts.map +1 -0
- package/dist/commands/migrate-scopes-to-canvas.js +143 -0
- package/dist/commands/migration.d.ts +6 -0
- package/dist/commands/migration.d.ts.map +1 -0
- package/dist/commands/migration.js +247 -0
- package/dist/commands/open-studio.d.ts +10 -0
- package/dist/commands/open-studio.d.ts.map +1 -0
- package/dist/commands/open-studio.js +32 -0
- package/dist/commands/opencode/fetch.d.ts +3 -0
- package/dist/commands/opencode/fetch.d.ts.map +1 -0
- package/dist/commands/opencode/fetch.js +25 -0
- package/dist/commands/opencode/index.d.ts +3 -0
- package/dist/commands/opencode/index.d.ts.map +1 -0
- package/dist/commands/opencode/index.js +11 -0
- package/dist/commands/opencode/list-sessions.d.ts +3 -0
- package/dist/commands/opencode/list-sessions.d.ts.map +1 -0
- package/dist/commands/opencode/list-sessions.js +20 -0
- package/dist/commands/repo.d.ts +18 -0
- package/dist/commands/repo.d.ts.map +1 -0
- package/dist/commands/repo.js +233 -0
- package/dist/commands/schema.d.ts +6 -0
- package/dist/commands/schema.d.ts.map +1 -0
- package/dist/commands/schema.js +367 -0
- package/dist/commands/scopes/index.d.ts +3 -0
- package/dist/commands/scopes/index.d.ts.map +1 -0
- package/dist/commands/scopes/index.js +9 -0
- package/dist/commands/scopes/validate.d.ts +3 -0
- package/dist/commands/scopes/validate.d.ts.map +1 -0
- package/dist/commands/scopes/validate.js +187 -0
- package/dist/commands/setup.d.ts +6 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +406 -0
- package/dist/commands/starred-collections.d.ts +12 -0
- package/dist/commands/starred-collections.d.ts.map +1 -0
- package/dist/commands/starred-collections.js +310 -0
- package/dist/commands/subsystem-model.d.ts +11 -0
- package/dist/commands/subsystem-model.d.ts.map +1 -0
- package/dist/commands/subsystem-model.js +249 -0
- package/dist/commands/topic.d.ts +15 -0
- package/dist/commands/topic.d.ts.map +1 -0
- package/dist/commands/topic.js +683 -0
- package/dist/commands/tour.d.ts +23 -0
- package/dist/commands/tour.d.ts.map +1 -0
- package/dist/commands/tour.js +783 -0
- package/dist/commands/trace/flow.d.ts +6 -0
- package/dist/commands/trace/flow.d.ts.map +1 -0
- package/dist/commands/trace/flow.js +158 -0
- package/dist/commands/trace/index.d.ts +6 -0
- package/dist/commands/trace/index.d.ts.map +1 -0
- package/dist/commands/trace/index.js +20 -0
- package/dist/commands/trace/inspect.d.ts +6 -0
- package/dist/commands/trace/inspect.d.ts.map +1 -0
- package/dist/commands/trace/inspect.js +187 -0
- package/dist/commands/trace/list.d.ts +6 -0
- package/dist/commands/trace/list.d.ts.map +1 -0
- package/dist/commands/trace/list.js +145 -0
- package/dist/commands/trace/registrations.d.ts +6 -0
- package/dist/commands/trace/registrations.d.ts.map +1 -0
- package/dist/commands/trace/registrations.js +81 -0
- package/dist/commands/trace/validate.d.ts +6 -0
- package/dist/commands/trace/validate.d.ts.map +1 -0
- package/dist/commands/trace/validate.js +276 -0
- package/dist/commands/trail.d.ts +36 -0
- package/dist/commands/trail.d.ts.map +1 -0
- package/dist/commands/trail.js +622 -0
- package/dist/commands/validate.d.ts +12 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +3334 -0
- package/dist/commands/workflow/implementation.d.ts +3 -0
- package/dist/commands/workflow/implementation.d.ts.map +1 -0
- package/dist/commands/workflow/implementation.js +51 -0
- package/dist/commands/workflow/index.d.ts +3 -0
- package/dist/commands/workflow/index.d.ts.map +1 -0
- package/dist/commands/workflow/index.js +19 -0
- package/dist/commands/workflow/inspect.d.ts +3 -0
- package/dist/commands/workflow/inspect.d.ts.map +1 -0
- package/dist/commands/workflow/inspect.js +123 -0
- package/dist/commands/workflow/list.d.ts +3 -0
- package/dist/commands/workflow/list.d.ts.map +1 -0
- package/dist/commands/workflow/list.js +95 -0
- package/dist/commands/workflow/render.d.ts +3 -0
- package/dist/commands/workflow/render.d.ts.map +1 -0
- package/dist/commands/workflow/render.js +88 -0
- package/dist/commands/workflow/test.d.ts +3 -0
- package/dist/commands/workflow/test.d.ts.map +1 -0
- package/dist/commands/workflow/test.js +141 -0
- package/dist/commands/workflow/utils.d.ts +49 -0
- package/dist/commands/workflow/utils.d.ts.map +1 -0
- package/dist/commands/workflow/utils.js +164 -0
- package/dist/commands/workflow/validate.d.ts +3 -0
- package/dist/commands/workflow/validate.d.ts.map +1 -0
- package/dist/commands/workflow/validate.js +344 -0
- package/dist/file-utils.d.ts +15 -0
- package/dist/file-utils.d.ts.map +1 -0
- package/dist/file-utils.js +29 -0
- package/dist/index.cjs +285718 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +81 -0
- package/dist/lib/bridge-ipc.d.ts +29 -0
- package/dist/lib/bridge-ipc.d.ts.map +1 -0
- package/dist/lib/bridge-ipc.js +128 -0
- package/dist/lib/github-user.d.ts +17 -0
- package/dist/lib/github-user.d.ts.map +1 -0
- package/dist/lib/github-user.js +53 -0
- package/dist/lib/open-url.d.ts +11 -0
- package/dist/lib/open-url.d.ts.map +1 -0
- package/dist/lib/open-url.js +45 -0
- package/dist/lib/subsystem-model-store.d.ts +74 -0
- package/dist/lib/subsystem-model-store.d.ts.map +1 -0
- package/dist/lib/subsystem-model-store.js +292 -0
- package/dist/lib/tour-cache.d.ts +26 -0
- package/dist/lib/tour-cache.d.ts.map +1 -0
- package/dist/lib/tour-cache.js +46 -0
- package/dist/lib/trail-cache.d.ts +40 -0
- package/dist/lib/trail-cache.d.ts.map +1 -0
- package/dist/lib/trail-cache.js +97 -0
- package/dist/lib/viewer-ipc.d.ts +44 -0
- package/dist/lib/viewer-ipc.d.ts.map +1 -0
- package/dist/lib/viewer-ipc.js +68 -0
- package/package.json +68 -0
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Topic command — create / list / view / add-trail on web-ade topics.
|
|
3
|
+
*
|
|
4
|
+
* A **topic** is a curated collection of trails on a shared subject. The
|
|
5
|
+
* topic record itself is `{ title, description, trailIds, owner }` — trails
|
|
6
|
+
* are referenced by id and continue to enforce their own repo-access checks
|
|
7
|
+
* on read. See docs/topics.md in web-ade for the full design.
|
|
8
|
+
*
|
|
9
|
+
* Resolves a GitHub token locally (gh CLI → git credential helper) and calls
|
|
10
|
+
* the web-ade topic API with `Authorization: Bearer <token>`. The token is
|
|
11
|
+
* never echoed to argv, env, stdout, or stderr.
|
|
12
|
+
*/
|
|
13
|
+
import { Command } from 'commander';
|
|
14
|
+
import { spawnSync } from 'node:child_process';
|
|
15
|
+
import { handoffTopicToBridge } from '../lib/bridge-ipc.js';
|
|
16
|
+
import { fetchGitHubMe, fetchGitHubUserByLogin } from '../lib/github-user.js';
|
|
17
|
+
import { openInBrowser } from '../lib/open-url.js';
|
|
18
|
+
const BASE_URL = 'https://app.principal-ade.com';
|
|
19
|
+
// Matches the v1 uuid shape web-ade mints for both trails and topics.
|
|
20
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Auth — same posture as `trail.ts`: gh CLI first, then git credential helper.
|
|
23
|
+
// Kept local so this command file is self-contained.
|
|
24
|
+
// ============================================================================
|
|
25
|
+
function resolveTokenViaGh() {
|
|
26
|
+
const result = spawnSync('gh', ['auth', 'token'], {
|
|
27
|
+
encoding: 'utf8',
|
|
28
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
29
|
+
});
|
|
30
|
+
if (result.status === 0 && result.stdout) {
|
|
31
|
+
const token = result.stdout.trim();
|
|
32
|
+
if (token)
|
|
33
|
+
return token;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
function resolveTokenViaGitCredential() {
|
|
38
|
+
const result = spawnSync('git', ['credential', 'fill'], {
|
|
39
|
+
encoding: 'utf8',
|
|
40
|
+
input: 'protocol=https\nhost=github.com\n\n',
|
|
41
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
42
|
+
});
|
|
43
|
+
if (result.status !== 0 || !result.stdout)
|
|
44
|
+
return null;
|
|
45
|
+
for (const line of result.stdout.split('\n')) {
|
|
46
|
+
if (line.startsWith('password=')) {
|
|
47
|
+
const token = line.slice('password='.length).trim();
|
|
48
|
+
if (token)
|
|
49
|
+
return token;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
function resolveToken() {
|
|
55
|
+
return resolveTokenViaGh() ?? resolveTokenViaGitCredential();
|
|
56
|
+
}
|
|
57
|
+
function exitWithTokenError() {
|
|
58
|
+
process.stderr.write('Could not resolve a GitHub token. Run `gh auth login`, or configure a git credential helper for github.com.\n');
|
|
59
|
+
process.exit(2);
|
|
60
|
+
}
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// HTTP helpers
|
|
63
|
+
// ============================================================================
|
|
64
|
+
async function describeHttpError(response) {
|
|
65
|
+
let serverMessage = '';
|
|
66
|
+
let code = '';
|
|
67
|
+
try {
|
|
68
|
+
const body = (await response.clone().json());
|
|
69
|
+
serverMessage = body.error ?? '';
|
|
70
|
+
code = body.code ?? '';
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// body wasn't JSON — fall through to status-only message
|
|
74
|
+
}
|
|
75
|
+
const fallback = response.status === 404
|
|
76
|
+
? 'Topic not found'
|
|
77
|
+
: response.status === 403
|
|
78
|
+
? 'Not the topic owner (or no permission)'
|
|
79
|
+
: response.status === 401
|
|
80
|
+
? 'GitHub token rejected'
|
|
81
|
+
: `HTTP ${response.status}`;
|
|
82
|
+
const human = serverMessage || fallback;
|
|
83
|
+
return `${human}${code ? ` [${code}]` : ''}`;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Comment-route error mapper — the comment endpoints surface a different
|
|
87
|
+
* vocabulary than the topic endpoints (COMMENT_FORBIDDEN vs NOT_OWNER,
|
|
88
|
+
* COMMENT_TOO_LONG with a dedicated 413, etc.). Map by `code` first so we
|
|
89
|
+
* stay accurate when the same status carries different meanings (e.g. 404
|
|
90
|
+
* for an unknown topic vs an unknown comment).
|
|
91
|
+
*/
|
|
92
|
+
async function describeCommentHttpError(response) {
|
|
93
|
+
let serverMessage = '';
|
|
94
|
+
let code = '';
|
|
95
|
+
try {
|
|
96
|
+
const body = (await response.clone().json());
|
|
97
|
+
serverMessage = body.error ?? '';
|
|
98
|
+
code = body.code ?? '';
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// body wasn't JSON — fall through to status-only message
|
|
102
|
+
}
|
|
103
|
+
const byCode = {
|
|
104
|
+
NOT_AUTHENTICATED: 'GitHub token required — run `gh auth login`',
|
|
105
|
+
NOT_FOUND: 'Topic not found',
|
|
106
|
+
COMMENT_NOT_FOUND: 'Comment not found',
|
|
107
|
+
COMMENT_FORBIDDEN: 'Not allowed to modify this comment',
|
|
108
|
+
COMMENT_TOO_LONG: 'Comment exceeds the 8000-character limit',
|
|
109
|
+
COMMENT_LIMIT_REACHED: 'This topic has reached its comment limit',
|
|
110
|
+
};
|
|
111
|
+
const fallback = byCode[code] ??
|
|
112
|
+
(response.status === 404
|
|
113
|
+
? 'Topic or comment not found'
|
|
114
|
+
: response.status === 403
|
|
115
|
+
? 'Not allowed to modify this comment'
|
|
116
|
+
: response.status === 401
|
|
117
|
+
? 'GitHub token rejected'
|
|
118
|
+
: response.status === 413
|
|
119
|
+
? 'Comment exceeds the 8000-character limit'
|
|
120
|
+
: `HTTP ${response.status}`);
|
|
121
|
+
const human = serverMessage || fallback;
|
|
122
|
+
return `${human}${code ? ` [${code}]` : ''}`;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Suggestion-route error mapper — the suggest endpoint has its own vocabulary
|
|
126
|
+
* (SUGGESTION_DUPLICATE, SUGGESTION_LIMIT_REACHED) plus a 404 that can mean
|
|
127
|
+
* either an unknown topic or an unknown trail. Map by `code` first so the
|
|
128
|
+
* user sees the precise reason rather than a generic "Topic not found".
|
|
129
|
+
*/
|
|
130
|
+
async function describeSuggestionHttpError(response) {
|
|
131
|
+
let serverMessage = '';
|
|
132
|
+
let code = '';
|
|
133
|
+
try {
|
|
134
|
+
const body = (await response.clone().json());
|
|
135
|
+
serverMessage = body.error ?? '';
|
|
136
|
+
code = body.code ?? '';
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// body wasn't JSON — fall through to status-only message
|
|
140
|
+
}
|
|
141
|
+
const byCode = {
|
|
142
|
+
NOT_AUTHENTICATED: 'GitHub token required — run `gh auth login`',
|
|
143
|
+
NOT_FOUND: 'Topic not found',
|
|
144
|
+
TRAIL_NOT_FOUND: 'Trail not found',
|
|
145
|
+
TRAIL_ALREADY_ADDED: 'That trail is already in this topic',
|
|
146
|
+
SUGGESTION_DUPLICATE: 'You already have a pending suggestion of that trail for this topic',
|
|
147
|
+
SUGGESTION_LIMIT_REACHED: 'This topic has reached its pending-suggestion limit',
|
|
148
|
+
};
|
|
149
|
+
const fallback = byCode[code] ??
|
|
150
|
+
(response.status === 404
|
|
151
|
+
? 'Topic or trail not found'
|
|
152
|
+
: response.status === 401
|
|
153
|
+
? 'GitHub token rejected'
|
|
154
|
+
: `HTTP ${response.status}`);
|
|
155
|
+
const human = serverMessage || fallback;
|
|
156
|
+
return `${human}${code ? ` [${code}]` : ''}`;
|
|
157
|
+
}
|
|
158
|
+
// ============================================================================
|
|
159
|
+
// Id / URL parsers
|
|
160
|
+
// ============================================================================
|
|
161
|
+
/** Extract a topic id from a bare uuid or a `…/topic/<id>` URL. */
|
|
162
|
+
function parseTopicId(input) {
|
|
163
|
+
const trimmed = input.trim();
|
|
164
|
+
if (UUID_PATTERN.test(trimmed))
|
|
165
|
+
return trimmed;
|
|
166
|
+
try {
|
|
167
|
+
const url = new URL(trimmed);
|
|
168
|
+
const match = url.pathname.match(/\/topic\/([^/]+)\/?$/);
|
|
169
|
+
if (match && UUID_PATTERN.test(match[1]))
|
|
170
|
+
return match[1];
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// not a URL — fall through; the server will reject if not a uuid
|
|
174
|
+
}
|
|
175
|
+
// Last-ditch: a "/topic/<uuid>" fragment anywhere in the string.
|
|
176
|
+
const m = trimmed.match(/topic\/([0-9a-f-]+)/i);
|
|
177
|
+
if (m && UUID_PATTERN.test(m[1]))
|
|
178
|
+
return m[1];
|
|
179
|
+
return trimmed;
|
|
180
|
+
}
|
|
181
|
+
/** Extract a trail id from a bare uuid or a `…/trail/<id>` URL. */
|
|
182
|
+
function parseTrailId(input) {
|
|
183
|
+
const trimmed = input.trim();
|
|
184
|
+
if (UUID_PATTERN.test(trimmed))
|
|
185
|
+
return trimmed;
|
|
186
|
+
try {
|
|
187
|
+
const url = new URL(trimmed);
|
|
188
|
+
const match = url.pathname.match(/\/trail\/([^/]+)\/?$/);
|
|
189
|
+
if (match && UUID_PATTERN.test(match[1]))
|
|
190
|
+
return match[1];
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// not a URL
|
|
194
|
+
}
|
|
195
|
+
const m = trimmed.match(/trail\/([0-9a-f-]+)/i);
|
|
196
|
+
if (m && UUID_PATTERN.test(m[1]))
|
|
197
|
+
return m[1];
|
|
198
|
+
return trimmed;
|
|
199
|
+
}
|
|
200
|
+
async function createTopic(options) {
|
|
201
|
+
if (!options.title || !options.title.trim()) {
|
|
202
|
+
process.stderr.write('--title is required\n');
|
|
203
|
+
process.exit(2);
|
|
204
|
+
}
|
|
205
|
+
const trailIds = (options.trail ?? []).map(parseTrailId);
|
|
206
|
+
for (const tid of trailIds) {
|
|
207
|
+
if (!UUID_PATTERN.test(tid)) {
|
|
208
|
+
process.stderr.write(`--trail value is not a valid trail id or URL: ${tid}\n`);
|
|
209
|
+
process.exit(2);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const token = resolveToken();
|
|
213
|
+
if (!token)
|
|
214
|
+
exitWithTokenError();
|
|
215
|
+
let response;
|
|
216
|
+
try {
|
|
217
|
+
response = await fetch(`${BASE_URL}/api/topics`, {
|
|
218
|
+
method: 'POST',
|
|
219
|
+
headers: {
|
|
220
|
+
Authorization: `Bearer ${token}`,
|
|
221
|
+
'Content-Type': 'application/json',
|
|
222
|
+
Accept: 'application/json',
|
|
223
|
+
},
|
|
224
|
+
body: JSON.stringify({
|
|
225
|
+
title: options.title.trim(),
|
|
226
|
+
description: options.description ?? '',
|
|
227
|
+
trailIds,
|
|
228
|
+
}),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
process.stderr.write(`Network error creating topic: ${err.message}\n`);
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
if (!response.ok) {
|
|
236
|
+
process.stderr.write(`${await describeHttpError(response)}\n`);
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
const body = (await response.json());
|
|
240
|
+
if (!body.url) {
|
|
241
|
+
process.stderr.write('Server response missing topic URL\n');
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
const fullUrl = body.url.startsWith('http') ? body.url : `${BASE_URL}${body.url}`;
|
|
245
|
+
process.stdout.write(`${fullUrl}\n`);
|
|
246
|
+
}
|
|
247
|
+
async function addTrailToTopic(topicArg, trailArg) {
|
|
248
|
+
const topicId = parseTopicId(topicArg);
|
|
249
|
+
const trailId = parseTrailId(trailArg);
|
|
250
|
+
if (!UUID_PATTERN.test(topicId)) {
|
|
251
|
+
process.stderr.write(`Not a valid topic id or URL: ${topicArg}\n`);
|
|
252
|
+
process.exit(2);
|
|
253
|
+
}
|
|
254
|
+
if (!UUID_PATTERN.test(trailId)) {
|
|
255
|
+
process.stderr.write(`Not a valid trail id or URL: ${trailArg}\n`);
|
|
256
|
+
process.exit(2);
|
|
257
|
+
}
|
|
258
|
+
const token = resolveToken();
|
|
259
|
+
if (!token)
|
|
260
|
+
exitWithTokenError();
|
|
261
|
+
let response;
|
|
262
|
+
try {
|
|
263
|
+
response = await fetch(`${BASE_URL}/api/topics/by-id/${encodeURIComponent(topicId)}/trails`, {
|
|
264
|
+
method: 'POST',
|
|
265
|
+
headers: {
|
|
266
|
+
Authorization: `Bearer ${token}`,
|
|
267
|
+
'Content-Type': 'application/json',
|
|
268
|
+
Accept: 'application/json',
|
|
269
|
+
},
|
|
270
|
+
body: JSON.stringify({ trailId }),
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
process.stderr.write(`Network error adding trail: ${err.message}\n`);
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
if (!response.ok) {
|
|
278
|
+
process.stderr.write(`${await describeHttpError(response)}\n`);
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
process.stdout.write(`${BASE_URL}/topic/${topicId}\n`);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Suggest a trail for a topic. Any GitHub-authenticated user can suggest any
|
|
285
|
+
* resolvable trail (their own or someone else's) — the topic owner reviews
|
|
286
|
+
* and accepts or rejects via the web UI. Prints the suggestion uuid so the
|
|
287
|
+
* suggester can refer to it later (e.g. to withdraw).
|
|
288
|
+
*/
|
|
289
|
+
async function suggestTrail(topicArg, trailArg, options) {
|
|
290
|
+
const topicId = parseTopicId(topicArg);
|
|
291
|
+
const trailId = parseTrailId(trailArg);
|
|
292
|
+
if (!UUID_PATTERN.test(topicId)) {
|
|
293
|
+
process.stderr.write(`Not a valid topic id or URL: ${topicArg}\n`);
|
|
294
|
+
process.exit(2);
|
|
295
|
+
}
|
|
296
|
+
if (!UUID_PATTERN.test(trailId)) {
|
|
297
|
+
process.stderr.write(`Not a valid trail id or URL: ${trailArg}\n`);
|
|
298
|
+
process.exit(2);
|
|
299
|
+
}
|
|
300
|
+
const reason = options.reason?.trim();
|
|
301
|
+
if (reason !== undefined && reason.length > 500) {
|
|
302
|
+
process.stderr.write('--reason exceeds the 500-character limit\n');
|
|
303
|
+
process.exit(2);
|
|
304
|
+
}
|
|
305
|
+
const token = resolveToken();
|
|
306
|
+
if (!token)
|
|
307
|
+
exitWithTokenError();
|
|
308
|
+
const payload = { trailId };
|
|
309
|
+
if (reason)
|
|
310
|
+
payload.reason = reason;
|
|
311
|
+
let response;
|
|
312
|
+
try {
|
|
313
|
+
response = await fetch(`${BASE_URL}/api/topics/by-id/${encodeURIComponent(topicId)}/suggestions`, {
|
|
314
|
+
method: 'POST',
|
|
315
|
+
headers: {
|
|
316
|
+
Authorization: `Bearer ${token}`,
|
|
317
|
+
'Content-Type': 'application/json',
|
|
318
|
+
Accept: 'application/json',
|
|
319
|
+
},
|
|
320
|
+
body: JSON.stringify(payload),
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
catch (err) {
|
|
324
|
+
process.stderr.write(`Network error suggesting trail: ${err.message}\n`);
|
|
325
|
+
process.exit(1);
|
|
326
|
+
}
|
|
327
|
+
if (!response.ok) {
|
|
328
|
+
process.stderr.write(`${await describeSuggestionHttpError(response)}\n`);
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
const body = (await response.json());
|
|
332
|
+
const suggestionId = body.suggestion?.id;
|
|
333
|
+
if (suggestionId) {
|
|
334
|
+
process.stdout.write(`${suggestionId}\n`);
|
|
335
|
+
}
|
|
336
|
+
process.stdout.write(`${BASE_URL}/topic/${topicId}\n`);
|
|
337
|
+
}
|
|
338
|
+
async function updateTopic(topicArg, options) {
|
|
339
|
+
const topicId = parseTopicId(topicArg);
|
|
340
|
+
if (!UUID_PATTERN.test(topicId)) {
|
|
341
|
+
process.stderr.write(`Not a valid topic id or URL: ${topicArg}\n`);
|
|
342
|
+
process.exit(2);
|
|
343
|
+
}
|
|
344
|
+
const hasTitle = typeof options.title === 'string';
|
|
345
|
+
const hasDescription = typeof options.description === 'string';
|
|
346
|
+
if (!hasTitle && !hasDescription) {
|
|
347
|
+
process.stderr.write('Pass at least one of --title or --description\n');
|
|
348
|
+
process.exit(2);
|
|
349
|
+
}
|
|
350
|
+
const patch = {};
|
|
351
|
+
if (hasTitle) {
|
|
352
|
+
const trimmed = options.title.trim();
|
|
353
|
+
if (!trimmed) {
|
|
354
|
+
process.stderr.write('--title must not be empty (omit the flag to leave it unchanged)\n');
|
|
355
|
+
process.exit(2);
|
|
356
|
+
}
|
|
357
|
+
patch.title = trimmed;
|
|
358
|
+
}
|
|
359
|
+
if (hasDescription) {
|
|
360
|
+
patch.description = options.description;
|
|
361
|
+
}
|
|
362
|
+
const token = resolveToken();
|
|
363
|
+
if (!token)
|
|
364
|
+
exitWithTokenError();
|
|
365
|
+
let response;
|
|
366
|
+
try {
|
|
367
|
+
response = await fetch(`${BASE_URL}/api/topics/by-id/${encodeURIComponent(topicId)}`, {
|
|
368
|
+
method: 'PATCH',
|
|
369
|
+
headers: {
|
|
370
|
+
Authorization: `Bearer ${token}`,
|
|
371
|
+
'Content-Type': 'application/json',
|
|
372
|
+
Accept: 'application/json',
|
|
373
|
+
},
|
|
374
|
+
body: JSON.stringify(patch),
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
catch (err) {
|
|
378
|
+
process.stderr.write(`Network error updating topic: ${err.message}\n`);
|
|
379
|
+
process.exit(1);
|
|
380
|
+
}
|
|
381
|
+
if (!response.ok) {
|
|
382
|
+
process.stderr.write(`${await describeHttpError(response)}\n`);
|
|
383
|
+
process.exit(1);
|
|
384
|
+
}
|
|
385
|
+
process.stdout.write(`${BASE_URL}/topic/${topicId}\n`);
|
|
386
|
+
}
|
|
387
|
+
async function viewTopic(input) {
|
|
388
|
+
const id = parseTopicId(input);
|
|
389
|
+
if (!UUID_PATTERN.test(id)) {
|
|
390
|
+
process.stderr.write(`Not a valid topic id or URL: ${input}\n`);
|
|
391
|
+
process.exit(2);
|
|
392
|
+
}
|
|
393
|
+
// GET /api/topics/by-id/{id} is public — token isn't required, but we
|
|
394
|
+
// attach one if available so the request is consistent with the other
|
|
395
|
+
// commands and so rate limits favor authenticated callers.
|
|
396
|
+
const token = resolveToken();
|
|
397
|
+
let response;
|
|
398
|
+
try {
|
|
399
|
+
response = await fetch(`${BASE_URL}/api/topics/by-id/${encodeURIComponent(id)}`, {
|
|
400
|
+
headers: {
|
|
401
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
402
|
+
Accept: 'application/json',
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
process.stderr.write(`Network error fetching topic: ${err.message}\n`);
|
|
408
|
+
process.exit(1);
|
|
409
|
+
}
|
|
410
|
+
if (!response.ok) {
|
|
411
|
+
process.stderr.write(`${await describeHttpError(response)}\n`);
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
const body = await response.text();
|
|
415
|
+
process.stdout.write(body);
|
|
416
|
+
if (!body.endsWith('\n'))
|
|
417
|
+
process.stdout.write('\n');
|
|
418
|
+
}
|
|
419
|
+
async function listTopics(options) {
|
|
420
|
+
let githubId;
|
|
421
|
+
if (options.id) {
|
|
422
|
+
const parsed = Number(options.id);
|
|
423
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
424
|
+
process.stderr.write(`--id must be a positive number, got: ${options.id}\n`);
|
|
425
|
+
process.exit(2);
|
|
426
|
+
}
|
|
427
|
+
githubId = parsed;
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
// `--user` resolves a login → numeric id via GitHub; the bare form resolves
|
|
431
|
+
// the authenticated user via /user. Both paths need a token.
|
|
432
|
+
const token = resolveToken();
|
|
433
|
+
if (!token)
|
|
434
|
+
exitWithTokenError();
|
|
435
|
+
const user = options.user
|
|
436
|
+
? await fetchGitHubUserByLogin(options.user, token)
|
|
437
|
+
: await fetchGitHubMe(token);
|
|
438
|
+
githubId = user.id;
|
|
439
|
+
}
|
|
440
|
+
let response;
|
|
441
|
+
try {
|
|
442
|
+
response = await fetch(`${BASE_URL}/api/topics/by-user/${githubId}`, {
|
|
443
|
+
headers: { Accept: 'application/json' },
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
catch (err) {
|
|
447
|
+
process.stderr.write(`Network error listing topics: ${err.message}\n`);
|
|
448
|
+
process.exit(1);
|
|
449
|
+
}
|
|
450
|
+
if (!response.ok) {
|
|
451
|
+
process.stderr.write(`${await describeHttpError(response)}\n`);
|
|
452
|
+
process.exit(1);
|
|
453
|
+
}
|
|
454
|
+
const body = await response.text();
|
|
455
|
+
process.stdout.write(body);
|
|
456
|
+
if (!body.endsWith('\n'))
|
|
457
|
+
process.stdout.write('\n');
|
|
458
|
+
}
|
|
459
|
+
// ============================================================================
|
|
460
|
+
// open — launch the topic page in the default browser
|
|
461
|
+
// ============================================================================
|
|
462
|
+
async function openTopic(input) {
|
|
463
|
+
const id = parseTopicId(input);
|
|
464
|
+
const isLocalId = id.startsWith('topic-');
|
|
465
|
+
// Local topic-* ids bypass the UUID check (they're minted by the desktop app).
|
|
466
|
+
if (!isLocalId && !UUID_PATTERN.test(id)) {
|
|
467
|
+
process.stderr.write(`Not a valid topic id or URL: ${input}\n`);
|
|
468
|
+
process.exit(2);
|
|
469
|
+
}
|
|
470
|
+
// Prefer the running desktop app when available (local topic-* ids only).
|
|
471
|
+
if (isLocalId && (await handoffTopicToBridge(id))) {
|
|
472
|
+
process.stderr.write(`Topic opened in running desktop app: ${id}\n`);
|
|
473
|
+
process.exit(0);
|
|
474
|
+
}
|
|
475
|
+
// Fall back to web-ade in the browser.
|
|
476
|
+
const url = `${BASE_URL}/topic/${id}`;
|
|
477
|
+
// Print first so the URL is captured in scrollback / pipes even if the
|
|
478
|
+
// platform opener fails (headless / no DISPLAY / missing xdg-open).
|
|
479
|
+
process.stdout.write(`${url}\n`);
|
|
480
|
+
openInBrowser(url);
|
|
481
|
+
}
|
|
482
|
+
async function addComment(topicArg, options) {
|
|
483
|
+
const topicId = parseTopicId(topicArg);
|
|
484
|
+
if (!UUID_PATTERN.test(topicId)) {
|
|
485
|
+
process.stderr.write(`Not a valid topic id or URL: ${topicArg}\n`);
|
|
486
|
+
process.exit(2);
|
|
487
|
+
}
|
|
488
|
+
const body = (options.body ?? '').trim();
|
|
489
|
+
if (!body) {
|
|
490
|
+
process.stderr.write('--body is required and must not be empty\n');
|
|
491
|
+
process.exit(2);
|
|
492
|
+
}
|
|
493
|
+
const token = resolveToken();
|
|
494
|
+
if (!token)
|
|
495
|
+
exitWithTokenError();
|
|
496
|
+
let response;
|
|
497
|
+
try {
|
|
498
|
+
response = await fetch(`${BASE_URL}/api/topics/by-id/${encodeURIComponent(topicId)}/comments`, {
|
|
499
|
+
method: 'POST',
|
|
500
|
+
headers: {
|
|
501
|
+
Authorization: `Bearer ${token}`,
|
|
502
|
+
'Content-Type': 'application/json',
|
|
503
|
+
Accept: 'application/json',
|
|
504
|
+
},
|
|
505
|
+
body: JSON.stringify({ body }),
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
catch (err) {
|
|
509
|
+
process.stderr.write(`Network error posting comment: ${err.message}\n`);
|
|
510
|
+
process.exit(1);
|
|
511
|
+
}
|
|
512
|
+
if (!response.ok) {
|
|
513
|
+
process.stderr.write(`${await describeCommentHttpError(response)}\n`);
|
|
514
|
+
process.exit(1);
|
|
515
|
+
}
|
|
516
|
+
const data = (await response.json());
|
|
517
|
+
if (!data.comment?.id) {
|
|
518
|
+
process.stderr.write('Server response missing comment id\n');
|
|
519
|
+
process.exit(1);
|
|
520
|
+
}
|
|
521
|
+
// Stdout is just the new id so it can be piped into `comment delete`.
|
|
522
|
+
process.stdout.write(`${data.comment.id}\n`);
|
|
523
|
+
}
|
|
524
|
+
async function listComments(topicArg) {
|
|
525
|
+
const topicId = parseTopicId(topicArg);
|
|
526
|
+
if (!UUID_PATTERN.test(topicId)) {
|
|
527
|
+
process.stderr.write(`Not a valid topic id or URL: ${topicArg}\n`);
|
|
528
|
+
process.exit(2);
|
|
529
|
+
}
|
|
530
|
+
// GET /comments is public — attach a token if present so rate limits favor
|
|
531
|
+
// authenticated callers, but don't error out if there isn't one.
|
|
532
|
+
const token = resolveToken();
|
|
533
|
+
let response;
|
|
534
|
+
try {
|
|
535
|
+
response = await fetch(`${BASE_URL}/api/topics/by-id/${encodeURIComponent(topicId)}/comments`, {
|
|
536
|
+
headers: {
|
|
537
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
538
|
+
Accept: 'application/json',
|
|
539
|
+
},
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
catch (err) {
|
|
543
|
+
process.stderr.write(`Network error fetching comments: ${err.message}\n`);
|
|
544
|
+
process.exit(1);
|
|
545
|
+
}
|
|
546
|
+
if (!response.ok) {
|
|
547
|
+
process.stderr.write(`${await describeCommentHttpError(response)}\n`);
|
|
548
|
+
process.exit(1);
|
|
549
|
+
}
|
|
550
|
+
const body = await response.text();
|
|
551
|
+
process.stdout.write(body);
|
|
552
|
+
if (!body.endsWith('\n'))
|
|
553
|
+
process.stdout.write('\n');
|
|
554
|
+
}
|
|
555
|
+
async function deleteComment(topicArg, commentIdArg) {
|
|
556
|
+
const topicId = parseTopicId(topicArg);
|
|
557
|
+
if (!UUID_PATTERN.test(topicId)) {
|
|
558
|
+
process.stderr.write(`Not a valid topic id or URL: ${topicArg}\n`);
|
|
559
|
+
process.exit(2);
|
|
560
|
+
}
|
|
561
|
+
const commentId = commentIdArg.trim();
|
|
562
|
+
if (!UUID_PATTERN.test(commentId)) {
|
|
563
|
+
process.stderr.write(`Not a valid comment id: ${commentIdArg}\n`);
|
|
564
|
+
process.exit(2);
|
|
565
|
+
}
|
|
566
|
+
const token = resolveToken();
|
|
567
|
+
if (!token)
|
|
568
|
+
exitWithTokenError();
|
|
569
|
+
let response;
|
|
570
|
+
try {
|
|
571
|
+
response = await fetch(`${BASE_URL}/api/topics/by-id/${encodeURIComponent(topicId)}/comments/${encodeURIComponent(commentId)}`, {
|
|
572
|
+
method: 'DELETE',
|
|
573
|
+
headers: {
|
|
574
|
+
Authorization: `Bearer ${token}`,
|
|
575
|
+
Accept: 'application/json',
|
|
576
|
+
},
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
catch (err) {
|
|
580
|
+
process.stderr.write(`Network error deleting comment: ${err.message}\n`);
|
|
581
|
+
process.exit(1);
|
|
582
|
+
}
|
|
583
|
+
if (!response.ok && response.status !== 204) {
|
|
584
|
+
process.stderr.write(`${await describeCommentHttpError(response)}\n`);
|
|
585
|
+
process.exit(1);
|
|
586
|
+
}
|
|
587
|
+
// 204 No Content is the happy path — keep stdout clean (scriptable) and
|
|
588
|
+
// mirror the topic URL on success so users see *what* they just mutated.
|
|
589
|
+
process.stdout.write(`${BASE_URL}/topic/${topicId}\n`);
|
|
590
|
+
}
|
|
591
|
+
// ============================================================================
|
|
592
|
+
// Command wiring
|
|
593
|
+
// ============================================================================
|
|
594
|
+
export function createTopicCommand() {
|
|
595
|
+
const command = new Command('topic');
|
|
596
|
+
command.description('Create or browse topics — curated collections of trails on web-ade');
|
|
597
|
+
command
|
|
598
|
+
.command('create')
|
|
599
|
+
.description('Create a new topic')
|
|
600
|
+
.requiredOption('--title <title>', 'Topic title (≤200 chars)')
|
|
601
|
+
.option('--description <text>', 'Topic description, markdown (≤8000 chars)')
|
|
602
|
+
.option('--trail <id-or-url>', 'Initial trail id or URL; pass repeatedly to seed multiple trails', (value, prev = []) => [...prev, value], [])
|
|
603
|
+
.action(async (options) => {
|
|
604
|
+
await createTopic(options);
|
|
605
|
+
});
|
|
606
|
+
command
|
|
607
|
+
.command('add-trail')
|
|
608
|
+
.description('Append a trail to a topic')
|
|
609
|
+
.argument('<topic-id-or-url>', 'Topic id or URL')
|
|
610
|
+
.argument('<trail-id-or-url>', 'Trail id or URL to add')
|
|
611
|
+
.action(async (topicArg, trailArg) => {
|
|
612
|
+
await addTrailToTopic(topicArg, trailArg);
|
|
613
|
+
});
|
|
614
|
+
command
|
|
615
|
+
.command('suggest')
|
|
616
|
+
.description('Suggest a trail for a topic — owner reviews and accepts/rejects. Any authenticated user can suggest any resolvable trail.')
|
|
617
|
+
.argument('<topic-id-or-url>', 'Topic id or URL')
|
|
618
|
+
.argument('<trail-id-or-url>', 'Trail id or URL to suggest')
|
|
619
|
+
.option('--reason <text>', 'Why this trail fits (≤500 chars)')
|
|
620
|
+
.action(async (topicArg, trailArg, options) => {
|
|
621
|
+
await suggestTrail(topicArg, trailArg, options);
|
|
622
|
+
});
|
|
623
|
+
command
|
|
624
|
+
.command('update')
|
|
625
|
+
.description("Update a topic's title and/or description (owner only)")
|
|
626
|
+
.argument('<id-or-url>', 'Topic id or URL')
|
|
627
|
+
.option('--title <title>', 'New title (≤200 chars); omit to leave unchanged')
|
|
628
|
+
.option('--description <text>', 'New description, markdown (≤8000 chars); omit to leave unchanged. Pass an empty string to clear.')
|
|
629
|
+
.action(async (input, options) => {
|
|
630
|
+
await updateTopic(input, options);
|
|
631
|
+
});
|
|
632
|
+
command
|
|
633
|
+
.command('view')
|
|
634
|
+
.description('Print a topic JSON to stdout')
|
|
635
|
+
.argument('<id-or-url>', 'Topic id or URL')
|
|
636
|
+
.action(async (input) => {
|
|
637
|
+
await viewTopic(input);
|
|
638
|
+
});
|
|
639
|
+
command
|
|
640
|
+
.command('open')
|
|
641
|
+
.description('Open the topic page in the default browser')
|
|
642
|
+
.argument('<id-or-url>', 'Topic id or URL')
|
|
643
|
+
.action(async (input) => {
|
|
644
|
+
await openTopic(input);
|
|
645
|
+
});
|
|
646
|
+
command
|
|
647
|
+
.command('list')
|
|
648
|
+
.description('List topics owned by a user (defaults to the authenticated user)')
|
|
649
|
+
.option('--user <login>', 'GitHub login to list topics for (resolves to numeric id)')
|
|
650
|
+
.option('--id <githubId>', 'GitHub numeric user id (skips the lookup)')
|
|
651
|
+
.action(async (options) => {
|
|
652
|
+
await listTopics(options);
|
|
653
|
+
});
|
|
654
|
+
// ---- comment subcommands -------------------------------------------------
|
|
655
|
+
// Flat thread attached to a topic. `add` and `delete` need a token; `list`
|
|
656
|
+
// is public and attaches one only when available.
|
|
657
|
+
const commentCommand = new Command('comment').description('Post or browse comments on a topic');
|
|
658
|
+
commentCommand
|
|
659
|
+
.command('add')
|
|
660
|
+
.description('Post a new comment on a topic')
|
|
661
|
+
.argument('<topic-id-or-url>', 'Topic id or URL')
|
|
662
|
+
.requiredOption('--body <text>', 'Comment body, markdown (≤8000 chars)')
|
|
663
|
+
.action(async (topicArg, options) => {
|
|
664
|
+
await addComment(topicArg, options);
|
|
665
|
+
});
|
|
666
|
+
commentCommand
|
|
667
|
+
.command('list')
|
|
668
|
+
.description('List the comments on a topic (public; token attached if present)')
|
|
669
|
+
.argument('<topic-id-or-url>', 'Topic id or URL')
|
|
670
|
+
.action(async (topicArg) => {
|
|
671
|
+
await listComments(topicArg);
|
|
672
|
+
});
|
|
673
|
+
commentCommand
|
|
674
|
+
.command('delete')
|
|
675
|
+
.description('Delete a comment (author or topic owner). Comment id is the uuid printed by `comment add`.')
|
|
676
|
+
.argument('<topic-id-or-url>', 'Topic id or URL')
|
|
677
|
+
.argument('<comment-id>', 'Comment uuid')
|
|
678
|
+
.action(async (topicArg, commentIdArg) => {
|
|
679
|
+
await deleteComment(topicArg, commentIdArg);
|
|
680
|
+
});
|
|
681
|
+
command.addCommand(commentCommand);
|
|
682
|
+
return command;
|
|
683
|
+
}
|