exponential-cli 1.9.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";
@@ -752,30 +752,59 @@ function outputStagesJson(stages) {
752
752
  total: stages.length
753
753
  }, null, 2));
754
754
  }
755
- function transformComment(comment) {
756
- const parentKey = "actionId" in comment ? "actionId" : "ticketId";
757
- 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;
758
760
  return {
759
761
  id: comment.id,
760
- [parentKey]: parentId,
761
- authorId: comment.authorId,
762
- content: comment.content,
763
- author: comment.author,
764
- createdAt: new Date(comment.createdAt).toISOString(),
765
- 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()
766
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;
767
791
  }
768
792
  function outputCommentJson(comment) {
769
793
  console.log(JSON.stringify(transformComment(comment), null, 2));
770
794
  }
771
795
  function outputCommentPretty(comment) {
772
- const author = comment.author?.name ?? comment.authorId;
773
- 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(" ");
774
803
  console.log(chalk3.gray("\u2500".repeat(50)));
775
- console.log(`${chalk3.bold(author)} ${chalk3.gray(`\u2014 ${when}`)}`);
776
- 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}`));
777
806
  console.log();
778
- console.log(comment.content);
807
+ console.log(n.content);
779
808
  console.log();
780
809
  }
781
810
  function outputCommentsJson(comments) {
@@ -2235,7 +2264,7 @@ function createEpicsCommand() {
2235
2264
  }
2236
2265
 
2237
2266
  // src/commands/features.ts
2238
- import { Command as Command10 } from "commander";
2267
+ import { Command as Command11 } from "commander";
2239
2268
 
2240
2269
  // src/commands/stories.ts
2241
2270
  import { Command as Command6 } from "commander";
@@ -2656,6 +2685,240 @@ function createAreasCommand() {
2656
2685
  return areas;
2657
2686
  }
2658
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
+
2659
2922
  // src/commands/features.ts
2660
2923
  var FEATURE_STATUSES = [
2661
2924
  "IDEA",
@@ -2686,7 +2949,7 @@ function validateFeatureStatus(value) {
2686
2949
  return value;
2687
2950
  }
2688
2951
  function createFeaturesCommand() {
2689
- const features = new Command10("features").description(
2952
+ const features = new Command11("features").description(
2690
2953
  "Manage features. A feature lives under a product and groups user stories, scopes, and tickets."
2691
2954
  );
2692
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(
@@ -2934,11 +3197,12 @@ Aborted: ${reason}. Feature "${feature.name}" was NOT deleted.`);
2934
3197
  features.addCommand(createScopesCommand());
2935
3198
  features.addCommand(createRequirementsCommand());
2936
3199
  features.addCommand(createAreasCommand());
3200
+ features.addCommand(createFeatureCommentsCommand());
2937
3201
  return features;
2938
3202
  }
2939
3203
 
2940
3204
  // src/commands/labels.ts
2941
- import { Command as Command11 } from "commander";
3205
+ import { Command as Command12 } from "commander";
2942
3206
  import chalk5 from "chalk";
2943
3207
  var TRIAGE_LABELS = [
2944
3208
  { slug: "needs-triage", name: "needs-triage", color: "avatar-orange" },
@@ -2992,7 +3256,7 @@ async function resolveLabelIds(client, workspaceId, values) {
2992
3256
  return [...byId, ...resolvedFromSlugs];
2993
3257
  }
2994
3258
  function createLabelsCommand() {
2995
- const labels = new Command11("labels").description(
3259
+ const labels = new Command12("labels").description(
2996
3260
  "Manage labels (orthogonal tags applied to actions, tickets, features, and epics)."
2997
3261
  );
2998
3262
  labels.command("list").description("List labels visible in a workspace").option("--workspace <slug|id>", "Workspace slug or CUID").option(
@@ -3095,7 +3359,7 @@ function createLabelsCommand() {
3095
3359
  }
3096
3360
 
3097
3361
  // src/commands/organizations.ts
3098
- import { Command as Command12 } from "commander";
3362
+ import { Command as Command13 } from "commander";
3099
3363
  var VALID_SIZES = [
3100
3364
  "1-10",
3101
3365
  "11-50",
@@ -3105,7 +3369,7 @@ var VALID_SIZES = [
3105
3369
  "1000+"
3106
3370
  ];
3107
3371
  function createOrganizationsCommand() {
3108
- const organizations = new Command12("organizations").alias("orgs").description("Manage CRM organizations");
3372
+ const organizations = new Command13("organizations").alias("orgs").description("Manage CRM organizations");
3109
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) => {
3110
3374
  const globalOpts = cmd.optsWithGlobals();
3111
3375
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
@@ -3174,7 +3438,105 @@ function createOrganizationsCommand() {
3174
3438
 
3175
3439
  // src/commands/pages.ts
3176
3440
  import { readFileSync } from "fs";
3177
- 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
3178
3540
  function readBody(body, bodyFile) {
3179
3541
  if (body !== void 0) return body;
3180
3542
  if (bodyFile === void 0) return void 0;
@@ -3182,7 +3544,7 @@ function readBody(body, bodyFile) {
3182
3544
  return readFileSync(bodyFile, "utf-8");
3183
3545
  }
3184
3546
  function createPagesCommand() {
3185
- const pages = new Command13("pages").description(
3547
+ const pages = new Command15("pages").description(
3186
3548
  'Manage Knowledge pages (PRDs, research, technical specs). Link one to a feature with "features link-page".'
3187
3549
  );
3188
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(
@@ -3260,13 +3622,14 @@ function createPagesCommand() {
3260
3622
  }
3261
3623
  }
3262
3624
  );
3625
+ pages.addCommand(createPageCommentsCommand());
3263
3626
  return pages;
3264
3627
  }
3265
3628
 
3266
3629
  // src/commands/products.ts
3267
- import { Command as Command14 } from "commander";
3630
+ import { Command as Command16 } from "commander";
3268
3631
  function createProductsCommand() {
3269
- const products = new Command14("products").description(
3632
+ const products = new Command16("products").description(
3270
3633
  "Manage products. A workspace contains products; a product holds features and tickets."
3271
3634
  );
3272
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) => {
@@ -3329,9 +3692,9 @@ function createProductsCommand() {
3329
3692
  }
3330
3693
 
3331
3694
  // src/commands/projects.ts
3332
- import { Command as Command15 } from "commander";
3695
+ import { Command as Command17 } from "commander";
3333
3696
  function createProjectsCommand() {
3334
- const projects = new Command15("projects").description("Manage projects");
3697
+ const projects = new Command17("projects").description("Manage projects");
3335
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) => {
3336
3699
  const globalOpts = cmd.optsWithGlobals();
3337
3700
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
@@ -3354,7 +3717,7 @@ function createProjectsCommand() {
3354
3717
  }
3355
3718
 
3356
3719
  // src/commands/search.ts
3357
- import { Command as Command16 } from "commander";
3720
+ import { Command as Command18 } from "commander";
3358
3721
  import chalk6 from "chalk";
3359
3722
  var TYPE_LABELS = {
3360
3723
  workspace: "Workspaces",
@@ -3373,7 +3736,7 @@ var TYPE_LABELS = {
3373
3736
  product: "Products"
3374
3737
  };
3375
3738
  function createSearchCommand() {
3376
- const search = new Command16("search").description(
3739
+ const search = new Command18("search").description(
3377
3740
  "Global search across projects, actions, goals, workspaces and more \u2014 same coverage as the app's Cmd+K palette"
3378
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) => {
3379
3742
  const globalOpts = cmd.optsWithGlobals();
@@ -3432,7 +3795,7 @@ ${TYPE_LABELS[type] ?? type}`));
3432
3795
  }
3433
3796
 
3434
3797
  // src/commands/tickets.ts
3435
- import { Command as Command17 } from "commander";
3798
+ import { Command as Command19 } from "commander";
3436
3799
  function collectRepeatable(value, previous) {
3437
3800
  return [...previous, value];
3438
3801
  }
@@ -3475,7 +3838,7 @@ function validateTicketType(value) {
3475
3838
  return value;
3476
3839
  }
3477
3840
  function createTicketsCommand() {
3478
- const tickets = new Command17("tickets").description(
3841
+ const tickets = new Command19("tickets").description(
3479
3842
  "Manage product backlog tickets. Tickets live under a product, link to features/epics, depend on each other, and can be implemented via actions."
3480
3843
  );
3481
3844
  tickets.command("list").description(
@@ -3789,25 +4152,78 @@ function createTicketsCommand() {
3789
4152
  handleError(error, useJson);
3790
4153
  }
3791
4154
  });
3792
- const comment = new Command17("comment").description("Manage comments on a ticket");
3793
- 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) => {
3794
4157
  const globalOpts = cmd.optsWithGlobals();
3795
4158
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
3796
4159
  try {
3797
4160
  const client = getClient();
3798
- const created = await client.tickets.addComment({
3799
- ticketId: options.id,
3800
- content: options.message
3801
- });
3802
- if (useJson) outputCommentJson(created);
3803
- else {
3804
- console.log("\n\u2713 Comment added");
3805
- outputCommentPretty(created);
3806
- }
4161
+ const ticket = await client.tickets.get(options.id);
4162
+ const comments = ticket.comments ?? [];
4163
+ if (useJson) outputCommentsJson(comments);
4164
+ else outputCommentsPretty(comments);
3807
4165
  } catch (error) {
3808
4166
  handleError(error, useJson);
3809
4167
  }
3810
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
+ );
3811
4227
  comment.command("delete").description("Delete one of your own ticket comments").requiredOption("--comment-id <id>", "Comment ID").action(async (options, cmd) => {
3812
4228
  const globalOpts = cmd.optsWithGlobals();
3813
4229
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
@@ -3828,10 +4244,10 @@ function createTicketsCommand() {
3828
4244
  }
3829
4245
 
3830
4246
  // src/commands/workspaces.ts
3831
- import { Command as Command18 } from "commander";
4247
+ import { Command as Command20 } from "commander";
3832
4248
  import chalk7 from "chalk";
3833
4249
  function createWorkspacesCommand() {
3834
- const workspaces = new Command18("workspaces").description("Manage workspaces");
4250
+ const workspaces = new Command20("workspaces").description("Manage workspaces");
3835
4251
  workspaces.command("list").description("List all workspaces").action(async (_options, cmd) => {
3836
4252
  const globalOpts = cmd.optsWithGlobals();
3837
4253
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
@@ -3873,11 +4289,172 @@ function createWorkspacesCommand() {
3873
4289
  handleError(error);
3874
4290
  }
3875
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
+ );
3876
4336
  return workspaces;
3877
4337
  }
3878
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
+
3879
4450
  // src/index.ts
3880
- 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();
3881
4458
  program.name("exponential").description(
3882
4459
  [
3883
4460
  "CLI to interact with Exponential productivity app.",
@@ -3886,7 +4463,7 @@ program.name("exponential").description(
3886
4463
  " workspace -> product -> feature -> ticket -> action",
3887
4464
  " epics are workspace-scoped and can group tickets and actions across products."
3888
4465
  ].join("\n")
3889
- ).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");
3890
4467
  program.addCommand(createAuthCommand());
3891
4468
  program.addCommand(createActionsCommand());
3892
4469
  program.addCommand(createContactsCommand());
@@ -3901,5 +4478,6 @@ program.addCommand(createProjectsCommand());
3901
4478
  program.addCommand(createSearchCommand());
3902
4479
  program.addCommand(createTicketsCommand());
3903
4480
  program.addCommand(createWorkspacesCommand());
4481
+ program.addCommand(createGoalsCommand());
3904
4482
  program.parse();
3905
4483
  //# sourceMappingURL=index.js.map