exponential-cli 1.1.0 → 1.4.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 Command11 } from "commander";
4
+ import { Command as Command13 } from "commander";
5
5
 
6
6
  // src/commands/auth.ts
7
7
  import { Command } from "commander";
@@ -170,6 +170,29 @@ function createAuthCommand() {
170
170
  handleError(error);
171
171
  }
172
172
  });
173
+ auth.command("show").description(
174
+ "Print parts of the current credentials. Use --token to print the raw JWT (for piping into other tools, e.g. `gh secret set EXPONENTIAL_TOKEN < <(exponential auth show --token)`)."
175
+ ).option("--token", "Print the raw JWT to stdout with no trailing newline").action((options) => {
176
+ const config = getConfig();
177
+ if (options.token) {
178
+ if (!config.token) {
179
+ console.error("Not authenticated. Run `exponential auth login` first.");
180
+ process.exit(1);
181
+ }
182
+ process.stdout.write(config.token);
183
+ return;
184
+ }
185
+ console.log(chalk2.bold("\nCredentials"));
186
+ console.log(chalk2.gray("\u2500".repeat(40)));
187
+ console.log(`API URL: ${config.apiUrl || chalk2.gray("(not set)")}`);
188
+ console.log(
189
+ `Token: ${config.token ? chalk2.gray(`${config.token.substring(0, 20)}...`) : chalk2.gray("(not set)")}`
190
+ );
191
+ console.log(
192
+ chalk2.gray("Use `exponential auth show --token` to print the raw JWT.")
193
+ );
194
+ console.log();
195
+ });
173
196
  auth.command("status").description("Show detailed authentication status").action(() => {
174
197
  const config = getConfig();
175
198
  console.log(chalk2.bold("\nAuthentication Status"));
@@ -768,6 +791,87 @@ function getFeatureStatusColor(status) {
768
791
  return "gray";
769
792
  }
770
793
  }
794
+ function transformUserStory(story) {
795
+ return {
796
+ id: story.id,
797
+ featureId: story.featureId,
798
+ scopeId: story.scopeId,
799
+ asA: story.asA,
800
+ iWant: story.iWant,
801
+ soThat: story.soThat,
802
+ acceptanceCriteria: story.acceptanceCriteria,
803
+ displayOrder: story.displayOrder,
804
+ createdAt: new Date(story.createdAt).toISOString(),
805
+ updatedAt: new Date(story.updatedAt).toISOString()
806
+ };
807
+ }
808
+ function outputUserStoryJson(story) {
809
+ console.log(JSON.stringify(transformUserStory(story), null, 2));
810
+ }
811
+ function outputUserStoryPretty(story) {
812
+ console.log(chalk3.gray("\u2500".repeat(50)));
813
+ const order = chalk3.gray(`#${story.displayOrder}`);
814
+ console.log(`
815
+ ${order} ${chalk3.bold(story.iWant ?? '(no "I want")')}`);
816
+ console.log(chalk3.gray(` ID: ${story.id}`));
817
+ if (story.asA) console.log(` ${chalk3.cyan("As a:")} ${story.asA}`);
818
+ if (story.iWant) console.log(` ${chalk3.cyan("I want:")} ${story.iWant}`);
819
+ if (story.soThat) console.log(` ${chalk3.cyan("So that:")} ${story.soThat}`);
820
+ if (story.acceptanceCriteria) {
821
+ console.log(` ${chalk3.yellow("Acceptance:")} ${story.acceptanceCriteria}`);
822
+ }
823
+ if (story.scopeId) console.log(` ${chalk3.magenta("Scope:")} ${story.scopeId}`);
824
+ console.log();
825
+ }
826
+ function outputUserStoriesJson(stories) {
827
+ console.log(JSON.stringify({
828
+ userStories: stories.map(transformUserStory),
829
+ total: stories.length
830
+ }, null, 2));
831
+ }
832
+ function outputUserStoriesPretty(stories) {
833
+ if (stories.length === 0) {
834
+ console.log(chalk3.gray("No user stories found."));
835
+ return;
836
+ }
837
+ console.log(chalk3.bold(`
838
+ User stories (${stories.length} total)`));
839
+ console.log(chalk3.gray("\u2500".repeat(50)));
840
+ for (const story of stories) {
841
+ const order = chalk3.gray(`#${story.displayOrder}`);
842
+ const parts = [
843
+ story.asA ? `As a ${story.asA},` : null,
844
+ story.iWant ? `I want ${story.iWant},` : null,
845
+ story.soThat ? `so that ${story.soThat}.` : null
846
+ ].filter(Boolean).join(" ");
847
+ console.log(` ${order} ${parts || chalk3.gray("(empty)")}`);
848
+ console.log(chalk3.gray(` ID: ${story.id}`));
849
+ }
850
+ console.log();
851
+ }
852
+ function outputBatchResultsJson(results) {
853
+ const succeeded = results.filter((r) => r.ok).length;
854
+ console.log(JSON.stringify({
855
+ results,
856
+ total: results.length,
857
+ succeeded,
858
+ failed: results.length - succeeded
859
+ }, null, 2));
860
+ }
861
+ function outputBatchResultsPretty(results) {
862
+ const succeeded = results.filter((r) => r.ok).length;
863
+ console.log(chalk3.bold(`
864
+ Added ${succeeded}/${results.length} user stories`));
865
+ console.log(chalk3.gray("\u2500".repeat(50)));
866
+ for (const r of results) {
867
+ if (r.ok) {
868
+ console.log(` ${chalk3.green("\u2713")} [${r.index}] ${chalk3.gray(r.id ?? "")}`);
869
+ } else {
870
+ console.log(` ${chalk3.red("\u2717")} [${r.index}] ${r.error ?? "failed"}`);
871
+ }
872
+ }
873
+ console.log();
874
+ }
771
875
  function outputTicketJson(ticket) {
772
876
  console.log(JSON.stringify(transformTicket(ticket), null, 2));
773
877
  }
@@ -1767,7 +1871,116 @@ function createEpicsCommand() {
1767
1871
  }
1768
1872
 
1769
1873
  // src/commands/features.ts
1874
+ import { Command as Command7 } from "commander";
1875
+
1876
+ // src/commands/stories.ts
1770
1877
  import { Command as Command6 } from "commander";
1878
+ async function readStdin() {
1879
+ const chunks = [];
1880
+ for await (const chunk of process.stdin) {
1881
+ chunks.push(chunk);
1882
+ }
1883
+ return Buffer.concat(chunks).toString("utf-8");
1884
+ }
1885
+ function createStoriesCommand() {
1886
+ const stories = new Command6("stories").description(
1887
+ "Manage a feature's native, structured user stories (As a / I want / So that)."
1888
+ );
1889
+ stories.command("add").description(
1890
+ [
1891
+ "Add user stories to a feature.",
1892
+ "",
1893
+ "Flag mode: pass --as-a/--i-want/--so-that (plus optional --acceptance/--scope) to add one story.",
1894
+ "Batch mode: pipe a JSON array of { asA?, iWant?, soThat?, acceptanceCriteria?, scopeId? } on stdin",
1895
+ " to add many at once (per-item success/failure is reported)."
1896
+ ].join("\n")
1897
+ ).requiredOption("--feature <id>", "Feature CUID").option("--as-a <text>", '"As a ..." actor').option("--i-want <text>", '"I want ..." capability').option("--so-that <text>", '"So that ..." outcome').option("--acceptance <text>", "Acceptance criteria").option("--scope <id>", "Feature scope CUID to group the story under").action(
1898
+ async (options, cmd) => {
1899
+ const globalOpts = cmd.optsWithGlobals();
1900
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
1901
+ const hasFlags = options.asA != null || options.iWant != null || options.soThat != null || options.acceptance != null;
1902
+ const isBatch = !hasFlags && !process.stdin.isTTY;
1903
+ try {
1904
+ const client = getClient();
1905
+ if (isBatch) {
1906
+ const raw = await readStdin();
1907
+ let items;
1908
+ try {
1909
+ const parsed = JSON.parse(raw);
1910
+ if (!Array.isArray(parsed)) {
1911
+ throw new Error("Expected a JSON array of story objects on stdin");
1912
+ }
1913
+ items = parsed;
1914
+ } catch (parseError) {
1915
+ throw new Error(
1916
+ `Could not parse stdin as a JSON array: ${parseError instanceof Error ? parseError.message : String(parseError)}`
1917
+ );
1918
+ }
1919
+ const results = [];
1920
+ for (let index = 0; index < items.length; index++) {
1921
+ const item = items[index];
1922
+ try {
1923
+ const story2 = await client.userStories.create({
1924
+ featureId: options.feature,
1925
+ asA: item.asA,
1926
+ iWant: item.iWant,
1927
+ soThat: item.soThat,
1928
+ acceptanceCriteria: item.acceptanceCriteria,
1929
+ scopeId: item.scopeId
1930
+ });
1931
+ results.push({ index, ok: true, id: story2.id });
1932
+ } catch (itemError) {
1933
+ results.push({
1934
+ index,
1935
+ ok: false,
1936
+ error: itemError instanceof Error ? itemError.message : String(itemError)
1937
+ });
1938
+ }
1939
+ }
1940
+ if (useJson) outputBatchResultsJson(results);
1941
+ else outputBatchResultsPretty(results);
1942
+ if (results.length > 0 && results.every((r) => !r.ok)) {
1943
+ process.exit(1);
1944
+ }
1945
+ return;
1946
+ }
1947
+ const input = {
1948
+ featureId: options.feature,
1949
+ asA: options.asA,
1950
+ iWant: options.iWant,
1951
+ soThat: options.soThat,
1952
+ acceptanceCriteria: options.acceptance,
1953
+ scopeId: options.scope
1954
+ };
1955
+ const story = await client.userStories.create(input);
1956
+ if (useJson) outputUserStoryJson(story);
1957
+ else {
1958
+ console.log("\n\u2713 User story added");
1959
+ outputUserStoryPretty(story);
1960
+ }
1961
+ } catch (error) {
1962
+ handleError(error, useJson);
1963
+ }
1964
+ }
1965
+ );
1966
+ stories.command("list").description("List a feature's user stories in display order").requiredOption("--feature <id>", "Feature CUID").action(
1967
+ async (options, cmd) => {
1968
+ const globalOpts = cmd.optsWithGlobals();
1969
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
1970
+ try {
1971
+ const client = getClient();
1972
+ const list = await client.userStories.list({ featureId: options.feature });
1973
+ if (useJson) outputUserStoriesJson(list);
1974
+ else outputUserStoriesPretty(list);
1975
+ } catch (error) {
1976
+ handleError(error, useJson);
1977
+ }
1978
+ }
1979
+ );
1980
+ return stories;
1981
+ }
1982
+
1983
+ // src/commands/features.ts
1771
1984
  var FEATURE_STATUSES = [
1772
1985
  "IDEA",
1773
1986
  "DEFINED",
@@ -1785,7 +1998,7 @@ function validateFeatureStatus(value) {
1785
1998
  return value;
1786
1999
  }
1787
2000
  function createFeaturesCommand() {
1788
- const features = new Command6("features").description(
2001
+ const features = new Command7("features").description(
1789
2002
  "Manage features. A feature lives under a product and groups user stories, scopes, and tickets."
1790
2003
  );
1791
2004
  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(
@@ -1879,13 +2092,171 @@ function createFeaturesCommand() {
1879
2092
  }
1880
2093
  }
1881
2094
  );
2095
+ features.addCommand(createStoriesCommand());
1882
2096
  return features;
1883
2097
  }
1884
2098
 
2099
+ // src/commands/labels.ts
2100
+ import { Command as Command8 } from "commander";
2101
+ import chalk4 from "chalk";
2102
+ var TRIAGE_LABELS = [
2103
+ { slug: "needs-triage", name: "needs-triage", color: "avatar-orange" },
2104
+ { slug: "needs-info", name: "needs-info", color: "avatar-yellow" },
2105
+ { slug: "ready-for-agent", name: "ready-for-agent", color: "avatar-green" },
2106
+ { slug: "ready-for-human", name: "ready-for-human", color: "avatar-lightBlue" },
2107
+ { slug: "wontfix", name: "wontfix", color: "avatar-lightGray" }
2108
+ ];
2109
+ var CUID_PATTERN = /^c[a-z0-9]{20,}$/i;
2110
+ function looksLikeCuid(value) {
2111
+ return CUID_PATTERN.test(value);
2112
+ }
2113
+ function outputTagsPretty(tags) {
2114
+ if (tags.length === 0) {
2115
+ console.log(chalk4.dim("No labels found."));
2116
+ return;
2117
+ }
2118
+ console.log();
2119
+ for (const tag of tags) {
2120
+ const scope = tag.workspaceId === null ? chalk4.cyan("global") : chalk4.magenta("workspace");
2121
+ const category = tag.category === null ? chalk4.dim("(label)") : chalk4.dim(`(${tag.category})`);
2122
+ console.log(
2123
+ ` ${chalk4.bold(tag.slug)} ${tag.name} ${chalk4.gray(tag.color)} ${scope} ${category}`
2124
+ );
2125
+ }
2126
+ console.log();
2127
+ }
2128
+ function outputTagPretty(tag, prefix) {
2129
+ if (prefix) console.log(prefix);
2130
+ console.log(` ${chalk4.bold(tag.slug)} ${tag.name} ${chalk4.gray(tag.color)}`);
2131
+ }
2132
+ async function resolveLabelIds(client, workspaceId, values) {
2133
+ if (values.length === 0) return [];
2134
+ const bySlug = values.filter((v) => !looksLikeCuid(v));
2135
+ const byId = values.filter((v) => looksLikeCuid(v));
2136
+ if (bySlug.length === 0) return byId;
2137
+ const list = await client.labels.list({ workspaceId });
2138
+ const slugMap = new Map(list.allTags.map((t) => [t.slug, t.id]));
2139
+ const resolvedFromSlugs = [];
2140
+ const missing = [];
2141
+ for (const slug of bySlug) {
2142
+ const id = slugMap.get(slug);
2143
+ if (id === void 0) missing.push(slug);
2144
+ else resolvedFromSlugs.push(id);
2145
+ }
2146
+ if (missing.length > 0) {
2147
+ throw new Error(
2148
+ `Unknown label slug(s): ${missing.join(", ")}. Run \`exponential labels list --workspace <slug>\` to see available labels.`
2149
+ );
2150
+ }
2151
+ return [...byId, ...resolvedFromSlugs];
2152
+ }
2153
+ function createLabelsCommand() {
2154
+ const labels = new Command8("labels").description(
2155
+ "Manage labels (orthogonal tags applied to actions, tickets, features, and epics)."
2156
+ );
2157
+ labels.command("list").description("List labels visible in a workspace").option("--workspace <slug|id>", "Workspace slug or CUID").option(
2158
+ "--category <category>",
2159
+ 'Filter by category. Use "label" for general labels, "null" for category-less labels, or omit to list all.'
2160
+ ).action(
2161
+ async (options, cmd) => {
2162
+ const globalOpts = cmd.optsWithGlobals();
2163
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
2164
+ try {
2165
+ const client = getClient();
2166
+ const workspaceId = await resolveWorkspaceId(client, options.workspace);
2167
+ const category = options.category === void 0 ? void 0 : options.category === "null" ? null : options.category;
2168
+ const result = await client.labels.list({ workspaceId, category });
2169
+ if (useJson) {
2170
+ console.log(JSON.stringify(result.allTags, null, 2));
2171
+ } else {
2172
+ outputTagsPretty(result.allTags);
2173
+ }
2174
+ } catch (error) {
2175
+ handleError(error, useJson);
2176
+ }
2177
+ }
2178
+ );
2179
+ labels.command("create").description("Create a workspace-scoped label").requiredOption("-n, --name <name>", "Label name").requiredOption("--color <token>", "Color token (e.g. avatar-green, brand-primary)").option("-d, --description <text>", "Description").option("--category <category>", 'Category. Omit (or pass "null") to create a general label.').option("--workspace <slug|id>", "Workspace slug or CUID").action(
2180
+ async (options, cmd) => {
2181
+ const globalOpts = cmd.optsWithGlobals();
2182
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
2183
+ try {
2184
+ const client = getClient();
2185
+ const workspaceId = await resolveWorkspaceId(client, options.workspace);
2186
+ if (workspaceId === void 0) {
2187
+ throw new Error(
2188
+ "Workspace is required. Pass --workspace <slug|id> or set a default with `exponential workspaces set-default`."
2189
+ );
2190
+ }
2191
+ const category = options.category === void 0 ? void 0 : options.category === "null" ? null : options.category;
2192
+ const tag = await client.labels.create({
2193
+ workspaceId,
2194
+ name: options.name,
2195
+ color: options.color,
2196
+ description: options.description,
2197
+ category
2198
+ });
2199
+ if (useJson) {
2200
+ console.log(JSON.stringify(tag, null, 2));
2201
+ } else {
2202
+ outputTagPretty(tag, chalk4.green("\n\u2713 Label created"));
2203
+ }
2204
+ } catch (error) {
2205
+ handleError(error, useJson);
2206
+ }
2207
+ }
2208
+ );
2209
+ labels.command("seed-triage").description("Idempotently seed the five canonical triage labels into a workspace.").option("--workspace <slug|id>", "Workspace slug or CUID").action(
2210
+ async (options, cmd) => {
2211
+ const globalOpts = cmd.optsWithGlobals();
2212
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
2213
+ try {
2214
+ const client = getClient();
2215
+ const workspaceId = await resolveWorkspaceId(client, options.workspace);
2216
+ if (workspaceId === void 0) {
2217
+ throw new Error(
2218
+ "Workspace is required. Pass --workspace <slug|id> or set a default with `exponential workspaces set-default`."
2219
+ );
2220
+ }
2221
+ const existing = await client.labels.list({ workspaceId });
2222
+ const existingSlugs = new Set(existing.allTags.map((t) => t.slug));
2223
+ const results = [];
2224
+ for (const def of TRIAGE_LABELS) {
2225
+ if (existingSlugs.has(def.slug)) {
2226
+ results.push({ slug: def.slug, status: "existed" });
2227
+ continue;
2228
+ }
2229
+ const tag = await client.labels.create({
2230
+ workspaceId,
2231
+ name: def.name,
2232
+ color: def.color
2233
+ });
2234
+ results.push({ slug: def.slug, status: "created", tag });
2235
+ }
2236
+ if (useJson) {
2237
+ console.log(JSON.stringify(results, null, 2));
2238
+ } else {
2239
+ console.log();
2240
+ console.log(chalk4.bold("Triage labels:"));
2241
+ for (const r of results) {
2242
+ const tag = chalk4.bold(r.slug);
2243
+ const status = r.status === "created" ? chalk4.green("created") : chalk4.dim("existed");
2244
+ console.log(` ${tag} ${status}`);
2245
+ }
2246
+ console.log();
2247
+ }
2248
+ } catch (error) {
2249
+ handleError(error, useJson);
2250
+ }
2251
+ }
2252
+ );
2253
+ return labels;
2254
+ }
2255
+
1885
2256
  // src/commands/products.ts
1886
- import { Command as Command7 } from "commander";
2257
+ import { Command as Command9 } from "commander";
1887
2258
  function createProductsCommand() {
1888
- const products = new Command7("products").description(
2259
+ const products = new Command9("products").description(
1889
2260
  "Manage products. A workspace contains products; a product holds features and tickets."
1890
2261
  );
1891
2262
  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) => {
@@ -1948,9 +2319,9 @@ function createProductsCommand() {
1948
2319
  }
1949
2320
 
1950
2321
  // src/commands/projects.ts
1951
- import { Command as Command8 } from "commander";
2322
+ import { Command as Command10 } from "commander";
1952
2323
  function createProjectsCommand() {
1953
- const projects = new Command8("projects").description("Manage projects");
2324
+ const projects = new Command10("projects").description("Manage projects");
1954
2325
  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) => {
1955
2326
  const globalOpts = cmd.optsWithGlobals();
1956
2327
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
@@ -1973,7 +2344,10 @@ function createProjectsCommand() {
1973
2344
  }
1974
2345
 
1975
2346
  // src/commands/tickets.ts
1976
- import { Command as Command9 } from "commander";
2347
+ import { Command as Command11 } from "commander";
2348
+ function collectRepeatable(value, previous) {
2349
+ return [...previous, value];
2350
+ }
1977
2351
  var TICKET_STATUSES = [
1978
2352
  "BACKLOG",
1979
2353
  "NEEDS_REFINEMENT",
@@ -2013,33 +2387,80 @@ function validateTicketType(value) {
2013
2387
  return value;
2014
2388
  }
2015
2389
  function createTicketsCommand() {
2016
- const tickets = new Command9("tickets").description(
2390
+ const tickets = new Command11("tickets").description(
2017
2391
  "Manage product backlog tickets. Tickets live under a product, link to features/epics, depend on each other, and can be implemented via actions."
2018
2392
  );
2019
- tickets.command("list").description("List tickets 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: ${TICKET_STATUSES.join(", ")}`).option("--type <type>", `Filter by type: ${TICKET_TYPES.join(", ")}`).option("--feature <id>", "Filter by feature CUID").option("--epic <id>", "Filter by epic CUID").option("--assignee <id>", "Filter by assignee user ID").action(
2393
+ tickets.command("list").description(
2394
+ "List tickets in a product, or workspace-wide when --pr or --branch is supplied"
2395
+ ).option(
2396
+ "--product <slug|id>",
2397
+ "Product slug or CUID (required unless --pr or --branch is supplied)"
2398
+ ).option("--workspace <slug|id>", "Workspace (required when --product is a slug)").option("--status <status>", `Filter by status: ${TICKET_STATUSES.join(", ")}`).option("--type <type>", `Filter by type: ${TICKET_TYPES.join(", ")}`).option("--feature <id>", "Filter by feature CUID").option("--epic <id>", "Filter by epic CUID").option("--assignee <id>", "Filter by assignee user ID").option(
2399
+ "--pr <url>",
2400
+ "Workspace-wide lookup by Ticket.prUrl (exact match). Makes --product optional."
2401
+ ).option(
2402
+ "--branch <name>",
2403
+ "Workspace-wide lookup by Ticket.branchName (exact match). Makes --product optional."
2404
+ ).option(
2405
+ "--label <slug-or-id>",
2406
+ "Filter to tickets carrying this label (repeatable; AND semantics)",
2407
+ collectRepeatable,
2408
+ []
2409
+ ).action(
2020
2410
  async (options, cmd) => {
2021
2411
  const globalOpts = cmd.optsWithGlobals();
2022
2412
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
2023
2413
  try {
2024
2414
  const status = validateTicketStatus(options.status);
2025
2415
  const type = validateTicketType(options.type);
2416
+ const hasWorkspaceFilter = Boolean(options.pr || options.branch);
2417
+ if (!hasWorkspaceFilter && !options.product) {
2418
+ throw new Error(
2419
+ "--product is required unless --pr or --branch is supplied"
2420
+ );
2421
+ }
2026
2422
  const client = getClient();
2027
- const workspaceId = await resolveWorkspaceId(client, options.workspace);
2028
- const productId = await resolveProductId(
2029
- client,
2030
- workspaceId,
2031
- options.product
2032
- );
2423
+ let workspaceId;
2424
+ let productId;
2425
+ if (options.product) {
2426
+ workspaceId = await resolveWorkspaceId(client, options.workspace);
2427
+ productId = await resolveProductId(
2428
+ client,
2429
+ workspaceId,
2430
+ options.product
2431
+ );
2432
+ }
2033
2433
  const list = await client.tickets.list({
2034
2434
  productId,
2035
2435
  status,
2036
2436
  type,
2037
2437
  featureId: options.feature,
2038
2438
  epicId: options.epic,
2039
- assigneeId: options.assignee
2439
+ assigneeId: options.assignee,
2440
+ prUrl: options.pr,
2441
+ branchName: options.branch
2040
2442
  });
2041
- if (useJson) outputTicketsJson(list);
2042
- else outputTicketsPretty(list);
2443
+ let filtered = list;
2444
+ if (options.label.length > 0) {
2445
+ const labelIds = new Set(
2446
+ await resolveLabelIds(client, workspaceId, options.label)
2447
+ );
2448
+ const ticketLabelSets = await Promise.all(
2449
+ list.map(
2450
+ (t) => client.labels.listForEntity({ entityType: "ticket", entityId: t.id }).then((tags) => new Set(tags.map((tag) => tag.id)))
2451
+ )
2452
+ );
2453
+ filtered = list.filter((_t, idx) => {
2454
+ const tagSet = ticketLabelSets[idx];
2455
+ if (tagSet === void 0) return false;
2456
+ for (const id of labelIds) {
2457
+ if (!tagSet.has(id)) return false;
2458
+ }
2459
+ return true;
2460
+ });
2461
+ }
2462
+ if (useJson) outputTicketsJson(filtered);
2463
+ else outputTicketsPretty(filtered);
2043
2464
  } catch (error) {
2044
2465
  handleError(error, useJson);
2045
2466
  }
@@ -2069,7 +2490,12 @@ function createTicketsCommand() {
2069
2490
  handleError(error, useJson);
2070
2491
  }
2071
2492
  });
2072
- tickets.command("create").description("Create a new ticket in a product").requiredOption("--product <slug|id>", "Product slug or CUID").requiredOption("-t, --title <title>", "Ticket title").option("-b, --body <text>", "Body (markdown supported)").option("--type <type>", `Ticket type: ${TICKET_TYPES.join(", ")}`).option("--status <status>", `Initial status: ${TICKET_STATUSES.join(", ")}`).option("--priority <n>", "Priority 0-4 (lower = higher)", parseInt).option("--points <n>", "Story points", parseFloat).option("--feature <id>", "Feature CUID to attach to").option("--epic <id>", "Epic CUID to attach to").option("--assignee <id>", "Assignee user ID").option("--branch <name>", "Branch name").option("--pr <url>", "Pull request URL").option("--workspace <slug|id>", "Workspace (required when --product is a slug)").action(
2493
+ tickets.command("create").description("Create a new ticket in a product").requiredOption("--product <slug|id>", "Product slug or CUID").requiredOption("-t, --title <title>", "Ticket title").option("-b, --body <text>", "Body (markdown supported)").option("--type <type>", `Ticket type: ${TICKET_TYPES.join(", ")}`).option("--status <status>", `Initial status: ${TICKET_STATUSES.join(", ")}`).option("--priority <n>", "Priority 0-4 (lower = higher)", parseInt).option("--points <n>", "Story points", parseFloat).option("--feature <id>", "Feature CUID to attach to").option("--epic <id>", "Epic CUID to attach to").option("--assignee <id>", "Assignee user ID").option("--branch <name>", "Branch name").option("--pr <url>", "Pull request URL").option("--workspace <slug|id>", "Workspace (required when --product is a slug)").option(
2494
+ "--label <slug-or-id>",
2495
+ "Label to attach (repeatable)",
2496
+ collectRepeatable,
2497
+ []
2498
+ ).action(
2073
2499
  async (options, cmd) => {
2074
2500
  const globalOpts = cmd.optsWithGlobals();
2075
2501
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
@@ -2097,6 +2523,14 @@ function createTicketsCommand() {
2097
2523
  branchName: options.branch,
2098
2524
  prUrl: options.pr
2099
2525
  });
2526
+ if (options.label.length > 0) {
2527
+ const tagIds = await resolveLabelIds(client, workspaceId, options.label);
2528
+ await client.labels.setEntityTags({
2529
+ entityType: "ticket",
2530
+ entityId: ticket.id,
2531
+ tagIds
2532
+ });
2533
+ }
2100
2534
  if (useJson) outputTicketJson(ticket);
2101
2535
  else {
2102
2536
  console.log("\n\u2713 Ticket created");
@@ -2107,7 +2541,17 @@ function createTicketsCommand() {
2107
2541
  }
2108
2542
  }
2109
2543
  );
2110
- tickets.command("update").description("Update an existing ticket").requiredOption("--id <id>", "Ticket CUID").option("-t, --title <title>", "New title").option("-b, --body <text>", "New body").option("--type <type>", `Type: ${TICKET_TYPES.join(", ")}`).option("--status <status>", `Status: ${TICKET_STATUSES.join(", ")}`).option("--priority <n>", 'Priority 0-4 (or "null" to clear)').option("--points <n>", 'Story points (or "null" to clear)').option("--feature <id>", 'Feature CUID (or "null" to detach)').option("--epic <id>", 'Epic CUID (or "null" to detach)').option("--assignee <id>", 'Assignee user ID (or "null" to unassign)').option("--branch <name>", 'Branch name (or "null")').option("--pr <url>", 'PR URL (or "null")').action(
2544
+ tickets.command("update").description("Update an existing ticket").requiredOption("--id <id>", "Ticket CUID").option("-t, --title <title>", "New title").option("-b, --body <text>", "New body").option("--type <type>", `Type: ${TICKET_TYPES.join(", ")}`).option("--status <status>", `Status: ${TICKET_STATUSES.join(", ")}`).option("--priority <n>", 'Priority 0-4 (or "null" to clear)').option("--points <n>", 'Story points (or "null" to clear)').option("--feature <id>", 'Feature CUID (or "null" to detach)').option("--epic <id>", 'Epic CUID (or "null" to detach)').option("--assignee <id>", 'Assignee user ID (or "null" to unassign)').option("--branch <name>", 'Branch name (or "null")').option("--pr <url>", 'PR URL (or "null")').option(
2545
+ "--add-label <slug-or-id>",
2546
+ "Label to attach to this ticket (repeatable)",
2547
+ collectRepeatable,
2548
+ []
2549
+ ).option(
2550
+ "--remove-label <slug-or-id>",
2551
+ "Label to detach from this ticket (repeatable)",
2552
+ collectRepeatable,
2553
+ []
2554
+ ).action(
2111
2555
  async (options, cmd) => {
2112
2556
  const globalOpts = cmd.optsWithGlobals();
2113
2557
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
@@ -2148,6 +2592,22 @@ function createTicketsCommand() {
2148
2592
  branchName: parseNullableString(options.branch),
2149
2593
  prUrl: parseNullableString(options.pr)
2150
2594
  });
2595
+ if (options.addLabel.length > 0 || options.removeLabel.length > 0) {
2596
+ const current = await client.labels.listForEntity({
2597
+ entityType: "ticket",
2598
+ entityId: options.id
2599
+ });
2600
+ const currentIds = new Set(current.map((t) => t.id));
2601
+ const addIds = await resolveLabelIds(client, void 0, options.addLabel);
2602
+ const removeIds = await resolveLabelIds(client, void 0, options.removeLabel);
2603
+ for (const id of addIds) currentIds.add(id);
2604
+ for (const id of removeIds) currentIds.delete(id);
2605
+ await client.labels.setEntityTags({
2606
+ entityType: "ticket",
2607
+ entityId: options.id,
2608
+ tagIds: [...currentIds]
2609
+ });
2610
+ }
2151
2611
  if (useJson) outputTicketJson(ticket);
2152
2612
  else {
2153
2613
  console.log("\n\u2713 Ticket updated");
@@ -2240,7 +2700,7 @@ function createTicketsCommand() {
2240
2700
  handleError(error, useJson);
2241
2701
  }
2242
2702
  });
2243
- const comment = new Command9("comment").description("Manage comments on a ticket");
2703
+ const comment = new Command11("comment").description("Manage comments on a ticket");
2244
2704
  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) => {
2245
2705
  const globalOpts = cmd.optsWithGlobals();
2246
2706
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
@@ -2279,10 +2739,10 @@ function createTicketsCommand() {
2279
2739
  }
2280
2740
 
2281
2741
  // src/commands/workspaces.ts
2282
- import { Command as Command10 } from "commander";
2283
- import chalk4 from "chalk";
2742
+ import { Command as Command12 } from "commander";
2743
+ import chalk5 from "chalk";
2284
2744
  function createWorkspacesCommand() {
2285
- const workspaces = new Command10("workspaces").description("Manage workspaces");
2745
+ const workspaces = new Command12("workspaces").description("Manage workspaces");
2286
2746
  workspaces.command("list").description("List all workspaces").action(async (_options, cmd) => {
2287
2747
  const globalOpts = cmd.optsWithGlobals();
2288
2748
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
@@ -2295,7 +2755,7 @@ function createWorkspacesCommand() {
2295
2755
  const config = getConfig();
2296
2756
  outputWorkspacesPretty(workspaces2);
2297
2757
  if (config.defaultWorkspaceSlug) {
2298
- console.log(chalk4.gray(`Default workspace: ${config.defaultWorkspaceSlug}`));
2758
+ console.log(chalk5.gray(`Default workspace: ${config.defaultWorkspaceSlug}`));
2299
2759
  }
2300
2760
  }
2301
2761
  } catch (error) {
@@ -2308,10 +2768,10 @@ function createWorkspacesCommand() {
2308
2768
  const workspaces2 = await client.workspaces.list();
2309
2769
  const workspace = workspaces2.find((w) => w.slug === slug);
2310
2770
  if (!workspace) {
2311
- console.error(chalk4.red(`Workspace with slug "${slug}" not found.`));
2312
- console.log(chalk4.gray("Available workspaces:"));
2771
+ console.error(chalk5.red(`Workspace with slug "${slug}" not found.`));
2772
+ console.log(chalk5.gray("Available workspaces:"));
2313
2773
  workspaces2.forEach((w) => {
2314
- console.log(chalk4.gray(` - ${w.slug} (${w.name})`));
2774
+ console.log(chalk5.gray(` - ${w.slug} (${w.name})`));
2315
2775
  });
2316
2776
  process.exit(1);
2317
2777
  }
@@ -2319,7 +2779,7 @@ function createWorkspacesCommand() {
2319
2779
  defaultWorkspaceId: workspace.id,
2320
2780
  defaultWorkspaceSlug: workspace.slug
2321
2781
  });
2322
- console.log(chalk4.green(`Default workspace set to: ${workspace.name} (${workspace.slug})`));
2782
+ console.log(chalk5.green(`Default workspace set to: ${workspace.name} (${workspace.slug})`));
2323
2783
  } catch (error) {
2324
2784
  handleError(error);
2325
2785
  }
@@ -2328,7 +2788,7 @@ function createWorkspacesCommand() {
2328
2788
  }
2329
2789
 
2330
2790
  // src/index.ts
2331
- var program = new Command11();
2791
+ var program = new Command13();
2332
2792
  program.name("exponential").description(
2333
2793
  [
2334
2794
  "CLI to interact with Exponential productivity app.",
@@ -2344,6 +2804,7 @@ program.addCommand(createContactsCommand());
2344
2804
  program.addCommand(createDealsCommand());
2345
2805
  program.addCommand(createEpicsCommand());
2346
2806
  program.addCommand(createFeaturesCommand());
2807
+ program.addCommand(createLabelsCommand());
2347
2808
  program.addCommand(createProductsCommand());
2348
2809
  program.addCommand(createProjectsCommand());
2349
2810
  program.addCommand(createTicketsCommand());