exponential-cli 1.18.1 → 1.19.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/README.md +22 -0
- package/dist/index.js +231 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -140,6 +140,7 @@ and live under `goals kr`.
|
|
|
140
140
|
exponential goals list --workspace clear --period Q3-2026
|
|
141
141
|
exponential goals list --workspace clear --tree
|
|
142
142
|
exponential goals list --all-workspaces # "what am I neglecting" is cross-workspace
|
|
143
|
+
exponential goals list --project <cuid> # objectives (yours) linked to a project
|
|
143
144
|
|
|
144
145
|
exponential goals get 46
|
|
145
146
|
exponential goals create --workspace clear --title "Ship the CLI" --period Q3-2026
|
|
@@ -312,6 +313,27 @@ exponential projects update --id <cuid> --product none
|
|
|
312
313
|
exponential projects delete --id <cuid> [--force]
|
|
313
314
|
```
|
|
314
315
|
|
|
316
|
+
### Products, tickets, epics and user stories
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
# Products take a slug or a CUID
|
|
320
|
+
exponential products list --workspace clear
|
|
321
|
+
exponential products update <slug|cuid> --workspace clear --description "..." [--fun-ticket-ids|--no-fun-ticket-ids]
|
|
322
|
+
# Deleting a product cascades its features, tickets and epics: refuses unless --force
|
|
323
|
+
exponential products delete <slug|cuid> --workspace clear [--force]
|
|
324
|
+
|
|
325
|
+
# Free-text ticket search within a product: title, shortId or ticket number
|
|
326
|
+
exponential tickets search "login redirect" --product <slug|cuid> --workspace clear [--limit 50]
|
|
327
|
+
|
|
328
|
+
# Epics: their tickets and actions are unlinked, not deleted
|
|
329
|
+
exponential epics delete --id <cuid>
|
|
330
|
+
|
|
331
|
+
# A feature's native user stories
|
|
332
|
+
exponential features stories list --feature <cuid>
|
|
333
|
+
exponential features stories update --id <story-cuid> --i-want "..." [--scope <scope-cuid>|null]
|
|
334
|
+
exponential features stories rm --id <story-cuid>
|
|
335
|
+
```
|
|
336
|
+
|
|
315
337
|
### Workspaces
|
|
316
338
|
|
|
317
339
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -1187,6 +1187,41 @@ Tickets (${tickets.length} total)`));
|
|
|
1187
1187
|
}
|
|
1188
1188
|
console.log();
|
|
1189
1189
|
}
|
|
1190
|
+
function transformTicketSearchHit(hit) {
|
|
1191
|
+
return {
|
|
1192
|
+
id: hit.id,
|
|
1193
|
+
number: hit.number,
|
|
1194
|
+
shortId: hit.shortId,
|
|
1195
|
+
title: hit.title,
|
|
1196
|
+
status: hit.status,
|
|
1197
|
+
priority: hit.priority,
|
|
1198
|
+
assignee: hit.assignee ?? null
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
function outputTicketSearchJson(hits, query) {
|
|
1202
|
+
console.log(JSON.stringify({
|
|
1203
|
+
query,
|
|
1204
|
+
tickets: hits.map(transformTicketSearchHit),
|
|
1205
|
+
total: hits.length
|
|
1206
|
+
}, null, 2));
|
|
1207
|
+
}
|
|
1208
|
+
function outputTicketSearchPretty(hits, query) {
|
|
1209
|
+
if (hits.length === 0) {
|
|
1210
|
+
console.log(chalk3.gray(`No tickets match "${query}".`));
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
console.log(chalk3.bold(`
|
|
1214
|
+
Tickets matching "${query}" (${hits.length})`));
|
|
1215
|
+
console.log(chalk3.gray("\u2500".repeat(50)));
|
|
1216
|
+
for (const t of hits) {
|
|
1217
|
+
const id = t.shortId ?? (t.number != null ? `#${t.number}` : "?");
|
|
1218
|
+
const statusBadge2 = chalk3[getTicketStatusColor(t.status)](`[${t.status}]`);
|
|
1219
|
+
const assignee = t.assignee ? chalk3.gray(` \u2014 ${t.assignee.name ?? t.assignee.id}`) : "";
|
|
1220
|
+
console.log(` ${statusBadge2} ${chalk3.bold(id)} ${t.title}${assignee}`);
|
|
1221
|
+
console.log(chalk3.gray(` ID: ${t.id}`));
|
|
1222
|
+
}
|
|
1223
|
+
console.log();
|
|
1224
|
+
}
|
|
1190
1225
|
function transformTicket(ticket) {
|
|
1191
1226
|
const detail = ticket;
|
|
1192
1227
|
return {
|
|
@@ -3959,6 +3994,23 @@ function createEpicsCommand() {
|
|
|
3959
3994
|
}
|
|
3960
3995
|
}
|
|
3961
3996
|
);
|
|
3997
|
+
epics.command("delete").description(
|
|
3998
|
+
"Delete an epic. Its tickets and actions are unlinked from it (epicId is cleared), not deleted. Only workspace owners/admins and the epic owner can delete."
|
|
3999
|
+
).requiredOption("--id <id>", "Epic CUID").action(async (options, cmd) => {
|
|
4000
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
4001
|
+
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
4002
|
+
try {
|
|
4003
|
+
const client = getClient();
|
|
4004
|
+
await client.epics.delete(options.id);
|
|
4005
|
+
if (useJson) {
|
|
4006
|
+
console.log(JSON.stringify({ deleted: true, id: options.id }, null, 2));
|
|
4007
|
+
} else {
|
|
4008
|
+
console.log("\n\u2713 Epic deleted");
|
|
4009
|
+
}
|
|
4010
|
+
} catch (error) {
|
|
4011
|
+
handleError(error, useJson);
|
|
4012
|
+
}
|
|
4013
|
+
});
|
|
3962
4014
|
return epics;
|
|
3963
4015
|
}
|
|
3964
4016
|
|
|
@@ -4069,6 +4121,42 @@ function createStoriesCommand() {
|
|
|
4069
4121
|
}
|
|
4070
4122
|
}
|
|
4071
4123
|
);
|
|
4124
|
+
stories.command("update").description("Update a user story. Only the fields you pass are written.").requiredOption("--id <id>", "User story 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 (or "null" to ungroup)').action(
|
|
4125
|
+
async (options, cmd) => {
|
|
4126
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
4127
|
+
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
4128
|
+
try {
|
|
4129
|
+
const client = getClient();
|
|
4130
|
+
const story = await client.userStories.update({
|
|
4131
|
+
id: options.id,
|
|
4132
|
+
asA: options.asA,
|
|
4133
|
+
iWant: options.iWant,
|
|
4134
|
+
soThat: options.soThat,
|
|
4135
|
+
acceptanceCriteria: options.acceptance,
|
|
4136
|
+
scopeId: options.scope === "null" ? null : options.scope
|
|
4137
|
+
});
|
|
4138
|
+
if (useJson) outputUserStoryJson(story);
|
|
4139
|
+
else {
|
|
4140
|
+
console.log("\n\u2713 User story updated");
|
|
4141
|
+
outputUserStoryPretty(story);
|
|
4142
|
+
}
|
|
4143
|
+
} catch (error) {
|
|
4144
|
+
handleError(error, useJson);
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
4147
|
+
);
|
|
4148
|
+
stories.command("rm").description("Delete a user story").requiredOption("--id <id>", "User story CUID").action(async (options, cmd) => {
|
|
4149
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
4150
|
+
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
4151
|
+
try {
|
|
4152
|
+
const client = getClient();
|
|
4153
|
+
const result = await client.userStories.delete(options.id);
|
|
4154
|
+
if (useJson) console.log(JSON.stringify(result, null, 2));
|
|
4155
|
+
else console.log("\n\u2713 User story deleted");
|
|
4156
|
+
} catch (error) {
|
|
4157
|
+
handleError(error, useJson);
|
|
4158
|
+
}
|
|
4159
|
+
});
|
|
4072
4160
|
return stories;
|
|
4073
4161
|
}
|
|
4074
4162
|
|
|
@@ -5635,6 +5723,84 @@ function createProductsCommand() {
|
|
|
5635
5723
|
}
|
|
5636
5724
|
}
|
|
5637
5725
|
);
|
|
5726
|
+
products.command("update <slugOrId>").description("Update a product. Only the fields you pass are written.").option("-n, --name <name>", "New name").option("-d, --description <text>", "New description").option("--icon <icon>", "Icon shortcode").option("--color <color>", "Color").option("--fun-ticket-ids", "Use fun ticket ids (e.g. prime.toucan) for new tickets").option("--no-fun-ticket-ids", "Use numbered ticket ids for new tickets").option("--workspace <slug|id>", "Workspace (required when using a slug)").action(
|
|
5727
|
+
async (slugOrId, options, cmd) => {
|
|
5728
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
5729
|
+
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
5730
|
+
try {
|
|
5731
|
+
const client = getClient();
|
|
5732
|
+
const workspaceId = await resolveWorkspaceId(client, options.workspace);
|
|
5733
|
+
const existing = await client.products.resolve(workspaceId, slugOrId);
|
|
5734
|
+
const product = await client.products.update({
|
|
5735
|
+
id: existing.id,
|
|
5736
|
+
name: options.name,
|
|
5737
|
+
description: options.description,
|
|
5738
|
+
icon: options.icon,
|
|
5739
|
+
color: options.color,
|
|
5740
|
+
funTicketIds: options.funTicketIds
|
|
5741
|
+
});
|
|
5742
|
+
if (useJson) outputProductJson(product);
|
|
5743
|
+
else {
|
|
5744
|
+
console.log("\n\u2713 Product updated");
|
|
5745
|
+
outputProductPretty(product);
|
|
5746
|
+
}
|
|
5747
|
+
} catch (error) {
|
|
5748
|
+
handleError(error, useJson);
|
|
5749
|
+
}
|
|
5750
|
+
}
|
|
5751
|
+
);
|
|
5752
|
+
products.command("delete <slugOrId>").description(
|
|
5753
|
+
"Delete a product. Deleting a product deletes everything under it \u2014 its features, tickets, epics, research and retrospectives cascade \u2014 so this refuses while it still has any of them, unless --force."
|
|
5754
|
+
).option("--force", "Delete anyway, taking everything under it with it (destructive)").option("--workspace <slug|id>", "Workspace (required when using a slug)").action(
|
|
5755
|
+
async (slugOrId, options, cmd) => {
|
|
5756
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
5757
|
+
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
5758
|
+
try {
|
|
5759
|
+
const client = getClient();
|
|
5760
|
+
const workspaceId = await resolveWorkspaceId(client, options.workspace);
|
|
5761
|
+
const product = await client.products.resolve(workspaceId, slugOrId);
|
|
5762
|
+
const counts = product._count ?? {};
|
|
5763
|
+
const blockers = [
|
|
5764
|
+
counts.features ? `${counts.features} feature(s)` : null,
|
|
5765
|
+
counts.tickets ? `${counts.tickets} ticket(s)` : null,
|
|
5766
|
+
counts.researches ? `${counts.researches} research item(s)` : null,
|
|
5767
|
+
counts.retrospectives ? `${counts.retrospectives} retrospective(s)` : null
|
|
5768
|
+
].filter(Boolean);
|
|
5769
|
+
if (blockers.length > 0 && !options.force) {
|
|
5770
|
+
const payload2 = {
|
|
5771
|
+
deleted: false,
|
|
5772
|
+
id: product.id,
|
|
5773
|
+
slug: product.slug,
|
|
5774
|
+
reason: `has ${blockers.join(", ")}`,
|
|
5775
|
+
counts
|
|
5776
|
+
};
|
|
5777
|
+
if (useJson) console.log(JSON.stringify(payload2, null, 2));
|
|
5778
|
+
else {
|
|
5779
|
+
console.error(
|
|
5780
|
+
`Product "${product.name}" has ${blockers.join(", ")}. Deleting it deletes them too.
|
|
5781
|
+
Re-run with --force to delete it anyway.`
|
|
5782
|
+
);
|
|
5783
|
+
}
|
|
5784
|
+
process.exitCode = 1;
|
|
5785
|
+
return;
|
|
5786
|
+
}
|
|
5787
|
+
await client.products.delete(product.id);
|
|
5788
|
+
const payload = {
|
|
5789
|
+
deleted: true,
|
|
5790
|
+
id: product.id,
|
|
5791
|
+
slug: product.slug,
|
|
5792
|
+
counts
|
|
5793
|
+
};
|
|
5794
|
+
if (useJson) console.log(JSON.stringify(payload, null, 2));
|
|
5795
|
+
else {
|
|
5796
|
+
console.log("\n\u2713 Product deleted");
|
|
5797
|
+
if (blockers.length) console.log(` ${blockers.join(", ")} went with it`);
|
|
5798
|
+
}
|
|
5799
|
+
} catch (error) {
|
|
5800
|
+
handleError(error, useJson);
|
|
5801
|
+
}
|
|
5802
|
+
}
|
|
5803
|
+
);
|
|
5638
5804
|
return products;
|
|
5639
5805
|
}
|
|
5640
5806
|
|
|
@@ -5995,6 +6161,49 @@ function createTicketsCommand() {
|
|
|
5995
6161
|
}
|
|
5996
6162
|
}
|
|
5997
6163
|
);
|
|
6164
|
+
tickets.command("search <query>").description(
|
|
6165
|
+
[
|
|
6166
|
+
"Find tickets in a product by free text \u2014 the same picker search the app uses",
|
|
6167
|
+
"when you link a dependency. Matches title (case-insensitive substring), shortId",
|
|
6168
|
+
'(word-order-insensitive, so "toucan.prime" finds prime.toucan) or an exact',
|
|
6169
|
+
"ticket number. Most recently updated first. Returns slim rows (id, number,",
|
|
6170
|
+
"shortId, title, status, priority, assignee); use `tickets get` for the rest."
|
|
6171
|
+
].join("\n")
|
|
6172
|
+
).requiredOption("--product <slug|id>", "Product slug or CUID").option("--workspace <slug|id>", "Workspace (required when --product is a slug)").option("--limit <n>", "Max results, 1-50 (default 20)").option("--exclude <id>", "Ticket CUID to leave out of the results").action(
|
|
6173
|
+
async (query, options, cmd) => {
|
|
6174
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
6175
|
+
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
6176
|
+
try {
|
|
6177
|
+
const trimmed = query.trim();
|
|
6178
|
+
if (!trimmed) {
|
|
6179
|
+
throw new Error("Search query is empty. Use `tickets list` to list a product's tickets.");
|
|
6180
|
+
}
|
|
6181
|
+
if (trimmed.length > 200) {
|
|
6182
|
+
throw new Error("Search query is too long (max 200 characters).");
|
|
6183
|
+
}
|
|
6184
|
+
let limit;
|
|
6185
|
+
if (options.limit !== void 0) {
|
|
6186
|
+
limit = Number(options.limit);
|
|
6187
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 50) {
|
|
6188
|
+
throw new Error(`Invalid --limit "${options.limit}". Use a whole number from 1 to 50.`);
|
|
6189
|
+
}
|
|
6190
|
+
}
|
|
6191
|
+
const client = getClient();
|
|
6192
|
+
const workspaceId = await resolveWorkspaceId(client, options.workspace);
|
|
6193
|
+
const productId = await resolveProductId(client, workspaceId, options.product);
|
|
6194
|
+
const hits = await client.tickets.search({
|
|
6195
|
+
productId,
|
|
6196
|
+
query: trimmed,
|
|
6197
|
+
excludeTicketId: options.exclude,
|
|
6198
|
+
limit
|
|
6199
|
+
});
|
|
6200
|
+
if (useJson) outputTicketSearchJson(hits, trimmed);
|
|
6201
|
+
else outputTicketSearchPretty(hits, trimmed);
|
|
6202
|
+
} catch (error) {
|
|
6203
|
+
handleError(error, useJson);
|
|
6204
|
+
}
|
|
6205
|
+
}
|
|
6206
|
+
);
|
|
5998
6207
|
tickets.command("get <id>").description("Show a ticket with dependencies, actions, and comments").action(async (id, _options, cmd) => {
|
|
5999
6208
|
const globalOpts = cmd.optsWithGlobals();
|
|
6000
6209
|
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
@@ -7092,13 +7301,34 @@ function createGoalsCommand() {
|
|
|
7092
7301
|
);
|
|
7093
7302
|
goals.command("list").description(
|
|
7094
7303
|
"List objectives, each with the projects linked to it \u2014 the join that ties an action (which carries a projectId) back to a goal."
|
|
7095
|
-
).option("-w, --workspace <slug|id>", "Workspace slug or ID (defaults to your default workspace)").option("--all-workspaces", "List across every workspace you belong to").option("--period <period>", "Filter by period, e.g. Q3-2026").option("--status <status>", `Filter by status: ${GOAL_STATUSES.join(", ")}`).option("--tree", "Render the parent/child cascade instead of a flat list").
|
|
7304
|
+
).option("-w, --workspace <slug|id>", "Workspace slug or ID (defaults to your default workspace)").option("--all-workspaces", "List across every workspace you belong to").option("--period <period>", "Filter by period, e.g. Q3-2026").option("--status <status>", `Filter by status: ${GOAL_STATUSES.join(", ")}`).option("--tree", "Render the parent/child cascade instead of a flat list").option(
|
|
7305
|
+
"--project <cuid>",
|
|
7306
|
+
"Only objectives linked to this project (yours only \u2014 the server scopes this lookup to objectives you created). Not combinable with --workspace, --all-workspaces or --tree."
|
|
7307
|
+
).action(
|
|
7096
7308
|
async (options, cmd) => {
|
|
7097
7309
|
const globalOpts = cmd.optsWithGlobals();
|
|
7098
7310
|
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
7099
7311
|
try {
|
|
7100
7312
|
const status = validateGoalStatus(options.status);
|
|
7101
7313
|
const client = getClient();
|
|
7314
|
+
if (options.project) {
|
|
7315
|
+
const conflicting = [
|
|
7316
|
+
options.workspace ? "--workspace" : null,
|
|
7317
|
+
options.allWorkspaces ? "--all-workspaces" : null,
|
|
7318
|
+
options.tree ? "--tree" : null
|
|
7319
|
+
].filter(Boolean);
|
|
7320
|
+
if (conflicting.length > 0) {
|
|
7321
|
+
throw new Error(
|
|
7322
|
+
`--project can't be combined with ${conflicting.join(", ")}.`
|
|
7323
|
+
);
|
|
7324
|
+
}
|
|
7325
|
+
const list2 = (await client.goals.listByProject(options.project)).filter(
|
|
7326
|
+
(g) => (!options.period || g.period === options.period) && (!status || g.status === status)
|
|
7327
|
+
);
|
|
7328
|
+
if (useJson) outputGoalsJson(list2);
|
|
7329
|
+
else outputGoalsPretty(list2);
|
|
7330
|
+
return;
|
|
7331
|
+
}
|
|
7102
7332
|
const workspaces = await targetWorkspaces(client, options);
|
|
7103
7333
|
if (options.tree) {
|
|
7104
7334
|
const trees = await fanOutByWorkspace(
|