exponential-cli 1.1.0 → 1.2.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 Command12 } from "commander";
5
5
 
6
6
  // src/commands/auth.ts
7
7
  import { Command } from "commander";
@@ -1882,10 +1882,167 @@ function createFeaturesCommand() {
1882
1882
  return features;
1883
1883
  }
1884
1884
 
1885
- // src/commands/products.ts
1885
+ // src/commands/labels.ts
1886
1886
  import { Command as Command7 } from "commander";
1887
+ import chalk4 from "chalk";
1888
+ var TRIAGE_LABELS = [
1889
+ { slug: "needs-triage", name: "needs-triage", color: "avatar-orange" },
1890
+ { slug: "needs-info", name: "needs-info", color: "avatar-yellow" },
1891
+ { slug: "ready-for-agent", name: "ready-for-agent", color: "avatar-green" },
1892
+ { slug: "ready-for-human", name: "ready-for-human", color: "avatar-lightBlue" },
1893
+ { slug: "wontfix", name: "wontfix", color: "avatar-lightGray" }
1894
+ ];
1895
+ var CUID_PATTERN = /^c[a-z0-9]{20,}$/i;
1896
+ function looksLikeCuid(value) {
1897
+ return CUID_PATTERN.test(value);
1898
+ }
1899
+ function outputTagsPretty(tags) {
1900
+ if (tags.length === 0) {
1901
+ console.log(chalk4.dim("No labels found."));
1902
+ return;
1903
+ }
1904
+ console.log();
1905
+ for (const tag of tags) {
1906
+ const scope = tag.workspaceId === null ? chalk4.cyan("global") : chalk4.magenta("workspace");
1907
+ const category = tag.category === null ? chalk4.dim("(label)") : chalk4.dim(`(${tag.category})`);
1908
+ console.log(
1909
+ ` ${chalk4.bold(tag.slug)} ${tag.name} ${chalk4.gray(tag.color)} ${scope} ${category}`
1910
+ );
1911
+ }
1912
+ console.log();
1913
+ }
1914
+ function outputTagPretty(tag, prefix) {
1915
+ if (prefix) console.log(prefix);
1916
+ console.log(` ${chalk4.bold(tag.slug)} ${tag.name} ${chalk4.gray(tag.color)}`);
1917
+ }
1918
+ async function resolveLabelIds(client, workspaceId, values) {
1919
+ if (values.length === 0) return [];
1920
+ const bySlug = values.filter((v) => !looksLikeCuid(v));
1921
+ const byId = values.filter((v) => looksLikeCuid(v));
1922
+ if (bySlug.length === 0) return byId;
1923
+ const list = await client.labels.list({ workspaceId });
1924
+ const slugMap = new Map(list.allTags.map((t) => [t.slug, t.id]));
1925
+ const resolvedFromSlugs = [];
1926
+ const missing = [];
1927
+ for (const slug of bySlug) {
1928
+ const id = slugMap.get(slug);
1929
+ if (id === void 0) missing.push(slug);
1930
+ else resolvedFromSlugs.push(id);
1931
+ }
1932
+ if (missing.length > 0) {
1933
+ throw new Error(
1934
+ `Unknown label slug(s): ${missing.join(", ")}. Run \`exponential labels list --workspace <slug>\` to see available labels.`
1935
+ );
1936
+ }
1937
+ return [...byId, ...resolvedFromSlugs];
1938
+ }
1939
+ function createLabelsCommand() {
1940
+ const labels = new Command7("labels").description(
1941
+ "Manage labels (orthogonal tags applied to actions, tickets, features, and epics)."
1942
+ );
1943
+ labels.command("list").description("List labels visible in a workspace").option("--workspace <slug|id>", "Workspace slug or CUID").option(
1944
+ "--category <category>",
1945
+ 'Filter by category. Use "label" for general labels, "null" for category-less labels, or omit to list all.'
1946
+ ).action(
1947
+ async (options, cmd) => {
1948
+ const globalOpts = cmd.optsWithGlobals();
1949
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
1950
+ try {
1951
+ const client = getClient();
1952
+ const workspaceId = await resolveWorkspaceId(client, options.workspace);
1953
+ const category = options.category === void 0 ? void 0 : options.category === "null" ? null : options.category;
1954
+ const result = await client.labels.list({ workspaceId, category });
1955
+ if (useJson) {
1956
+ console.log(JSON.stringify(result.allTags, null, 2));
1957
+ } else {
1958
+ outputTagsPretty(result.allTags);
1959
+ }
1960
+ } catch (error) {
1961
+ handleError(error, useJson);
1962
+ }
1963
+ }
1964
+ );
1965
+ 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(
1966
+ async (options, cmd) => {
1967
+ const globalOpts = cmd.optsWithGlobals();
1968
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
1969
+ try {
1970
+ const client = getClient();
1971
+ const workspaceId = await resolveWorkspaceId(client, options.workspace);
1972
+ if (workspaceId === void 0) {
1973
+ throw new Error(
1974
+ "Workspace is required. Pass --workspace <slug|id> or set a default with `exponential workspaces set-default`."
1975
+ );
1976
+ }
1977
+ const category = options.category === void 0 ? void 0 : options.category === "null" ? null : options.category;
1978
+ const tag = await client.labels.create({
1979
+ workspaceId,
1980
+ name: options.name,
1981
+ color: options.color,
1982
+ description: options.description,
1983
+ category
1984
+ });
1985
+ if (useJson) {
1986
+ console.log(JSON.stringify(tag, null, 2));
1987
+ } else {
1988
+ outputTagPretty(tag, chalk4.green("\n\u2713 Label created"));
1989
+ }
1990
+ } catch (error) {
1991
+ handleError(error, useJson);
1992
+ }
1993
+ }
1994
+ );
1995
+ labels.command("seed-triage").description("Idempotently seed the five canonical triage labels into a workspace.").option("--workspace <slug|id>", "Workspace slug or CUID").action(
1996
+ async (options, cmd) => {
1997
+ const globalOpts = cmd.optsWithGlobals();
1998
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
1999
+ try {
2000
+ const client = getClient();
2001
+ const workspaceId = await resolveWorkspaceId(client, options.workspace);
2002
+ if (workspaceId === void 0) {
2003
+ throw new Error(
2004
+ "Workspace is required. Pass --workspace <slug|id> or set a default with `exponential workspaces set-default`."
2005
+ );
2006
+ }
2007
+ const existing = await client.labels.list({ workspaceId });
2008
+ const existingSlugs = new Set(existing.allTags.map((t) => t.slug));
2009
+ const results = [];
2010
+ for (const def of TRIAGE_LABELS) {
2011
+ if (existingSlugs.has(def.slug)) {
2012
+ results.push({ slug: def.slug, status: "existed" });
2013
+ continue;
2014
+ }
2015
+ const tag = await client.labels.create({
2016
+ workspaceId,
2017
+ name: def.name,
2018
+ color: def.color
2019
+ });
2020
+ results.push({ slug: def.slug, status: "created", tag });
2021
+ }
2022
+ if (useJson) {
2023
+ console.log(JSON.stringify(results, null, 2));
2024
+ } else {
2025
+ console.log();
2026
+ console.log(chalk4.bold("Triage labels:"));
2027
+ for (const r of results) {
2028
+ const tag = chalk4.bold(r.slug);
2029
+ const status = r.status === "created" ? chalk4.green("created") : chalk4.dim("existed");
2030
+ console.log(` ${tag} ${status}`);
2031
+ }
2032
+ console.log();
2033
+ }
2034
+ } catch (error) {
2035
+ handleError(error, useJson);
2036
+ }
2037
+ }
2038
+ );
2039
+ return labels;
2040
+ }
2041
+
2042
+ // src/commands/products.ts
2043
+ import { Command as Command8 } from "commander";
1887
2044
  function createProductsCommand() {
1888
- const products = new Command7("products").description(
2045
+ const products = new Command8("products").description(
1889
2046
  "Manage products. A workspace contains products; a product holds features and tickets."
1890
2047
  );
1891
2048
  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 +2105,9 @@ function createProductsCommand() {
1948
2105
  }
1949
2106
 
1950
2107
  // src/commands/projects.ts
1951
- import { Command as Command8 } from "commander";
2108
+ import { Command as Command9 } from "commander";
1952
2109
  function createProjectsCommand() {
1953
- const projects = new Command8("projects").description("Manage projects");
2110
+ const projects = new Command9("projects").description("Manage projects");
1954
2111
  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
2112
  const globalOpts = cmd.optsWithGlobals();
1956
2113
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
@@ -1973,7 +2130,10 @@ function createProjectsCommand() {
1973
2130
  }
1974
2131
 
1975
2132
  // src/commands/tickets.ts
1976
- import { Command as Command9 } from "commander";
2133
+ import { Command as Command10 } from "commander";
2134
+ function collectRepeatable(value, previous) {
2135
+ return [...previous, value];
2136
+ }
1977
2137
  var TICKET_STATUSES = [
1978
2138
  "BACKLOG",
1979
2139
  "NEEDS_REFINEMENT",
@@ -2013,10 +2173,15 @@ function validateTicketType(value) {
2013
2173
  return value;
2014
2174
  }
2015
2175
  function createTicketsCommand() {
2016
- const tickets = new Command9("tickets").description(
2176
+ const tickets = new Command10("tickets").description(
2017
2177
  "Manage product backlog tickets. Tickets live under a product, link to features/epics, depend on each other, and can be implemented via actions."
2018
2178
  );
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(
2179
+ 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").option(
2180
+ "--label <slug-or-id>",
2181
+ "Filter to tickets carrying this label (repeatable; AND semantics)",
2182
+ collectRepeatable,
2183
+ []
2184
+ ).action(
2020
2185
  async (options, cmd) => {
2021
2186
  const globalOpts = cmd.optsWithGlobals();
2022
2187
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
@@ -2038,8 +2203,27 @@ function createTicketsCommand() {
2038
2203
  epicId: options.epic,
2039
2204
  assigneeId: options.assignee
2040
2205
  });
2041
- if (useJson) outputTicketsJson(list);
2042
- else outputTicketsPretty(list);
2206
+ let filtered = list;
2207
+ if (options.label.length > 0) {
2208
+ const labelIds = new Set(
2209
+ await resolveLabelIds(client, workspaceId, options.label)
2210
+ );
2211
+ const ticketLabelSets = await Promise.all(
2212
+ list.map(
2213
+ (t) => client.labels.listForEntity({ entityType: "ticket", entityId: t.id }).then((tags) => new Set(tags.map((tag) => tag.id)))
2214
+ )
2215
+ );
2216
+ filtered = list.filter((_t, idx) => {
2217
+ const tagSet = ticketLabelSets[idx];
2218
+ if (tagSet === void 0) return false;
2219
+ for (const id of labelIds) {
2220
+ if (!tagSet.has(id)) return false;
2221
+ }
2222
+ return true;
2223
+ });
2224
+ }
2225
+ if (useJson) outputTicketsJson(filtered);
2226
+ else outputTicketsPretty(filtered);
2043
2227
  } catch (error) {
2044
2228
  handleError(error, useJson);
2045
2229
  }
@@ -2069,7 +2253,12 @@ function createTicketsCommand() {
2069
2253
  handleError(error, useJson);
2070
2254
  }
2071
2255
  });
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(
2256
+ 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(
2257
+ "--label <slug-or-id>",
2258
+ "Label to attach (repeatable)",
2259
+ collectRepeatable,
2260
+ []
2261
+ ).action(
2073
2262
  async (options, cmd) => {
2074
2263
  const globalOpts = cmd.optsWithGlobals();
2075
2264
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
@@ -2097,6 +2286,14 @@ function createTicketsCommand() {
2097
2286
  branchName: options.branch,
2098
2287
  prUrl: options.pr
2099
2288
  });
2289
+ if (options.label.length > 0) {
2290
+ const tagIds = await resolveLabelIds(client, workspaceId, options.label);
2291
+ await client.labels.setEntityTags({
2292
+ entityType: "ticket",
2293
+ entityId: ticket.id,
2294
+ tagIds
2295
+ });
2296
+ }
2100
2297
  if (useJson) outputTicketJson(ticket);
2101
2298
  else {
2102
2299
  console.log("\n\u2713 Ticket created");
@@ -2107,7 +2304,17 @@ function createTicketsCommand() {
2107
2304
  }
2108
2305
  }
2109
2306
  );
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(
2307
+ 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(
2308
+ "--add-label <slug-or-id>",
2309
+ "Label to attach to this ticket (repeatable)",
2310
+ collectRepeatable,
2311
+ []
2312
+ ).option(
2313
+ "--remove-label <slug-or-id>",
2314
+ "Label to detach from this ticket (repeatable)",
2315
+ collectRepeatable,
2316
+ []
2317
+ ).action(
2111
2318
  async (options, cmd) => {
2112
2319
  const globalOpts = cmd.optsWithGlobals();
2113
2320
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
@@ -2148,6 +2355,22 @@ function createTicketsCommand() {
2148
2355
  branchName: parseNullableString(options.branch),
2149
2356
  prUrl: parseNullableString(options.pr)
2150
2357
  });
2358
+ if (options.addLabel.length > 0 || options.removeLabel.length > 0) {
2359
+ const current = await client.labels.listForEntity({
2360
+ entityType: "ticket",
2361
+ entityId: options.id
2362
+ });
2363
+ const currentIds = new Set(current.map((t) => t.id));
2364
+ const addIds = await resolveLabelIds(client, void 0, options.addLabel);
2365
+ const removeIds = await resolveLabelIds(client, void 0, options.removeLabel);
2366
+ for (const id of addIds) currentIds.add(id);
2367
+ for (const id of removeIds) currentIds.delete(id);
2368
+ await client.labels.setEntityTags({
2369
+ entityType: "ticket",
2370
+ entityId: options.id,
2371
+ tagIds: [...currentIds]
2372
+ });
2373
+ }
2151
2374
  if (useJson) outputTicketJson(ticket);
2152
2375
  else {
2153
2376
  console.log("\n\u2713 Ticket updated");
@@ -2240,7 +2463,7 @@ function createTicketsCommand() {
2240
2463
  handleError(error, useJson);
2241
2464
  }
2242
2465
  });
2243
- const comment = new Command9("comment").description("Manage comments on a ticket");
2466
+ const comment = new Command10("comment").description("Manage comments on a ticket");
2244
2467
  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
2468
  const globalOpts = cmd.optsWithGlobals();
2246
2469
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
@@ -2279,10 +2502,10 @@ function createTicketsCommand() {
2279
2502
  }
2280
2503
 
2281
2504
  // src/commands/workspaces.ts
2282
- import { Command as Command10 } from "commander";
2283
- import chalk4 from "chalk";
2505
+ import { Command as Command11 } from "commander";
2506
+ import chalk5 from "chalk";
2284
2507
  function createWorkspacesCommand() {
2285
- const workspaces = new Command10("workspaces").description("Manage workspaces");
2508
+ const workspaces = new Command11("workspaces").description("Manage workspaces");
2286
2509
  workspaces.command("list").description("List all workspaces").action(async (_options, cmd) => {
2287
2510
  const globalOpts = cmd.optsWithGlobals();
2288
2511
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
@@ -2295,7 +2518,7 @@ function createWorkspacesCommand() {
2295
2518
  const config = getConfig();
2296
2519
  outputWorkspacesPretty(workspaces2);
2297
2520
  if (config.defaultWorkspaceSlug) {
2298
- console.log(chalk4.gray(`Default workspace: ${config.defaultWorkspaceSlug}`));
2521
+ console.log(chalk5.gray(`Default workspace: ${config.defaultWorkspaceSlug}`));
2299
2522
  }
2300
2523
  }
2301
2524
  } catch (error) {
@@ -2308,10 +2531,10 @@ function createWorkspacesCommand() {
2308
2531
  const workspaces2 = await client.workspaces.list();
2309
2532
  const workspace = workspaces2.find((w) => w.slug === slug);
2310
2533
  if (!workspace) {
2311
- console.error(chalk4.red(`Workspace with slug "${slug}" not found.`));
2312
- console.log(chalk4.gray("Available workspaces:"));
2534
+ console.error(chalk5.red(`Workspace with slug "${slug}" not found.`));
2535
+ console.log(chalk5.gray("Available workspaces:"));
2313
2536
  workspaces2.forEach((w) => {
2314
- console.log(chalk4.gray(` - ${w.slug} (${w.name})`));
2537
+ console.log(chalk5.gray(` - ${w.slug} (${w.name})`));
2315
2538
  });
2316
2539
  process.exit(1);
2317
2540
  }
@@ -2319,7 +2542,7 @@ function createWorkspacesCommand() {
2319
2542
  defaultWorkspaceId: workspace.id,
2320
2543
  defaultWorkspaceSlug: workspace.slug
2321
2544
  });
2322
- console.log(chalk4.green(`Default workspace set to: ${workspace.name} (${workspace.slug})`));
2545
+ console.log(chalk5.green(`Default workspace set to: ${workspace.name} (${workspace.slug})`));
2323
2546
  } catch (error) {
2324
2547
  handleError(error);
2325
2548
  }
@@ -2328,7 +2551,7 @@ function createWorkspacesCommand() {
2328
2551
  }
2329
2552
 
2330
2553
  // src/index.ts
2331
- var program = new Command11();
2554
+ var program = new Command12();
2332
2555
  program.name("exponential").description(
2333
2556
  [
2334
2557
  "CLI to interact with Exponential productivity app.",
@@ -2344,6 +2567,7 @@ program.addCommand(createContactsCommand());
2344
2567
  program.addCommand(createDealsCommand());
2345
2568
  program.addCommand(createEpicsCommand());
2346
2569
  program.addCommand(createFeaturesCommand());
2570
+ program.addCommand(createLabelsCommand());
2347
2571
  program.addCommand(createProductsCommand());
2348
2572
  program.addCommand(createProjectsCommand());
2349
2573
  program.addCommand(createTicketsCommand());