exponential-cli 1.2.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 Command12 } 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,11 +2092,12 @@ function createFeaturesCommand() {
1879
2092
  }
1880
2093
  }
1881
2094
  );
2095
+ features.addCommand(createStoriesCommand());
1882
2096
  return features;
1883
2097
  }
1884
2098
 
1885
2099
  // src/commands/labels.ts
1886
- import { Command as Command7 } from "commander";
2100
+ import { Command as Command8 } from "commander";
1887
2101
  import chalk4 from "chalk";
1888
2102
  var TRIAGE_LABELS = [
1889
2103
  { slug: "needs-triage", name: "needs-triage", color: "avatar-orange" },
@@ -1937,7 +2151,7 @@ async function resolveLabelIds(client, workspaceId, values) {
1937
2151
  return [...byId, ...resolvedFromSlugs];
1938
2152
  }
1939
2153
  function createLabelsCommand() {
1940
- const labels = new Command7("labels").description(
2154
+ const labels = new Command8("labels").description(
1941
2155
  "Manage labels (orthogonal tags applied to actions, tickets, features, and epics)."
1942
2156
  );
1943
2157
  labels.command("list").description("List labels visible in a workspace").option("--workspace <slug|id>", "Workspace slug or CUID").option(
@@ -2040,9 +2254,9 @@ function createLabelsCommand() {
2040
2254
  }
2041
2255
 
2042
2256
  // src/commands/products.ts
2043
- import { Command as Command8 } from "commander";
2257
+ import { Command as Command9 } from "commander";
2044
2258
  function createProductsCommand() {
2045
- const products = new Command8("products").description(
2259
+ const products = new Command9("products").description(
2046
2260
  "Manage products. A workspace contains products; a product holds features and tickets."
2047
2261
  );
2048
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) => {
@@ -2105,9 +2319,9 @@ function createProductsCommand() {
2105
2319
  }
2106
2320
 
2107
2321
  // src/commands/projects.ts
2108
- import { Command as Command9 } from "commander";
2322
+ import { Command as Command10 } from "commander";
2109
2323
  function createProjectsCommand() {
2110
- const projects = new Command9("projects").description("Manage projects");
2324
+ const projects = new Command10("projects").description("Manage projects");
2111
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) => {
2112
2326
  const globalOpts = cmd.optsWithGlobals();
2113
2327
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
@@ -2130,7 +2344,7 @@ function createProjectsCommand() {
2130
2344
  }
2131
2345
 
2132
2346
  // src/commands/tickets.ts
2133
- import { Command as Command10 } from "commander";
2347
+ import { Command as Command11 } from "commander";
2134
2348
  function collectRepeatable(value, previous) {
2135
2349
  return [...previous, value];
2136
2350
  }
@@ -2173,10 +2387,21 @@ function validateTicketType(value) {
2173
2387
  return value;
2174
2388
  }
2175
2389
  function createTicketsCommand() {
2176
- const tickets = new Command10("tickets").description(
2390
+ const tickets = new Command11("tickets").description(
2177
2391
  "Manage product backlog tickets. Tickets live under a product, link to features/epics, depend on each other, and can be implemented via actions."
2178
2392
  );
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(
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(
2180
2405
  "--label <slug-or-id>",
2181
2406
  "Filter to tickets carrying this label (repeatable; AND semantics)",
2182
2407
  collectRepeatable,
@@ -2188,20 +2413,32 @@ function createTicketsCommand() {
2188
2413
  try {
2189
2414
  const status = validateTicketStatus(options.status);
2190
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
+ }
2191
2422
  const client = getClient();
2192
- const workspaceId = await resolveWorkspaceId(client, options.workspace);
2193
- const productId = await resolveProductId(
2194
- client,
2195
- workspaceId,
2196
- options.product
2197
- );
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
+ }
2198
2433
  const list = await client.tickets.list({
2199
2434
  productId,
2200
2435
  status,
2201
2436
  type,
2202
2437
  featureId: options.feature,
2203
2438
  epicId: options.epic,
2204
- assigneeId: options.assignee
2439
+ assigneeId: options.assignee,
2440
+ prUrl: options.pr,
2441
+ branchName: options.branch
2205
2442
  });
2206
2443
  let filtered = list;
2207
2444
  if (options.label.length > 0) {
@@ -2463,7 +2700,7 @@ function createTicketsCommand() {
2463
2700
  handleError(error, useJson);
2464
2701
  }
2465
2702
  });
2466
- const comment = new Command10("comment").description("Manage comments on a ticket");
2703
+ const comment = new Command11("comment").description("Manage comments on a ticket");
2467
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) => {
2468
2705
  const globalOpts = cmd.optsWithGlobals();
2469
2706
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
@@ -2502,10 +2739,10 @@ function createTicketsCommand() {
2502
2739
  }
2503
2740
 
2504
2741
  // src/commands/workspaces.ts
2505
- import { Command as Command11 } from "commander";
2742
+ import { Command as Command12 } from "commander";
2506
2743
  import chalk5 from "chalk";
2507
2744
  function createWorkspacesCommand() {
2508
- const workspaces = new Command11("workspaces").description("Manage workspaces");
2745
+ const workspaces = new Command12("workspaces").description("Manage workspaces");
2509
2746
  workspaces.command("list").description("List all workspaces").action(async (_options, cmd) => {
2510
2747
  const globalOpts = cmd.optsWithGlobals();
2511
2748
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
@@ -2551,7 +2788,7 @@ function createWorkspacesCommand() {
2551
2788
  }
2552
2789
 
2553
2790
  // src/index.ts
2554
- var program = new Command12();
2791
+ var program = new Command13();
2555
2792
  program.name("exponential").description(
2556
2793
  [
2557
2794
  "CLI to interact with Exponential productivity app.",