exponential-cli 1.8.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command19 } from "commander";
4
+ import { Command as Command22 } from "commander";
5
5
 
6
6
  // src/commands/auth.ts
7
7
  import { Command } from "commander";
@@ -329,6 +329,91 @@ ${statusBadge} ${chalk3.bold(action.name)}`);
329
329
  }
330
330
  console.log();
331
331
  }
332
+ function outputTodaysActionsJson(t, filters = {}) {
333
+ console.log(JSON.stringify({
334
+ overdue: { count: t.overdue.count, actions: t.overdue.actions },
335
+ today: { count: t.today.count, actions: t.today.actions },
336
+ inbox: { count: t.inbox.count, actions: t.inbox.actions },
337
+ total: t.overdue.count + t.today.count + t.inbox.count,
338
+ filters
339
+ }, null, 2));
340
+ }
341
+ function printTodayGroup(label, group, color) {
342
+ if (group.count === 0) return;
343
+ const truncated = group.count > group.actions.length ? chalk3.gray(` (showing ${group.actions.length})`) : "";
344
+ console.log(`
345
+ ${chalk3[color].bold(`${label} (${group.count})`)}${truncated}`);
346
+ for (const a of group.actions) {
347
+ const when = a.scheduledStart ?? a.dueDate;
348
+ const stamp = when ? chalk3.gray(formatDate(new Date(when))) : "";
349
+ const project = a.projectName ? chalk3.cyan(` [${a.projectName}]`) : "";
350
+ console.log(` ${a.name}${project} ${stamp}`);
351
+ console.log(chalk3.gray(` ID: ${a.id}`));
352
+ }
353
+ }
354
+ function outputTodaysActionsPretty(t) {
355
+ const total = t.overdue.count + t.today.count + t.inbox.count;
356
+ if (total === 0) {
357
+ console.log(chalk3.gray("Nothing on your plate."));
358
+ return;
359
+ }
360
+ console.log(chalk3.bold("\nOn your plate"));
361
+ console.log(chalk3.gray("\u2500".repeat(50)));
362
+ printTodayGroup("Overdue", t.overdue, "red");
363
+ printTodayGroup("Today", t.today, "green");
364
+ printTodayGroup("Inbox", t.inbox, "gray");
365
+ if (t.overdue.count > 0) {
366
+ console.log(chalk3.gray(`
367
+ Run "exponential actions overdue" to see why the overdue pile is that size.`));
368
+ }
369
+ console.log();
370
+ }
371
+ function outputOverdueTriageJson(t) {
372
+ console.log(JSON.stringify(t, null, 2));
373
+ }
374
+ function outputOverdueTriagePretty(t) {
375
+ if (t.totalOverdue === 0) {
376
+ console.log(chalk3.green("Nothing overdue."));
377
+ return;
378
+ }
379
+ console.log(chalk3.bold(`
380
+ Overdue: ${t.totalOverdue}`));
381
+ console.log(chalk3.gray("\u2500".repeat(50)));
382
+ if (t.cohorts.length > 0) {
383
+ console.log(
384
+ `
385
+ ${chalk3.yellow.bold(`${t.cohortCount} of these were bulk-created`)} ${chalk3.gray("\u2014 stamped with one identical timestamp, so almost certainly never individually due.")}`
386
+ );
387
+ for (const c of t.cohorts) {
388
+ console.log(
389
+ `
390
+ ${chalk3.bold(`${c.count} actions`)} stamped ${chalk3.gray(c.stampedAt.toISOString())} ${chalk3.red(`(${c.daysOverdue}d overdue)`)}`
391
+ );
392
+ if (c.projectNames.length > 0) {
393
+ console.log(` ${chalk3.cyan("Projects:")} ${c.projectNames.join(", ")}`);
394
+ }
395
+ for (const a of c.actions.slice(0, 4)) {
396
+ console.log(chalk3.gray(` \xB7 ${a.name.substring(0, 62)}`));
397
+ }
398
+ if (c.actions.length > 4) {
399
+ console.log(chalk3.gray(` \xB7 \u2026 and ${c.actions.length - 4} more`));
400
+ }
401
+ console.log(
402
+ chalk3.gray(` Amnesty: `) + `exponential actions defer --ids ${c.actionIds.slice(0, 2).join(",")}${c.actionIds.length > 2 ? ",\u2026" : ""}`
403
+ );
404
+ }
405
+ }
406
+ if (t.loose.length > 0) {
407
+ console.log(`
408
+ ${chalk3.bold(`${t.loose.length} individually dated`)} ${chalk3.gray("\u2014 real debt, oldest first.")}`);
409
+ for (const a of t.loose) {
410
+ const project = a.projectName ? chalk3.cyan(` [${a.projectName}]`) : "";
411
+ console.log(` ${chalk3.red(`${String(a.daysOverdue).padStart(3)}d`)} ${a.name.substring(0, 62)}${project}`);
412
+ console.log(chalk3.gray(` ID: ${a.id}`));
413
+ }
414
+ }
415
+ console.log();
416
+ }
332
417
  function outputProjectsJson(projects) {
333
418
  const output = {
334
419
  projects: projects.map(transformProject),
@@ -667,30 +752,59 @@ function outputStagesJson(stages) {
667
752
  total: stages.length
668
753
  }, null, 2));
669
754
  }
670
- function transformComment(comment) {
671
- const parentKey = "actionId" in comment ? "actionId" : "ticketId";
672
- const parentId = comment[parentKey];
755
+ var COMMENT_PARENT_KEYS = ["actionId", "ticketId", "goalId", "featureId", "pageId"];
756
+ function normalizeComment(comment) {
757
+ const raw = comment;
758
+ const parentKey = COMMENT_PARENT_KEYS.find((k) => k in raw) ?? "parentId";
759
+ const author = raw.author ?? raw.createdBy ?? null;
673
760
  return {
674
761
  id: comment.id,
675
- [parentKey]: parentId,
676
- authorId: comment.authorId,
677
- content: comment.content,
678
- author: comment.author,
679
- createdAt: new Date(comment.createdAt).toISOString(),
680
- updatedAt: new Date(comment.updatedAt).toISOString()
762
+ parentKey,
763
+ parentId: raw[parentKey],
764
+ authorId: raw.authorId ?? raw.createdById,
765
+ author,
766
+ content: raw.content ?? raw.body ?? "",
767
+ createdAt: new Date(comment.createdAt),
768
+ updatedAt: new Date(comment.updatedAt),
769
+ parentCommentId: raw.parentId ?? null,
770
+ threadId: raw.threadId ?? null,
771
+ resolvedAt: raw.resolvedAt ? new Date(raw.resolvedAt) : null
772
+ };
773
+ }
774
+ function transformComment(comment) {
775
+ const n = normalizeComment(comment);
776
+ const out = {
777
+ id: n.id,
778
+ [n.parentKey]: n.parentId,
779
+ authorId: n.authorId,
780
+ content: n.content,
781
+ author: n.author,
782
+ createdAt: n.createdAt.toISOString(),
783
+ updatedAt: n.updatedAt.toISOString()
681
784
  };
785
+ if (n.threadId !== null) out.threadId = n.threadId;
786
+ if (n.parentCommentId !== null && n.parentKey !== "parentId") {
787
+ out.parentCommentId = n.parentCommentId;
788
+ }
789
+ if (n.resolvedAt) out.resolvedAt = n.resolvedAt.toISOString();
790
+ return out;
682
791
  }
683
792
  function outputCommentJson(comment) {
684
793
  console.log(JSON.stringify(transformComment(comment), null, 2));
685
794
  }
686
795
  function outputCommentPretty(comment) {
687
- const author = comment.author?.name ?? comment.authorId;
688
- const when = formatDate(new Date(comment.createdAt));
796
+ const n = normalizeComment(comment);
797
+ const author = n.author?.name ?? n.authorId;
798
+ const when = formatDate(n.createdAt);
799
+ const badges = [
800
+ n.parentCommentId && n.parentKey !== "parentId" ? chalk3.gray("(reply)") : "",
801
+ n.resolvedAt ? chalk3.green("(resolved)") : ""
802
+ ].filter(Boolean).join(" ");
689
803
  console.log(chalk3.gray("\u2500".repeat(50)));
690
- console.log(`${chalk3.bold(author)} ${chalk3.gray(`\u2014 ${when}`)}`);
691
- console.log(chalk3.gray(` ID: ${comment.id}`));
804
+ console.log(`${chalk3.bold(author)} ${chalk3.gray(`\u2014 ${when}`)}${badges ? ` ${badges}` : ""}`);
805
+ console.log(chalk3.gray(` ID: ${n.id}`));
692
806
  console.log();
693
- console.log(comment.content);
807
+ console.log(n.content);
694
808
  console.log();
695
809
  }
696
810
  function outputCommentsJson(comments) {
@@ -1312,6 +1426,22 @@ var VALID_PRIORITIES = [
1312
1426
  "Watch",
1313
1427
  "Someday Maybe"
1314
1428
  ];
1429
+ function parseNullableDate(raw, flag) {
1430
+ if (raw === void 0) return void 0;
1431
+ if (raw === "null") return null;
1432
+ const parsed = new Date(raw);
1433
+ if (isNaN(parsed.getTime())) {
1434
+ throw new Error(`Invalid --${flag} value "${raw}". Use YYYY-MM-DD, an ISO datetime, or "null".`);
1435
+ }
1436
+ return parsed;
1437
+ }
1438
+ function parseIds(raw) {
1439
+ const ids = raw.split(",").map((s) => s.trim()).filter(Boolean);
1440
+ if (ids.length === 0) {
1441
+ throw new Error("No action IDs supplied. Pass --ids id1,id2");
1442
+ }
1443
+ return ids;
1444
+ }
1315
1445
  function createActionsCommand() {
1316
1446
  const actions = new Command2("actions").description("Manage actions/tasks");
1317
1447
  actions.command("list").description("List all actions").option("--project <id>", "Filter by project ID").option("--status <status>", "Filter by kanban status (BACKLOG, TODO, IN_PROGRESS, IN_REVIEW, DONE)").option("--assignee <id>", "Filter by assignee ID").action(async (options, cmd) => {
@@ -1337,16 +1467,76 @@ function createActionsCommand() {
1337
1467
  handleError(error, useJson);
1338
1468
  }
1339
1469
  });
1340
- actions.command("today").description("Get actions due today").option("--workspace <id>", "Filter by workspace ID").action(async (options, cmd) => {
1470
+ actions.command("today").description("What's on your plate: overdue, today, and inbox").option("--workspace <id>", "Filter by workspace ID").option("--due-only", "Only actions whose dueDate is today (the pre-1.8 shape \u2014 excludes overdue)").action(async (options, cmd) => {
1341
1471
  const globalOpts = cmd.optsWithGlobals();
1342
1472
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
1343
1473
  try {
1344
1474
  const client = getClient();
1345
- const actions2 = await client.actions.getToday(options.workspace);
1475
+ if (options.dueOnly) {
1476
+ const actions2 = await client.actions.getToday(options.workspace);
1477
+ if (useJson) {
1478
+ outputActionsJson(actions2, { workspaceId: options.workspace });
1479
+ } else {
1480
+ outputActionsPretty(actions2);
1481
+ }
1482
+ return;
1483
+ }
1484
+ const todays = await client.actions.getTodaysActions(options.workspace);
1346
1485
  if (useJson) {
1347
- outputActionsJson(actions2, { workspaceId: options.workspace });
1486
+ outputTodaysActionsJson(todays, { workspaceId: options.workspace });
1348
1487
  } else {
1349
- outputActionsPretty(actions2);
1488
+ outputTodaysActionsPretty(todays);
1489
+ }
1490
+ } catch (error) {
1491
+ handleError(error, useJson);
1492
+ }
1493
+ });
1494
+ actions.command("overdue").description("Why the overdue pile is that size: bulk-created cohorts vs real debt").option("--workspace <id>", "Filter by workspace ID").action(async (options, cmd) => {
1495
+ const globalOpts = cmd.optsWithGlobals();
1496
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
1497
+ try {
1498
+ const client = getClient();
1499
+ const triage = await client.actions.getOverdueTriage(options.workspace);
1500
+ if (useJson) {
1501
+ outputOverdueTriageJson(triage);
1502
+ } else {
1503
+ outputOverdueTriagePretty(triage);
1504
+ }
1505
+ } catch (error) {
1506
+ handleError(error, useJson);
1507
+ }
1508
+ });
1509
+ actions.command("defer").description("Amnesty: clear dates so actions fall back to their project backlog untimed").requiredOption("--ids <ids>", "Comma-separated action IDs").action(async (options, cmd) => {
1510
+ const globalOpts = cmd.optsWithGlobals();
1511
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
1512
+ try {
1513
+ const ids = parseIds(options.ids);
1514
+ const client = getClient();
1515
+ const result = await client.actions.bulkDefer(ids);
1516
+ if (useJson) {
1517
+ console.log(JSON.stringify(result, null, 2));
1518
+ } else {
1519
+ console.log(`\u2713 ${result.message}`);
1520
+ }
1521
+ } catch (error) {
1522
+ handleError(error, useJson);
1523
+ }
1524
+ });
1525
+ actions.command("reschedule").description('Move actions to a new do-date (use "actions defer" if they were never really due)').requiredOption("--ids <ids>", "Comma-separated action IDs").requiredOption("--to <date>", 'New do-date (YYYY-MM-DD, or "today")').action(async (options, cmd) => {
1526
+ const globalOpts = cmd.optsWithGlobals();
1527
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
1528
+ try {
1529
+ const ids = parseIds(options.ids);
1530
+ const when = options.to === "today" ? /* @__PURE__ */ new Date() : new Date(options.to);
1531
+ if (isNaN(when.getTime())) {
1532
+ throw new Error('Invalid date format. Use YYYY-MM-DD or "today"');
1533
+ }
1534
+ const client = getClient();
1535
+ const result = await client.actions.bulkReschedule(ids, when);
1536
+ if (useJson) {
1537
+ console.log(JSON.stringify(result, null, 2));
1538
+ } else {
1539
+ console.log(`\u2713 Rescheduled ${result.count} action${result.count === 1 ? "" : "s"}`);
1350
1540
  }
1351
1541
  } catch (error) {
1352
1542
  handleError(error, useJson);
@@ -1395,7 +1585,7 @@ function createActionsCommand() {
1395
1585
  handleError(error, useJson);
1396
1586
  }
1397
1587
  });
1398
- actions.command("update").description("Update an existing action").requiredOption("--id <id>", "Action ID to update").option("-n, --name <name>", "New action name").option("-d, --description <text>", "New description").option("-p, --project <id>", "Move to project ID").option("--priority <priority>", "Priority (Quick, Scheduled, 1st Priority, etc.)").option("--status <status>", "Status (ACTIVE, COMPLETED, CANCELLED)").option("--kanban <status>", "Kanban status (BACKLOG, TODO, IN_PROGRESS, IN_REVIEW, DONE)").option("--due <date>", 'Due date (YYYY-MM-DD or "null" to clear)').action(async (options, cmd) => {
1588
+ actions.command("update").description("Update an existing action").requiredOption("--id <id>", "Action ID to update").option("-n, --name <name>", "New action name").option("-d, --description <text>", "New description").option("-p, --project <id>", "Move to project ID").option("--priority <priority>", "Priority (Quick, Scheduled, 1st Priority, etc.)").option("--status <status>", "Status (ACTIVE, COMPLETED, CANCELLED)").option("--kanban <status>", "Kanban status (BACKLOG, TODO, IN_PROGRESS, IN_REVIEW, DONE)").option("--due <date>", 'Due date (YYYY-MM-DD or "null" to clear)').option("--scheduled-start <datetime>", 'Do-date: when you plan to work on it (YYYY-MM-DD, ISO datetime, or "null" to clear). This is what /today partitions on.').option("--scheduled-end <datetime>", 'End of the time block (YYYY-MM-DD, ISO datetime, or "null" to clear)').action(async (options, cmd) => {
1399
1589
  const globalOpts = cmd.optsWithGlobals();
1400
1590
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
1401
1591
  try {
@@ -1411,6 +1601,8 @@ function createActionsCommand() {
1411
1601
  throw new Error("Invalid due date format. Use YYYY-MM-DD");
1412
1602
  }
1413
1603
  }
1604
+ const scheduledStart = parseNullableDate(options.scheduledStart, "scheduled-start");
1605
+ const scheduledEnd = parseNullableDate(options.scheduledEnd, "scheduled-end");
1414
1606
  const client = getClient();
1415
1607
  const action = await client.actions.update({
1416
1608
  id: options.id,
@@ -1420,7 +1612,9 @@ function createActionsCommand() {
1420
1612
  priority: options.priority,
1421
1613
  status: options.status,
1422
1614
  kanbanStatus: options.kanban,
1423
- dueDate
1615
+ dueDate,
1616
+ scheduledStart,
1617
+ scheduledEnd
1424
1618
  });
1425
1619
  if (useJson) {
1426
1620
  outputActionJson(action);
@@ -2034,7 +2228,7 @@ function createEpicsCommand() {
2034
2228
  const globalOpts = cmd.optsWithGlobals();
2035
2229
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
2036
2230
  try {
2037
- const parseNullableDate = (v) => {
2231
+ const parseNullableDate2 = (v) => {
2038
2232
  if (v === void 0) return void 0;
2039
2233
  if (v === "null") return null;
2040
2234
  const d = new Date(v);
@@ -2053,8 +2247,8 @@ function createEpicsCommand() {
2053
2247
  description,
2054
2248
  status,
2055
2249
  priority,
2056
- startDate: parseNullableDate(options.start),
2057
- targetDate: parseNullableDate(options.target)
2250
+ startDate: parseNullableDate2(options.start),
2251
+ targetDate: parseNullableDate2(options.target)
2058
2252
  });
2059
2253
  if (useJson) outputEpicJson(epic);
2060
2254
  else {
@@ -2070,7 +2264,7 @@ function createEpicsCommand() {
2070
2264
  }
2071
2265
 
2072
2266
  // src/commands/features.ts
2073
- import { Command as Command10 } from "commander";
2267
+ import { Command as Command11 } from "commander";
2074
2268
 
2075
2269
  // src/commands/stories.ts
2076
2270
  import { Command as Command6 } from "commander";
@@ -2491,6 +2685,240 @@ function createAreasCommand() {
2491
2685
  return areas;
2492
2686
  }
2493
2687
 
2688
+ // src/commands/featureComments.ts
2689
+ import { Command as Command10 } from "commander";
2690
+
2691
+ // src/utils/mentions.ts
2692
+ function candidateLabels(member) {
2693
+ const labels = [member.id];
2694
+ if (member.name) {
2695
+ labels.push(member.name);
2696
+ const first = member.name.trim().split(/\s+/)[0];
2697
+ if (first) labels.push(first);
2698
+ }
2699
+ if (member.email) {
2700
+ labels.push(member.email);
2701
+ const local = member.email.split("@")[0];
2702
+ if (local) labels.push(local);
2703
+ }
2704
+ return labels;
2705
+ }
2706
+ function describe(member) {
2707
+ return member.name ?? member.email ?? member.id;
2708
+ }
2709
+ function resolveMember(members, token) {
2710
+ const needle = token.trim().toLowerCase().replace(/^@/, "");
2711
+ const exact = members.filter(
2712
+ (m) => m.id.toLowerCase() === needle || m.name?.toLowerCase() === needle || m.email?.toLowerCase() === needle
2713
+ );
2714
+ if (exact.length === 1) return exact[0];
2715
+ if (exact.length > 1) {
2716
+ throw new Error(
2717
+ `"${token}" matches ${exact.length} members. Use a user id instead: ${exact.map((m) => `${describe(m)} (${m.id})`).join(", ")}`
2718
+ );
2719
+ }
2720
+ const partial = members.filter(
2721
+ (m) => candidateLabels(m).some((l) => l.toLowerCase() === needle)
2722
+ );
2723
+ if (partial.length === 1) return partial[0];
2724
+ if (partial.length > 1) {
2725
+ throw new Error(
2726
+ `"${token}" is ambiguous \u2014 matches ${partial.map((m) => `${describe(m)} (${m.id})`).join(", ")}. Use a full name, email, or user id.`
2727
+ );
2728
+ }
2729
+ const known = members.map(describe).join(", ");
2730
+ throw new Error(
2731
+ `No workspace member matches "${token}". Members: ${known || "(none)"}. Run \`exponential workspaces members\` to see the roster.`
2732
+ );
2733
+ }
2734
+ function escapeRegExp(value) {
2735
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2736
+ }
2737
+ async function applyMentions(client, workspaceId, body, mentions) {
2738
+ if (!mentions || mentions.length === 0) return body;
2739
+ const members = await client.workspaces.listMembers(workspaceId);
2740
+ let result = body;
2741
+ const prepend = [];
2742
+ for (const token of mentions) {
2743
+ const member = resolveMember(members, token);
2744
+ const handle = token.trim().replace(/^@/, "");
2745
+ const inline = new RegExp(`@${escapeRegExp(handle)}(?![\\w-])`, "gi");
2746
+ const substituted = result.replace(inline, member.mentionSyntax);
2747
+ if (substituted !== result) {
2748
+ result = substituted;
2749
+ } else if (!result.includes(member.mentionSyntax)) {
2750
+ prepend.push(member.mentionSyntax);
2751
+ }
2752
+ }
2753
+ return prepend.length > 0 ? `${prepend.join(" ")} ${result}` : result;
2754
+ }
2755
+ var MENTION_OPTION_DESCRIPTION = "Mention a workspace member by name, email, or id (repeatable). Writing @name in the message substitutes in place; otherwise the mention is prepended.";
2756
+ function collectMention(value, previous) {
2757
+ return [...previous, value];
2758
+ }
2759
+
2760
+ // src/commands/featureComments.ts
2761
+ function createFeatureCommentsCommand() {
2762
+ const comment = new Command10("comment").description(
2763
+ "Read and post comments on a feature. Mention teammates with --mention."
2764
+ );
2765
+ comment.command("list").description("List comments on a feature").requiredOption("--feature <id>", "Feature ID").action(async (options, cmd) => {
2766
+ const globalOpts = cmd.optsWithGlobals();
2767
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
2768
+ try {
2769
+ const client = getClient();
2770
+ const comments = await client.featureComments.list(options.feature);
2771
+ if (useJson) outputCommentsJson(comments);
2772
+ else outputCommentsPretty(comments);
2773
+ } catch (error) {
2774
+ handleError(error, useJson);
2775
+ }
2776
+ });
2777
+ comment.command("add").description("Add a comment to a feature").requiredOption("--feature <id>", "Feature ID").requiredOption("-m, --message <text>", "Comment body (markdown supported)").option("--mention <name>", MENTION_OPTION_DESCRIPTION, collectMention, []).option(
2778
+ "--workspace <slug|id>",
2779
+ "Workspace used to resolve --mention (defaults to your default workspace)"
2780
+ ).option("--scope <id>", "Attach to a scope's activity feed instead of the feature").action(
2781
+ async (options, cmd) => {
2782
+ const globalOpts = cmd.optsWithGlobals();
2783
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
2784
+ try {
2785
+ const client = getClient();
2786
+ const body = options.mention.length > 0 ? await applyMentions(
2787
+ client,
2788
+ await resolveWorkspaceId(client, options.workspace),
2789
+ options.message,
2790
+ options.mention
2791
+ ) : options.message;
2792
+ const created = await client.featureComments.create({
2793
+ featureId: options.feature,
2794
+ scopeId: options.scope,
2795
+ body
2796
+ });
2797
+ if (useJson) {
2798
+ outputCommentJson(created);
2799
+ } else {
2800
+ console.log("\n\u2713 Comment added");
2801
+ outputCommentPretty(created);
2802
+ }
2803
+ } catch (error) {
2804
+ handleError(error, useJson);
2805
+ }
2806
+ }
2807
+ );
2808
+ comment.command("reply").description("Reply to a comment on a feature").requiredOption("--comment-id <id>", "Comment ID to reply to").requiredOption("-m, --message <text>", "Reply body (markdown supported)").option("--mention <name>", MENTION_OPTION_DESCRIPTION, collectMention, []).option(
2809
+ "--workspace <slug|id>",
2810
+ "Workspace used to resolve --mention (defaults to your default workspace)"
2811
+ ).action(
2812
+ async (options, cmd) => {
2813
+ const globalOpts = cmd.optsWithGlobals();
2814
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
2815
+ try {
2816
+ const client = getClient();
2817
+ const body = options.mention.length > 0 ? await applyMentions(
2818
+ client,
2819
+ await resolveWorkspaceId(client, options.workspace),
2820
+ options.message,
2821
+ options.mention
2822
+ ) : options.message;
2823
+ const created = await client.featureComments.reply({
2824
+ parentId: options.commentId,
2825
+ body
2826
+ });
2827
+ if (useJson) {
2828
+ outputCommentJson(created);
2829
+ } else {
2830
+ console.log("\n\u2713 Reply added");
2831
+ outputCommentPretty(created);
2832
+ }
2833
+ } catch (error) {
2834
+ handleError(error, useJson);
2835
+ }
2836
+ }
2837
+ );
2838
+ comment.command("update").description("Update one of your own comments").requiredOption("--comment-id <id>", "Comment ID").requiredOption("-m, --message <text>", "New comment body").option("--mention <name>", MENTION_OPTION_DESCRIPTION, collectMention, []).option(
2839
+ "--workspace <slug|id>",
2840
+ "Workspace used to resolve --mention (defaults to your default workspace)"
2841
+ ).action(
2842
+ async (options, cmd) => {
2843
+ const globalOpts = cmd.optsWithGlobals();
2844
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
2845
+ try {
2846
+ const client = getClient();
2847
+ const body = options.mention.length > 0 ? await applyMentions(
2848
+ client,
2849
+ await resolveWorkspaceId(client, options.workspace),
2850
+ options.message,
2851
+ options.mention
2852
+ ) : options.message;
2853
+ const updated = await client.featureComments.update({
2854
+ commentId: options.commentId,
2855
+ body
2856
+ });
2857
+ if (useJson) {
2858
+ outputCommentJson(updated);
2859
+ } else {
2860
+ console.log("\n\u2713 Comment updated");
2861
+ outputCommentPretty(updated);
2862
+ }
2863
+ } catch (error) {
2864
+ handleError(error, useJson);
2865
+ }
2866
+ }
2867
+ );
2868
+ comment.command("rm").description("Delete one of your own comments").requiredOption("--comment-id <id>", "Comment ID").action(async (options, cmd) => {
2869
+ const globalOpts = cmd.optsWithGlobals();
2870
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
2871
+ try {
2872
+ const client = getClient();
2873
+ await client.featureComments.delete(options.commentId);
2874
+ if (useJson) {
2875
+ console.log(JSON.stringify({ deleted: true, id: options.commentId }, null, 2));
2876
+ } else {
2877
+ console.log("\u2713 Comment deleted");
2878
+ }
2879
+ } catch (error) {
2880
+ handleError(error, useJson);
2881
+ }
2882
+ });
2883
+ comment.command("resolve").description("Resolve an anchored comment thread").requiredOption("--feature <id>", "Feature ID").requiredOption("--thread <id>", "Thread ID (anchored comments only)").action(async (options, cmd) => {
2884
+ const globalOpts = cmd.optsWithGlobals();
2885
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
2886
+ try {
2887
+ const client = getClient();
2888
+ await client.featureComments.resolve({
2889
+ featureId: options.feature,
2890
+ threadId: options.thread
2891
+ });
2892
+ if (useJson) {
2893
+ console.log(JSON.stringify({ resolved: true, threadId: options.thread }, null, 2));
2894
+ } else {
2895
+ console.log("\u2713 Thread resolved");
2896
+ }
2897
+ } catch (error) {
2898
+ handleError(error, useJson);
2899
+ }
2900
+ });
2901
+ comment.command("unresolve").description("Reopen a resolved comment thread").requiredOption("--feature <id>", "Feature ID").requiredOption("--thread <id>", "Thread ID (anchored comments only)").action(async (options, cmd) => {
2902
+ const globalOpts = cmd.optsWithGlobals();
2903
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
2904
+ try {
2905
+ const client = getClient();
2906
+ await client.featureComments.unresolve({
2907
+ featureId: options.feature,
2908
+ threadId: options.thread
2909
+ });
2910
+ if (useJson) {
2911
+ console.log(JSON.stringify({ resolved: false, threadId: options.thread }, null, 2));
2912
+ } else {
2913
+ console.log("\u2713 Thread reopened");
2914
+ }
2915
+ } catch (error) {
2916
+ handleError(error, useJson);
2917
+ }
2918
+ });
2919
+ return comment;
2920
+ }
2921
+
2494
2922
  // src/commands/features.ts
2495
2923
  var FEATURE_STATUSES = [
2496
2924
  "IDEA",
@@ -2500,6 +2928,17 @@ var FEATURE_STATUSES = [
2500
2928
  "DEPRECATED",
2501
2929
  "ARCHIVED"
2502
2930
  ];
2931
+ function summarizeTicket(ticket) {
2932
+ return {
2933
+ id: ticket.id,
2934
+ shortId: ticket.shortId ?? null,
2935
+ number: ticket.number ?? null,
2936
+ title: ticket.title
2937
+ };
2938
+ }
2939
+ function ticketLabel(ticket) {
2940
+ return ticket.shortId ?? (ticket.number != null ? `#${ticket.number}` : ticket.id);
2941
+ }
2503
2942
  function validateFeatureStatus(value) {
2504
2943
  if (!value) return void 0;
2505
2944
  if (!FEATURE_STATUSES.includes(value)) {
@@ -2510,7 +2949,7 @@ function validateFeatureStatus(value) {
2510
2949
  return value;
2511
2950
  }
2512
2951
  function createFeaturesCommand() {
2513
- const features = new Command10("features").description(
2952
+ const features = new Command11("features").description(
2514
2953
  "Manage features. A feature lives under a product and groups user stories, scopes, and tickets."
2515
2954
  );
2516
2955
  features.command("list").description("List features in a product").requiredOption("--product <slug|id>", "Product slug or CUID").option("--workspace <slug|id>", "Workspace (required when --product is a slug)").option("--status <status>", `Filter by status: ${FEATURE_STATUSES.join(", ")}`).action(
@@ -2606,6 +3045,119 @@ function createFeaturesCommand() {
2606
3045
  }
2607
3046
  }
2608
3047
  );
3048
+ features.command("delete <id>").description(
3049
+ "Delete a feature. Refuses if it still has tickets \u2014 deleting a feature does not delete its tickets, it unlinks them (Ticket.featureId is SetNull), silently orphaning them in the product backlog."
3050
+ ).option(
3051
+ "--with-tickets",
3052
+ "Delete the feature's tickets first, then the feature (destructive)"
3053
+ ).option(
3054
+ "--orphan-tickets",
3055
+ "Delete the feature anyway, leaving its tickets in the backlog unlinked"
3056
+ ).action(
3057
+ async (id, options, cmd) => {
3058
+ const globalOpts = cmd.optsWithGlobals();
3059
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
3060
+ try {
3061
+ if (options.withTickets && options.orphanTickets) {
3062
+ throw new Error(
3063
+ "--with-tickets and --orphan-tickets are mutually exclusive"
3064
+ );
3065
+ }
3066
+ const client = getClient();
3067
+ const feature = await client.features.get(id);
3068
+ const tickets = feature._count?.tickets === 0 ? [] : await client.tickets.list({
3069
+ productId: feature.productId,
3070
+ featureId: feature.id
3071
+ });
3072
+ if (tickets.length > 0 && !options.withTickets && !options.orphanTickets) {
3073
+ const reason = `has ${tickets.length} ticket${tickets.length === 1 ? "" : "s"}`;
3074
+ if (useJson) {
3075
+ console.log(
3076
+ JSON.stringify(
3077
+ {
3078
+ deleted: false,
3079
+ id: feature.id,
3080
+ reason,
3081
+ tickets: tickets.map(summarizeTicket)
3082
+ },
3083
+ null,
3084
+ 2
3085
+ )
3086
+ );
3087
+ } else {
3088
+ console.error(
3089
+ `
3090
+ Refusing to delete "${feature.name}": it ${reason}.
3091
+ Deleting the feature would leave them unlinked in the product backlog.
3092
+ --with-tickets delete those tickets first, then the feature
3093
+ --orphan-tickets delete the feature anyway and unlink them`
3094
+ );
3095
+ for (const t of tickets) {
3096
+ console.error(` - ${ticketLabel(t)} ${t.title}`);
3097
+ }
3098
+ }
3099
+ process.exitCode = 1;
3100
+ return;
3101
+ }
3102
+ const ticketsDeleted = [];
3103
+ const ticketsFailed = [];
3104
+ if (options.withTickets) {
3105
+ for (const ticket of tickets) {
3106
+ try {
3107
+ await client.tickets.delete(ticket.id);
3108
+ ticketsDeleted.push(summarizeTicket(ticket));
3109
+ } catch (error) {
3110
+ ticketsFailed.push({
3111
+ ...summarizeTicket(ticket),
3112
+ error: error instanceof Error ? error.message : String(error)
3113
+ });
3114
+ }
3115
+ }
3116
+ }
3117
+ if (ticketsFailed.length > 0) {
3118
+ const reason = `failed to delete ${ticketsFailed.length} of ${tickets.length} tickets`;
3119
+ if (useJson) {
3120
+ console.log(
3121
+ JSON.stringify(
3122
+ { deleted: false, id: feature.id, reason, ticketsDeleted, ticketsFailed },
3123
+ null,
3124
+ 2
3125
+ )
3126
+ );
3127
+ } else {
3128
+ console.error(`
3129
+ Aborted: ${reason}. Feature "${feature.name}" was NOT deleted.`);
3130
+ for (const t of ticketsFailed) {
3131
+ console.error(` - ${t.shortId ?? t.id}: ${t.error}`);
3132
+ }
3133
+ }
3134
+ process.exitCode = 1;
3135
+ return;
3136
+ }
3137
+ await client.features.delete(feature.id);
3138
+ const ticketsOrphaned = options.orphanTickets ? tickets.map(summarizeTicket) : [];
3139
+ if (useJson) {
3140
+ console.log(
3141
+ JSON.stringify(
3142
+ { deleted: true, id: feature.id, ticketsDeleted, ticketsOrphaned },
3143
+ null,
3144
+ 2
3145
+ )
3146
+ );
3147
+ } else {
3148
+ console.log("\n\u2713 Feature deleted");
3149
+ if (ticketsDeleted.length > 0) {
3150
+ console.log(` ${ticketsDeleted.length} ticket(s) deleted with it`);
3151
+ }
3152
+ if (ticketsOrphaned.length > 0) {
3153
+ console.log(` ${ticketsOrphaned.length} ticket(s) left unlinked in the backlog`);
3154
+ }
3155
+ }
3156
+ } catch (error) {
3157
+ handleError(error, useJson);
3158
+ }
3159
+ }
3160
+ );
2609
3161
  features.command("link-page").description("Link a Knowledge page (PRD, spec, research) to a feature").requiredOption("--feature <id>", "Feature CUID").requiredOption("--page <id>", "Page CUID").option("--scope <id>", "Feature scope CUID to pin the page to").action(
2610
3162
  async (options, cmd) => {
2611
3163
  const globalOpts = cmd.optsWithGlobals();
@@ -2645,11 +3197,12 @@ function createFeaturesCommand() {
2645
3197
  features.addCommand(createScopesCommand());
2646
3198
  features.addCommand(createRequirementsCommand());
2647
3199
  features.addCommand(createAreasCommand());
3200
+ features.addCommand(createFeatureCommentsCommand());
2648
3201
  return features;
2649
3202
  }
2650
3203
 
2651
3204
  // src/commands/labels.ts
2652
- import { Command as Command11 } from "commander";
3205
+ import { Command as Command12 } from "commander";
2653
3206
  import chalk5 from "chalk";
2654
3207
  var TRIAGE_LABELS = [
2655
3208
  { slug: "needs-triage", name: "needs-triage", color: "avatar-orange" },
@@ -2703,7 +3256,7 @@ async function resolveLabelIds(client, workspaceId, values) {
2703
3256
  return [...byId, ...resolvedFromSlugs];
2704
3257
  }
2705
3258
  function createLabelsCommand() {
2706
- const labels = new Command11("labels").description(
3259
+ const labels = new Command12("labels").description(
2707
3260
  "Manage labels (orthogonal tags applied to actions, tickets, features, and epics)."
2708
3261
  );
2709
3262
  labels.command("list").description("List labels visible in a workspace").option("--workspace <slug|id>", "Workspace slug or CUID").option(
@@ -2806,7 +3359,7 @@ function createLabelsCommand() {
2806
3359
  }
2807
3360
 
2808
3361
  // src/commands/organizations.ts
2809
- import { Command as Command12 } from "commander";
3362
+ import { Command as Command13 } from "commander";
2810
3363
  var VALID_SIZES = [
2811
3364
  "1-10",
2812
3365
  "11-50",
@@ -2816,7 +3369,7 @@ var VALID_SIZES = [
2816
3369
  "1000+"
2817
3370
  ];
2818
3371
  function createOrganizationsCommand() {
2819
- const organizations = new Command12("organizations").alias("orgs").description("Manage CRM organizations");
3372
+ const organizations = new Command13("organizations").alias("orgs").description("Manage CRM organizations");
2820
3373
  organizations.command("list").description("List organizations").requiredOption("--workspace <id>", "Workspace ID").option("--search <query>", "Search by name or description").option("--industry <industry>", "Filter by industry").option("--limit <n>", "Max results to return", parseInt).option("--cursor <cursor>", "Pagination cursor").action(async (options, cmd) => {
2821
3374
  const globalOpts = cmd.optsWithGlobals();
2822
3375
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
@@ -2885,7 +3438,105 @@ function createOrganizationsCommand() {
2885
3438
 
2886
3439
  // src/commands/pages.ts
2887
3440
  import { readFileSync } from "fs";
2888
- import { Command as Command13 } from "commander";
3441
+ import { Command as Command15 } from "commander";
3442
+
3443
+ // src/commands/pageComments.ts
3444
+ import { Command as Command14 } from "commander";
3445
+ function createPageCommentsCommand() {
3446
+ const comment = new Command14("comment").description(
3447
+ "Read and post comments on a page. Mention teammates with --mention."
3448
+ );
3449
+ comment.command("list").description("List comments on a page").requiredOption("--page <id>", "Page ID").action(async (options, cmd) => {
3450
+ const globalOpts = cmd.optsWithGlobals();
3451
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
3452
+ try {
3453
+ const client = getClient();
3454
+ const comments = await client.pageComments.list(options.page);
3455
+ if (useJson) outputCommentsJson(comments);
3456
+ else outputCommentsPretty(comments);
3457
+ } catch (error) {
3458
+ handleError(error, useJson);
3459
+ }
3460
+ });
3461
+ comment.command("add").description("Add a comment to a page").requiredOption("--page <id>", "Page ID").requiredOption("-m, --message <text>", "Comment body (markdown supported)").option("--mention <name>", MENTION_OPTION_DESCRIPTION, collectMention, []).option(
3462
+ "--workspace <slug|id>",
3463
+ "Workspace used to resolve --mention (defaults to your default workspace)"
3464
+ ).action(
3465
+ async (options, cmd) => {
3466
+ const globalOpts = cmd.optsWithGlobals();
3467
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
3468
+ try {
3469
+ const client = getClient();
3470
+ const body = options.mention.length > 0 ? await applyMentions(
3471
+ client,
3472
+ await resolveWorkspaceId(client, options.workspace),
3473
+ options.message,
3474
+ options.mention
3475
+ ) : options.message;
3476
+ const created = await client.pageComments.create({
3477
+ pageId: options.page,
3478
+ body
3479
+ });
3480
+ if (useJson) {
3481
+ outputCommentJson(created);
3482
+ } else {
3483
+ console.log("\n\u2713 Comment added");
3484
+ outputCommentPretty(created);
3485
+ }
3486
+ } catch (error) {
3487
+ handleError(error, useJson);
3488
+ }
3489
+ }
3490
+ );
3491
+ comment.command("update").description("Update one of your own comments").requiredOption("--comment-id <id>", "Comment ID").requiredOption("-m, --message <text>", "New comment body").option("--mention <name>", MENTION_OPTION_DESCRIPTION, collectMention, []).option(
3492
+ "--workspace <slug|id>",
3493
+ "Workspace used to resolve --mention (defaults to your default workspace)"
3494
+ ).action(
3495
+ async (options, cmd) => {
3496
+ const globalOpts = cmd.optsWithGlobals();
3497
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
3498
+ try {
3499
+ const client = getClient();
3500
+ const body = options.mention.length > 0 ? await applyMentions(
3501
+ client,
3502
+ await resolveWorkspaceId(client, options.workspace),
3503
+ options.message,
3504
+ options.mention
3505
+ ) : options.message;
3506
+ const updated = await client.pageComments.update({
3507
+ commentId: options.commentId,
3508
+ body
3509
+ });
3510
+ if (useJson) {
3511
+ outputCommentJson(updated);
3512
+ } else {
3513
+ console.log("\n\u2713 Comment updated");
3514
+ outputCommentPretty(updated);
3515
+ }
3516
+ } catch (error) {
3517
+ handleError(error, useJson);
3518
+ }
3519
+ }
3520
+ );
3521
+ comment.command("rm").description("Delete one of your own comments").requiredOption("--comment-id <id>", "Comment ID").action(async (options, cmd) => {
3522
+ const globalOpts = cmd.optsWithGlobals();
3523
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
3524
+ try {
3525
+ const client = getClient();
3526
+ await client.pageComments.delete(options.commentId);
3527
+ if (useJson) {
3528
+ console.log(JSON.stringify({ deleted: true, id: options.commentId }, null, 2));
3529
+ } else {
3530
+ console.log("\u2713 Comment deleted");
3531
+ }
3532
+ } catch (error) {
3533
+ handleError(error, useJson);
3534
+ }
3535
+ });
3536
+ return comment;
3537
+ }
3538
+
3539
+ // src/commands/pages.ts
2889
3540
  function readBody(body, bodyFile) {
2890
3541
  if (body !== void 0) return body;
2891
3542
  if (bodyFile === void 0) return void 0;
@@ -2893,7 +3544,7 @@ function readBody(body, bodyFile) {
2893
3544
  return readFileSync(bodyFile, "utf-8");
2894
3545
  }
2895
3546
  function createPagesCommand() {
2896
- const pages = new Command13("pages").description(
3547
+ const pages = new Command15("pages").description(
2897
3548
  'Manage Knowledge pages (PRDs, research, technical specs). Link one to a feature with "features link-page".'
2898
3549
  );
2899
3550
  pages.command("list").description("List pages in a workspace").option("--workspace <slug|id>", "Workspace (defaults to the configured default)").option("--search <text>", "Filter by title/content").action(
@@ -2971,13 +3622,14 @@ function createPagesCommand() {
2971
3622
  }
2972
3623
  }
2973
3624
  );
3625
+ pages.addCommand(createPageCommentsCommand());
2974
3626
  return pages;
2975
3627
  }
2976
3628
 
2977
3629
  // src/commands/products.ts
2978
- import { Command as Command14 } from "commander";
3630
+ import { Command as Command16 } from "commander";
2979
3631
  function createProductsCommand() {
2980
- const products = new Command14("products").description(
3632
+ const products = new Command16("products").description(
2981
3633
  "Manage products. A workspace contains products; a product holds features and tickets."
2982
3634
  );
2983
3635
  products.command("list").description("List products in a workspace").option("--workspace <slug|id>", "Workspace slug or CUID (defaults to configured workspace)").action(async (options, cmd) => {
@@ -3040,9 +3692,9 @@ function createProductsCommand() {
3040
3692
  }
3041
3693
 
3042
3694
  // src/commands/projects.ts
3043
- import { Command as Command15 } from "commander";
3695
+ import { Command as Command17 } from "commander";
3044
3696
  function createProjectsCommand() {
3045
- const projects = new Command15("projects").description("Manage projects");
3697
+ const projects = new Command17("projects").description("Manage projects");
3046
3698
  projects.command("list").description("List all projects").option("--workspace <id>", "Filter by workspace ID").option("--include-actions", "Include actions in output").action(async (options, cmd) => {
3047
3699
  const globalOpts = cmd.optsWithGlobals();
3048
3700
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
@@ -3065,7 +3717,7 @@ function createProjectsCommand() {
3065
3717
  }
3066
3718
 
3067
3719
  // src/commands/search.ts
3068
- import { Command as Command16 } from "commander";
3720
+ import { Command as Command18 } from "commander";
3069
3721
  import chalk6 from "chalk";
3070
3722
  var TYPE_LABELS = {
3071
3723
  workspace: "Workspaces",
@@ -3084,7 +3736,7 @@ var TYPE_LABELS = {
3084
3736
  product: "Products"
3085
3737
  };
3086
3738
  function createSearchCommand() {
3087
- const search = new Command16("search").description(
3739
+ const search = new Command18("search").description(
3088
3740
  "Global search across projects, actions, goals, workspaces and more \u2014 same coverage as the app's Cmd+K palette"
3089
3741
  ).argument("<query>", "Text to search for").option("-w, --workspace <slug>", "Restrict results to one workspace (slug or ID)").option("-l, --limit <n>", "Max results per entity type (1-25, default 10)").action(async (query, options, cmd) => {
3090
3742
  const globalOpts = cmd.optsWithGlobals();
@@ -3143,7 +3795,7 @@ ${TYPE_LABELS[type] ?? type}`));
3143
3795
  }
3144
3796
 
3145
3797
  // src/commands/tickets.ts
3146
- import { Command as Command17 } from "commander";
3798
+ import { Command as Command19 } from "commander";
3147
3799
  function collectRepeatable(value, previous) {
3148
3800
  return [...previous, value];
3149
3801
  }
@@ -3186,7 +3838,7 @@ function validateTicketType(value) {
3186
3838
  return value;
3187
3839
  }
3188
3840
  function createTicketsCommand() {
3189
- const tickets = new Command17("tickets").description(
3841
+ const tickets = new Command19("tickets").description(
3190
3842
  "Manage product backlog tickets. Tickets live under a product, link to features/epics, depend on each other, and can be implemented via actions."
3191
3843
  );
3192
3844
  tickets.command("list").description(
@@ -3500,25 +4152,78 @@ function createTicketsCommand() {
3500
4152
  handleError(error, useJson);
3501
4153
  }
3502
4154
  });
3503
- const comment = new Command17("comment").description("Manage comments on a ticket");
3504
- comment.command("add").description("Add a comment to a ticket").requiredOption("--id <id>", "Ticket CUID").requiredOption("-m, --message <text>", "Comment content (markdown supported)").action(async (options, cmd) => {
4155
+ const comment = new Command19("comment").description("Manage comments on a ticket");
4156
+ comment.command("list").description("List comments on a ticket").requiredOption("--id <id>", "Ticket CUID").action(async (options, cmd) => {
3505
4157
  const globalOpts = cmd.optsWithGlobals();
3506
4158
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
3507
4159
  try {
3508
4160
  const client = getClient();
3509
- const created = await client.tickets.addComment({
3510
- ticketId: options.id,
3511
- content: options.message
3512
- });
3513
- if (useJson) outputCommentJson(created);
3514
- else {
3515
- console.log("\n\u2713 Comment added");
3516
- outputCommentPretty(created);
3517
- }
4161
+ const ticket = await client.tickets.get(options.id);
4162
+ const comments = ticket.comments ?? [];
4163
+ if (useJson) outputCommentsJson(comments);
4164
+ else outputCommentsPretty(comments);
3518
4165
  } catch (error) {
3519
4166
  handleError(error, useJson);
3520
4167
  }
3521
4168
  });
4169
+ comment.command("add").description("Add a comment to a ticket").requiredOption("--id <id>", "Ticket CUID").requiredOption("-m, --message <text>", "Comment content (markdown supported)").option("--mention <name>", MENTION_OPTION_DESCRIPTION, collectMention, []).option(
4170
+ "--workspace <slug|id>",
4171
+ "Workspace used to resolve --mention (defaults to your default workspace)"
4172
+ ).action(
4173
+ async (options, cmd) => {
4174
+ const globalOpts = cmd.optsWithGlobals();
4175
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
4176
+ try {
4177
+ const client = getClient();
4178
+ const content = options.mention.length > 0 ? await applyMentions(
4179
+ client,
4180
+ await resolveWorkspaceId(client, options.workspace),
4181
+ options.message,
4182
+ options.mention
4183
+ ) : options.message;
4184
+ const created = await client.tickets.addComment({
4185
+ ticketId: options.id,
4186
+ content
4187
+ });
4188
+ if (useJson) outputCommentJson(created);
4189
+ else {
4190
+ console.log("\n\u2713 Comment added");
4191
+ outputCommentPretty(created);
4192
+ }
4193
+ } catch (error) {
4194
+ handleError(error, useJson);
4195
+ }
4196
+ }
4197
+ );
4198
+ comment.command("update").description("Update one of your own ticket comments").requiredOption("--comment-id <id>", "Comment ID").requiredOption("-m, --message <text>", "New comment content").option("--mention <name>", MENTION_OPTION_DESCRIPTION, collectMention, []).option(
4199
+ "--workspace <slug|id>",
4200
+ "Workspace used to resolve --mention (defaults to your default workspace)"
4201
+ ).action(
4202
+ async (options, cmd) => {
4203
+ const globalOpts = cmd.optsWithGlobals();
4204
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
4205
+ try {
4206
+ const client = getClient();
4207
+ const content = options.mention.length > 0 ? await applyMentions(
4208
+ client,
4209
+ await resolveWorkspaceId(client, options.workspace),
4210
+ options.message,
4211
+ options.mention
4212
+ ) : options.message;
4213
+ const updated = await client.tickets.updateComment({
4214
+ id: options.commentId,
4215
+ content
4216
+ });
4217
+ if (useJson) outputCommentJson(updated);
4218
+ else {
4219
+ console.log("\n\u2713 Comment updated");
4220
+ outputCommentPretty(updated);
4221
+ }
4222
+ } catch (error) {
4223
+ handleError(error, useJson);
4224
+ }
4225
+ }
4226
+ );
3522
4227
  comment.command("delete").description("Delete one of your own ticket comments").requiredOption("--comment-id <id>", "Comment ID").action(async (options, cmd) => {
3523
4228
  const globalOpts = cmd.optsWithGlobals();
3524
4229
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
@@ -3539,10 +4244,10 @@ function createTicketsCommand() {
3539
4244
  }
3540
4245
 
3541
4246
  // src/commands/workspaces.ts
3542
- import { Command as Command18 } from "commander";
4247
+ import { Command as Command20 } from "commander";
3543
4248
  import chalk7 from "chalk";
3544
4249
  function createWorkspacesCommand() {
3545
- const workspaces = new Command18("workspaces").description("Manage workspaces");
4250
+ const workspaces = new Command20("workspaces").description("Manage workspaces");
3546
4251
  workspaces.command("list").description("List all workspaces").action(async (_options, cmd) => {
3547
4252
  const globalOpts = cmd.optsWithGlobals();
3548
4253
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
@@ -3584,11 +4289,172 @@ function createWorkspacesCommand() {
3584
4289
  handleError(error);
3585
4290
  }
3586
4291
  });
4292
+ workspaces.command("members").description(
4293
+ "List workspace members. Each row carries the exact @[Name](id) markup to paste into a comment to mention that person."
4294
+ ).option("--workspace <slug|id>", "Workspace (defaults to your default workspace)").option("--search <text>", "Filter by name or email").action(
4295
+ async (options, cmd) => {
4296
+ const globalOpts = cmd.optsWithGlobals();
4297
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
4298
+ try {
4299
+ const client = getClient();
4300
+ const workspaceId = await resolveWorkspaceId(client, options.workspace);
4301
+ let members = await client.workspaces.listMembers(workspaceId);
4302
+ if (options.search) {
4303
+ const needle = options.search.toLowerCase();
4304
+ members = members.filter(
4305
+ (m) => m.name?.toLowerCase().includes(needle) || m.email?.toLowerCase().includes(needle)
4306
+ );
4307
+ }
4308
+ if (useJson) {
4309
+ console.log(
4310
+ JSON.stringify({ members, total: members.length }, null, 2)
4311
+ );
4312
+ return;
4313
+ }
4314
+ if (members.length === 0) {
4315
+ console.log(chalk7.gray("No members found."));
4316
+ return;
4317
+ }
4318
+ console.log(chalk7.bold(`
4319
+ Members (${members.length})
4320
+ `));
4321
+ for (const m of members) {
4322
+ const label = m.name ?? m.email ?? m.id;
4323
+ const via = m.source === "team" ? chalk7.gray(
4324
+ ` via team${m.teams.length ? `: ${m.teams.map((t) => t.name).join(", ")}` : ""}` + (m.teamRole ? ` [team ${m.teamRole}]` : "")
4325
+ ) : "";
4326
+ console.log(`${chalk7.bold(label)} ${chalk7.gray(`(${m.role})`)}${via}`);
4327
+ if (m.email) console.log(chalk7.gray(` ${m.email}`));
4328
+ console.log(chalk7.gray(` mention: ${m.mentionSyntax}`));
4329
+ console.log();
4330
+ }
4331
+ } catch (error) {
4332
+ handleError(error, useJson);
4333
+ }
4334
+ }
4335
+ );
3587
4336
  return workspaces;
3588
4337
  }
3589
4338
 
4339
+ // src/commands/goals.ts
4340
+ import { Command as Command21 } from "commander";
4341
+ function parseGoalId(value) {
4342
+ const id = Number(value);
4343
+ if (!Number.isInteger(id)) {
4344
+ throw new Error(
4345
+ `Goal id must be a whole number, got "${value}". Find one with \`exponential search "<goal title>"\`.`
4346
+ );
4347
+ }
4348
+ return id;
4349
+ }
4350
+ function createGoalsCommand() {
4351
+ const goals = new Command21("goals").description(
4352
+ 'Work with goals. Find goal ids with `exponential search "<title>"`.'
4353
+ );
4354
+ const comment = new Command21("comment").description(
4355
+ "Read and post comments on a goal. Mention teammates with --mention."
4356
+ );
4357
+ comment.command("list").description("List comments on a goal").requiredOption("--goal <id>", "Goal ID (a number)").action(async (options, cmd) => {
4358
+ const globalOpts = cmd.optsWithGlobals();
4359
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
4360
+ try {
4361
+ const client = getClient();
4362
+ const comments = await client.goalComments.list(parseGoalId(options.goal));
4363
+ if (useJson) outputCommentsJson(comments);
4364
+ else outputCommentsPretty(comments);
4365
+ } catch (error) {
4366
+ handleError(error, useJson);
4367
+ }
4368
+ });
4369
+ comment.command("add").description("Add a comment to a goal").requiredOption("--goal <id>", "Goal ID (a number)").requiredOption("-m, --message <text>", "Comment content (markdown supported)").option("--mention <name>", MENTION_OPTION_DESCRIPTION, collectMention, []).option(
4370
+ "--workspace <slug|id>",
4371
+ "Workspace used to resolve --mention (defaults to your default workspace)"
4372
+ ).option("--parent-update <id>", "Thread under a specific goal update").action(
4373
+ async (options, cmd) => {
4374
+ const globalOpts = cmd.optsWithGlobals();
4375
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
4376
+ try {
4377
+ const client = getClient();
4378
+ const goalId = parseGoalId(options.goal);
4379
+ const content = options.mention.length > 0 ? await applyMentions(
4380
+ client,
4381
+ await resolveWorkspaceId(client, options.workspace),
4382
+ options.message,
4383
+ options.mention
4384
+ ) : options.message;
4385
+ const created = await client.goalComments.add({
4386
+ goalId,
4387
+ content,
4388
+ parentUpdateId: options.parentUpdate
4389
+ });
4390
+ if (useJson) {
4391
+ outputCommentJson(created);
4392
+ } else {
4393
+ console.log("\n\u2713 Comment added");
4394
+ outputCommentPretty(created);
4395
+ }
4396
+ } catch (error) {
4397
+ handleError(error, useJson);
4398
+ }
4399
+ }
4400
+ );
4401
+ comment.command("update").description("Update one of your own comments").requiredOption("--comment-id <id>", "Comment ID").requiredOption("-m, --message <text>", "New comment content").option("--mention <name>", MENTION_OPTION_DESCRIPTION, collectMention, []).option(
4402
+ "--workspace <slug|id>",
4403
+ "Workspace used to resolve --mention (defaults to your default workspace)"
4404
+ ).action(
4405
+ async (options, cmd) => {
4406
+ const globalOpts = cmd.optsWithGlobals();
4407
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
4408
+ try {
4409
+ const client = getClient();
4410
+ const content = options.mention.length > 0 ? await applyMentions(
4411
+ client,
4412
+ await resolveWorkspaceId(client, options.workspace),
4413
+ options.message,
4414
+ options.mention
4415
+ ) : options.message;
4416
+ const updated = await client.goalComments.update({
4417
+ commentId: options.commentId,
4418
+ content
4419
+ });
4420
+ if (useJson) {
4421
+ outputCommentJson(updated);
4422
+ } else {
4423
+ console.log("\n\u2713 Comment updated");
4424
+ outputCommentPretty(updated);
4425
+ }
4426
+ } catch (error) {
4427
+ handleError(error, useJson);
4428
+ }
4429
+ }
4430
+ );
4431
+ comment.command("rm").description("Delete one of your own comments").requiredOption("--comment-id <id>", "Comment ID").action(async (options, cmd) => {
4432
+ const globalOpts = cmd.optsWithGlobals();
4433
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
4434
+ try {
4435
+ const client = getClient();
4436
+ await client.goalComments.delete(options.commentId);
4437
+ if (useJson) {
4438
+ console.log(JSON.stringify({ deleted: true, id: options.commentId }, null, 2));
4439
+ } else {
4440
+ console.log("\u2713 Comment deleted");
4441
+ }
4442
+ } catch (error) {
4443
+ handleError(error, useJson);
4444
+ }
4445
+ });
4446
+ goals.addCommand(comment);
4447
+ return goals;
4448
+ }
4449
+
3590
4450
  // src/index.ts
3591
- var program = new Command19();
4451
+ import { readFileSync as readFileSync2 } from "fs";
4452
+ import { dirname, join } from "path";
4453
+ import { fileURLToPath } from "url";
4454
+ var PKG_VERSION = JSON.parse(
4455
+ readFileSync2(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf-8")
4456
+ ).version;
4457
+ var program = new Command22();
3592
4458
  program.name("exponential").description(
3593
4459
  [
3594
4460
  "CLI to interact with Exponential productivity app.",
@@ -3597,7 +4463,7 @@ program.name("exponential").description(
3597
4463
  " workspace -> product -> feature -> ticket -> action",
3598
4464
  " epics are workspace-scoped and can group tickets and actions across products."
3599
4465
  ].join("\n")
3600
- ).version("1.0.0").option("--json", "Output as JSON (default when piped)").option("--pretty", "Force pretty-printed output");
4466
+ ).version(PKG_VERSION).option("--json", "Output as JSON (default when piped)").option("--pretty", "Force pretty-printed output");
3601
4467
  program.addCommand(createAuthCommand());
3602
4468
  program.addCommand(createActionsCommand());
3603
4469
  program.addCommand(createContactsCommand());
@@ -3612,5 +4478,6 @@ program.addCommand(createProjectsCommand());
3612
4478
  program.addCommand(createSearchCommand());
3613
4479
  program.addCommand(createTicketsCommand());
3614
4480
  program.addCommand(createWorkspacesCommand());
4481
+ program.addCommand(createGoalsCommand());
3615
4482
  program.parse();
3616
4483
  //# sourceMappingURL=index.js.map