@vellumai/cli 0.8.3 → 0.8.5

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.
@@ -0,0 +1,449 @@
1
+ import { readPlatformToken, getWebUrl } from "../lib/platform-client.js";
2
+
3
+ function printUsage(): void {
4
+ console.log("Usage: vellum roadmap <subcommand>");
5
+ console.log("");
6
+ console.log("Manage roadmap items.");
7
+ console.log("");
8
+ console.log("Subcommands:");
9
+ console.log(
10
+ " list [--query <q>] [--status <s>] [--tag <slug>] [--sort upvotes|created] [--limit <n>]",
11
+ );
12
+ console.log(" get <slug>");
13
+ console.log(
14
+ " create --title <title> [--description <desc>] [--tag <slug>...]",
15
+ );
16
+ console.log(
17
+ " update <slug> [--title <title>] [--description <desc>] [--status <s>] [--tag <slug>...]",
18
+ );
19
+ console.log(" delete <slug>");
20
+ console.log(" upvote <slug>");
21
+ console.log(" unvote <slug>");
22
+ console.log("");
23
+ console.log("Examples:");
24
+ console.log(' $ vellum roadmap list --query "dark mode"');
25
+ console.log(" $ vellum roadmap list --status planned --sort upvotes");
26
+ console.log(" $ vellum roadmap get my-feature-slug");
27
+ console.log(' $ vellum roadmap create --title "Add dark mode"');
28
+ console.log(
29
+ ' $ vellum roadmap update my-feature --status planned --tag integrations',
30
+ );
31
+ console.log(" $ vellum roadmap upvote my-feature-slug");
32
+ }
33
+
34
+ function consumeValue(args: string[], i: number, flag: string): string {
35
+ const next = args[i + 1];
36
+ if (next === undefined || next.startsWith("--")) {
37
+ console.error(`Error: ${flag} requires a value.`);
38
+ process.exit(1);
39
+ }
40
+ return next;
41
+ }
42
+
43
+ function requireAuth(): string {
44
+ const token = readPlatformToken();
45
+ if (!token) {
46
+ console.error("Not logged in. Run `vellum login` first.");
47
+ process.exit(1);
48
+ }
49
+ return token;
50
+ }
51
+
52
+ function requireSlug(args: string[], command: string): string {
53
+ const slug = args[0];
54
+ if (!slug || slug.startsWith("--")) {
55
+ console.error(`Usage: vellum roadmap ${command} <slug>`);
56
+ process.exit(1);
57
+ }
58
+ return slug;
59
+ }
60
+
61
+ // eslint-disable-next-line no-control-regex
62
+ const ANSI_RE = /[\x00-\x08\x0b-\x1f\x7f]|\x1b(?:\[[0-9;]*[A-Za-z]|\].*?(?:\x07|\x1b\\))/g;
63
+ function sanitize(text: string): string {
64
+ return text.replace(ANSI_RE, "");
65
+ }
66
+
67
+ function makeLink(url: string): string {
68
+ return `\x1b]8;;${url}\x1b\\${url}\x1b]8;;\x1b\\`;
69
+ }
70
+
71
+ async function apiFetch(
72
+ path: string,
73
+ options: {
74
+ method?: string;
75
+ token?: string;
76
+ body?: Record<string, unknown>;
77
+ params?: Record<string, string>;
78
+ } = {},
79
+ ): Promise<Response> {
80
+ const webUrl = getWebUrl();
81
+ let url = `${webUrl}/api/marketing${path}`;
82
+ if (options.params) {
83
+ const qs = new URLSearchParams(options.params).toString();
84
+ if (qs) url += `?${qs}`;
85
+ }
86
+
87
+ const headers: Record<string, string> = {};
88
+ if (options.token) headers["X-Session-Token"] = options.token;
89
+ if (options.body) headers["Content-Type"] = "application/json";
90
+
91
+ return fetch(url, {
92
+ method: options.method ?? "GET",
93
+ headers,
94
+ body: options.body ? JSON.stringify(options.body) : undefined,
95
+ });
96
+ }
97
+
98
+ async function handleError(
99
+ response: Response,
100
+ action: string,
101
+ ): Promise<never> {
102
+ const text = await response.text().catch(() => "");
103
+ console.error(`Failed to ${action} (${response.status}): ${text}`);
104
+ process.exit(1);
105
+ }
106
+
107
+ // ── list ──
108
+
109
+ interface ListItem {
110
+ slug: string;
111
+ title: string;
112
+ status: string;
113
+ upvote_count: number;
114
+ comment_count: number;
115
+ tags: { slug: string; name: string }[];
116
+ viewer_upvoted: boolean | null;
117
+ }
118
+
119
+ async function roadmapList(args: string[]): Promise<void> {
120
+ const params: Record<string, string> = {};
121
+ const token = readPlatformToken() ?? undefined;
122
+
123
+ for (let i = 0; i < args.length; i++) {
124
+ switch (args[i]) {
125
+ case "--query":
126
+ case "-q":
127
+ params.q = consumeValue(args, i, "--query");
128
+ i++;
129
+ break;
130
+ case "--status":
131
+ params.status = consumeValue(args, i, "--status");
132
+ i++;
133
+ break;
134
+ case "--tag":
135
+ params.tag = consumeValue(args, i, "--tag");
136
+ i++;
137
+ break;
138
+ case "--sort":
139
+ params.sort = consumeValue(args, i, "--sort");
140
+ i++;
141
+ break;
142
+ case "--limit":
143
+ params.limit = consumeValue(args, i, "--limit");
144
+ i++;
145
+ break;
146
+ case "--offset":
147
+ params.offset = consumeValue(args, i, "--offset");
148
+ i++;
149
+ break;
150
+ }
151
+ }
152
+
153
+ const response = await apiFetch("/v1/roadmap", { params, token });
154
+ if (!response.ok) return handleError(response, "list roadmap items");
155
+
156
+ const data = (await response.json()) as {
157
+ items: ListItem[];
158
+ total: number;
159
+ };
160
+
161
+ if (data.items.length === 0) {
162
+ console.log("No roadmap items found.");
163
+ return;
164
+ }
165
+
166
+ const webUrl = getWebUrl();
167
+ console.log(`Showing ${data.items.length} of ${data.total} items:\n`);
168
+
169
+ for (const item of data.items) {
170
+ const upvoted = item.viewer_upvoted ? " (upvoted)" : "";
171
+ const tags = item.tags.length > 0
172
+ ? ` [${item.tags.map((t) => sanitize(t.slug)).join(", ")}]`
173
+ : "";
174
+ console.log(
175
+ ` ${sanitize(item.title)} ▲${item.upvote_count}${upvoted} 💬${item.comment_count} ${item.status}${tags}`,
176
+ );
177
+ console.log(` ${makeLink(`${webUrl}/roadmap/${item.slug}`)}`);
178
+ }
179
+ }
180
+
181
+ // ── get ──
182
+
183
+ async function roadmapGet(args: string[]): Promise<void> {
184
+ const slug = requireSlug(args, "get");
185
+ const token = readPlatformToken() ?? undefined;
186
+ const response = await apiFetch(`/v1/roadmap/${slug}`, { token });
187
+ if (!response.ok) return handleError(response, "get roadmap item");
188
+
189
+ const item = (await response.json()) as {
190
+ slug: string;
191
+ title: string;
192
+ description: string;
193
+ status: string;
194
+ upvote_count: number;
195
+ comment_count: number;
196
+ tags: { slug: string; name: string }[];
197
+ viewer_upvoted: boolean | null;
198
+ creator_username: string;
199
+ created: string;
200
+ comments: {
201
+ id: string;
202
+ author_username: string;
203
+ author_is_staff: boolean;
204
+ body: string;
205
+ created: string;
206
+ }[];
207
+ };
208
+
209
+ const webUrl = getWebUrl();
210
+ const upvoted = item.viewer_upvoted ? " (upvoted)" : "";
211
+ const tags =
212
+ item.tags.length > 0
213
+ ? item.tags.map((t) => sanitize(t.slug)).join(", ")
214
+ : "none";
215
+
216
+ console.log(sanitize(item.title));
217
+ console.log(` slug: ${item.slug}`);
218
+ console.log(` status: ${item.status}`);
219
+ console.log(` upvotes: ${item.upvote_count}${upvoted}`);
220
+ console.log(` tags: ${tags}`);
221
+ console.log(` by: ${sanitize(item.creator_username)}`);
222
+ console.log(` created: ${item.created}`);
223
+ console.log(` url: ${makeLink(`${webUrl}/roadmap/${item.slug}`)}`);
224
+ if (item.description) {
225
+ console.log(`\n${sanitize(item.description)}`);
226
+ }
227
+
228
+ if (item.comments.length > 0) {
229
+ console.log(`\nComments (${item.comments.length}):`);
230
+ for (const c of item.comments) {
231
+ const staff = c.author_is_staff ? " [staff]" : "";
232
+ console.log(` ${sanitize(c.author_username)}${staff} (${c.created}):`);
233
+ console.log(` ${sanitize(c.body)}`);
234
+ }
235
+ }
236
+ }
237
+
238
+ // ── create ──
239
+
240
+ async function roadmapCreate(args: string[]): Promise<void> {
241
+ let title: string | undefined;
242
+ let description: string | undefined;
243
+ const tags: string[] = [];
244
+
245
+ for (let i = 0; i < args.length; i++) {
246
+ switch (args[i]) {
247
+ case "--title":
248
+ title = consumeValue(args, i, "--title");
249
+ i++;
250
+ break;
251
+ case "--description":
252
+ description = consumeValue(args, i, "--description");
253
+ i++;
254
+ break;
255
+ case "--tag":
256
+ tags.push(consumeValue(args, i, "--tag"));
257
+ i++;
258
+ break;
259
+ }
260
+ }
261
+
262
+ if (!title) {
263
+ console.error("Error: --title is required.");
264
+ console.error('Usage: vellum roadmap create --title "My feature request"');
265
+ process.exitCode = 1;
266
+ return;
267
+ }
268
+
269
+ const token = requireAuth();
270
+ const body: Record<string, unknown> = { title };
271
+ if (description) body.description = description;
272
+ if (tags.length > 0) body.tags = tags;
273
+
274
+ const response = await apiFetch("/v1/roadmap", {
275
+ method: "POST",
276
+ token,
277
+ body,
278
+ });
279
+ if (!response.ok) return handleError(response, "create roadmap item");
280
+
281
+ const item = (await response.json()) as {
282
+ slug: string;
283
+ title: string;
284
+ status: string;
285
+ };
286
+
287
+ const webUrl = getWebUrl();
288
+ console.log(`Created roadmap item: ${sanitize(item.title)}`);
289
+ console.log(` slug: ${item.slug}`);
290
+ console.log(` status: ${item.status}`);
291
+ console.log(` url: ${makeLink(`${webUrl}/roadmap/${item.slug}`)}`);
292
+ }
293
+
294
+ // ── update ──
295
+
296
+ async function roadmapUpdate(args: string[]): Promise<void> {
297
+ const slug = requireSlug(args, "update");
298
+
299
+ let title: string | undefined;
300
+ let description: string | undefined;
301
+ let status: string | undefined;
302
+ const tags: string[] = [];
303
+
304
+ for (let i = 1; i < args.length; i++) {
305
+ switch (args[i]) {
306
+ case "--title":
307
+ title = consumeValue(args, i, "--title");
308
+ i++;
309
+ break;
310
+ case "--description":
311
+ description = consumeValue(args, i, "--description");
312
+ i++;
313
+ break;
314
+ case "--status":
315
+ status = consumeValue(args, i, "--status");
316
+ i++;
317
+ break;
318
+ case "--tag":
319
+ tags.push(consumeValue(args, i, "--tag"));
320
+ i++;
321
+ break;
322
+ }
323
+ }
324
+
325
+ const body: Record<string, unknown> = {};
326
+ if (title !== undefined) body.title = title;
327
+ if (description !== undefined) body.description = description;
328
+ if (status !== undefined) body.status = status;
329
+ if (tags.length > 0) body.tags = tags;
330
+
331
+ if (Object.keys(body).length === 0) {
332
+ console.error("Error: at least one field to update is required.");
333
+ process.exitCode = 1;
334
+ return;
335
+ }
336
+
337
+ const token = requireAuth();
338
+ const response = await apiFetch(`/v1/roadmap/${slug}`, {
339
+ method: "PATCH",
340
+ token,
341
+ body,
342
+ });
343
+ if (!response.ok) return handleError(response, "update roadmap item");
344
+
345
+ const item = (await response.json()) as {
346
+ slug: string;
347
+ title: string;
348
+ status: string;
349
+ };
350
+
351
+ const webUrl = getWebUrl();
352
+ console.log(`Updated roadmap item: ${sanitize(item.title)}`);
353
+ console.log(` slug: ${item.slug}`);
354
+ console.log(` status: ${item.status}`);
355
+ console.log(` url: ${makeLink(`${webUrl}/roadmap/${item.slug}`)}`);
356
+ }
357
+
358
+ // ── delete ──
359
+
360
+ async function roadmapDelete(args: string[]): Promise<void> {
361
+ const slug = requireSlug(args, "delete");
362
+ const token = requireAuth();
363
+ const response = await apiFetch(`/v1/roadmap/${slug}`, {
364
+ method: "DELETE",
365
+ token,
366
+ });
367
+ if (!response.ok) return handleError(response, "delete roadmap item");
368
+
369
+ console.log(`Deleted roadmap item: ${slug}`);
370
+ }
371
+
372
+ // ── upvote / unvote ──
373
+
374
+ async function roadmapUpvote(args: string[]): Promise<void> {
375
+ const slug = requireSlug(args, "upvote");
376
+ const token = requireAuth();
377
+ const response = await apiFetch(`/v1/roadmap/${slug}/upvote`, {
378
+ method: "POST",
379
+ token,
380
+ });
381
+ if (!response.ok) return handleError(response, "upvote roadmap item");
382
+
383
+ const data = (await response.json()) as {
384
+ slug: string;
385
+ upvote_count: number;
386
+ };
387
+
388
+ console.log(`Upvoted: ${data.slug} (${data.upvote_count} total)`);
389
+ }
390
+
391
+ async function roadmapUnvote(args: string[]): Promise<void> {
392
+ const slug = requireSlug(args, "unvote");
393
+ const token = requireAuth();
394
+ const response = await apiFetch(`/v1/roadmap/${slug}/upvote`, {
395
+ method: "DELETE",
396
+ token,
397
+ });
398
+ if (!response.ok) return handleError(response, "remove upvote");
399
+
400
+ const data = (await response.json()) as {
401
+ slug: string;
402
+ upvote_count: number;
403
+ };
404
+
405
+ console.log(`Removed upvote: ${data.slug} (${data.upvote_count} total)`);
406
+ }
407
+
408
+ // ── main ──
409
+
410
+ export async function roadmap(): Promise<void> {
411
+ const args = process.argv.slice(3);
412
+ const sub = args[0];
413
+
414
+ if (!sub || sub === "--help" || sub === "-h") {
415
+ printUsage();
416
+ return;
417
+ }
418
+
419
+ switch (sub) {
420
+ case "list":
421
+ case "ls":
422
+ await roadmapList(args.slice(1));
423
+ break;
424
+ case "get":
425
+ case "show":
426
+ await roadmapGet(args.slice(1));
427
+ break;
428
+ case "create":
429
+ await roadmapCreate(args.slice(1));
430
+ break;
431
+ case "update":
432
+ await roadmapUpdate(args.slice(1));
433
+ break;
434
+ case "delete":
435
+ case "rm":
436
+ await roadmapDelete(args.slice(1));
437
+ break;
438
+ case "upvote":
439
+ await roadmapUpvote(args.slice(1));
440
+ break;
441
+ case "unvote":
442
+ await roadmapUnvote(args.slice(1));
443
+ break;
444
+ default:
445
+ console.error(`Unknown subcommand: ${sub}`);
446
+ printUsage();
447
+ process.exitCode = 1;
448
+ }
449
+ }
@@ -1,19 +1,24 @@
1
1
  import {
2
- findAssistantByName,
2
+ formatAssistantLookupError,
3
+ formatAssistantReference,
3
4
  getActiveAssistant,
5
+ lookupAssistantByIdentifier,
4
6
  setActiveAssistant,
5
7
  } from "../lib/assistant-config.js";
8
+ import { parseAssistantTargetArg } from "../lib/assistant-target-args.js";
6
9
 
7
10
  export async function use(): Promise<void> {
8
11
  const args = process.argv.slice(3);
9
12
 
10
13
  if (args.includes("--help") || args.includes("-h")) {
11
- console.log("Usage: vellum use [<name>]");
14
+ console.log("Usage: vellum use [<name-or-id>]");
12
15
  console.log("");
13
16
  console.log("Set the active assistant for commands.");
14
17
  console.log("");
15
18
  console.log("Arguments:");
16
- console.log(" <name> Name of the assistant to make active");
19
+ console.log(
20
+ " <name-or-id> Assistant display name or ID to make active",
21
+ );
17
22
  console.log("");
18
23
  console.log(
19
24
  "When called without a name, prints the current active assistant.",
@@ -21,24 +26,33 @@ export async function use(): Promise<void> {
21
26
  process.exit(0);
22
27
  }
23
28
 
24
- const name = args.find((a) => !a.startsWith("-"));
29
+ const name = parseAssistantTargetArg(args);
25
30
 
26
31
  if (!name) {
27
32
  const active = getActiveAssistant();
28
33
  if (active) {
29
- console.log(`Active assistant: ${active}`);
34
+ const result = lookupAssistantByIdentifier(active);
35
+ if (result.status === "found") {
36
+ console.log(
37
+ `Active assistant: ${formatAssistantReference(result.entry)}`,
38
+ );
39
+ } else {
40
+ console.log(`Active assistant: ${active} (not found in lockfile)`);
41
+ }
30
42
  } else {
31
43
  console.log("No active assistant set.");
32
44
  }
33
45
  return;
34
46
  }
35
47
 
36
- const entry = findAssistantByName(name);
37
- if (!entry) {
38
- console.error(`No assistant found with name '${name}'.`);
48
+ const result = lookupAssistantByIdentifier(name);
49
+ if (result.status !== "found") {
50
+ console.error(formatAssistantLookupError(name, result));
39
51
  process.exit(1);
40
52
  }
41
53
 
42
- setActiveAssistant(name);
43
- console.log(`Active assistant set to '${name}'.`);
54
+ setActiveAssistant(result.entry.assistantId);
55
+ console.log(
56
+ `Active assistant set to ${formatAssistantReference(result.entry)}.`,
57
+ );
44
58
  }