exponential-cli 1.7.0 → 1.9.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 +51 -1
- package/dist/index.js +390 -22
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -70,9 +70,17 @@ exponential actions create -n "Task name" -d "Description" -p <project-id> --pri
|
|
|
70
70
|
exponential actions update --id <action-id> --kanban DONE
|
|
71
71
|
exponential actions update --id <action-id> -n "New name" --priority "2nd Priority" --due 2026-03-15
|
|
72
72
|
|
|
73
|
-
#
|
|
73
|
+
# Set the do-date (when you plan to work on it). This is what `today` partitions on,
|
|
74
|
+
# and it takes precedence over --due.
|
|
75
|
+
exponential actions update --id <action-id> --scheduled-start 2026-08-05T09:00
|
|
76
|
+
exponential actions update --id <action-id> --scheduled-start null # clear it
|
|
77
|
+
|
|
78
|
+
# What's actually on your plate: overdue + today + inbox
|
|
74
79
|
exponential actions today
|
|
75
80
|
|
|
81
|
+
# Just the actions whose dueDate is today (excludes overdue — see note below)
|
|
82
|
+
exponential actions today --due-only
|
|
83
|
+
|
|
76
84
|
# Get actions in a date range
|
|
77
85
|
exponential actions range --start 2024-01-01 --end 2024-01-31
|
|
78
86
|
|
|
@@ -80,6 +88,48 @@ exponential actions range --start 2024-01-01 --end 2024-01-31
|
|
|
80
88
|
exponential actions kanban --project <project-id>
|
|
81
89
|
```
|
|
82
90
|
|
|
91
|
+
#### Triaging an overdue pile
|
|
92
|
+
|
|
93
|
+
A large overdue count is usually not a large number of missed commitments — it's
|
|
94
|
+
a few bulk writes (a generated project plan, an import) that stamped every row
|
|
95
|
+
with one identical timestamp. `actions overdue` separates the two:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
exponential actions overdue
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
Overdue: 43
|
|
103
|
+
|
|
104
|
+
21 of these were bulk-created — stamped with one identical timestamp,
|
|
105
|
+
so almost certainly never individually due.
|
|
106
|
+
|
|
107
|
+
17 actions stamped 2026-07-25T08:29:55.483Z (10d overdue)
|
|
108
|
+
Projects: Entity Money Map, Net Worth Baseline, Tax Reserve System, …
|
|
109
|
+
Amnesty: exponential actions defer --ids cmqcgvtlh…,cmqcgvtnn…,…
|
|
110
|
+
|
|
111
|
+
19 individually dated — real debt, oldest first.
|
|
112
|
+
12d Send out the job description for the data engineering role [Hiring]
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Then pick a disposition:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
# Amnesty: clear the dates, back to the project backlog untimed.
|
|
119
|
+
# For work that was never really due on the date it carries.
|
|
120
|
+
exponential actions defer --ids id1,id2,id3
|
|
121
|
+
|
|
122
|
+
# Reschedule: still due, just later.
|
|
123
|
+
exponential actions reschedule --ids id1,id2 --to today
|
|
124
|
+
exponential actions reschedule --ids id1,id2 --to 2026-08-10
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
> **Note on `actions today`:** as of v1.9.0 this returns the same
|
|
128
|
+
> overdue/today/inbox partition the `/today` page renders, rather than a
|
|
129
|
+
> due-date-only list. The old behaviour silently omitted the overdue pile — it
|
|
130
|
+
> would report 6 actions while 43 sat overdue. Pass `--due-only` for the
|
|
131
|
+
> previous output shape.
|
|
132
|
+
|
|
83
133
|
### Projects
|
|
84
134
|
|
|
85
135
|
```bash
|
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
|
|
4
|
+
import { Command as Command19 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/auth.ts
|
|
7
7
|
import { Command } from "commander";
|
|
@@ -329,6 +329,91 @@ ${statusBadge} ${chalk3.bold(action.name)}`);
|
|
|
329
329
|
}
|
|
330
330
|
console.log();
|
|
331
331
|
}
|
|
332
|
+
function outputTodaysActionsJson(t, filters = {}) {
|
|
333
|
+
console.log(JSON.stringify({
|
|
334
|
+
overdue: { count: t.overdue.count, actions: t.overdue.actions },
|
|
335
|
+
today: { count: t.today.count, actions: t.today.actions },
|
|
336
|
+
inbox: { count: t.inbox.count, actions: t.inbox.actions },
|
|
337
|
+
total: t.overdue.count + t.today.count + t.inbox.count,
|
|
338
|
+
filters
|
|
339
|
+
}, null, 2));
|
|
340
|
+
}
|
|
341
|
+
function printTodayGroup(label, group, color) {
|
|
342
|
+
if (group.count === 0) return;
|
|
343
|
+
const truncated = group.count > group.actions.length ? chalk3.gray(` (showing ${group.actions.length})`) : "";
|
|
344
|
+
console.log(`
|
|
345
|
+
${chalk3[color].bold(`${label} (${group.count})`)}${truncated}`);
|
|
346
|
+
for (const a of group.actions) {
|
|
347
|
+
const when = a.scheduledStart ?? a.dueDate;
|
|
348
|
+
const stamp = when ? chalk3.gray(formatDate(new Date(when))) : "";
|
|
349
|
+
const project = a.projectName ? chalk3.cyan(` [${a.projectName}]`) : "";
|
|
350
|
+
console.log(` ${a.name}${project} ${stamp}`);
|
|
351
|
+
console.log(chalk3.gray(` ID: ${a.id}`));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function outputTodaysActionsPretty(t) {
|
|
355
|
+
const total = t.overdue.count + t.today.count + t.inbox.count;
|
|
356
|
+
if (total === 0) {
|
|
357
|
+
console.log(chalk3.gray("Nothing on your plate."));
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
console.log(chalk3.bold("\nOn your plate"));
|
|
361
|
+
console.log(chalk3.gray("\u2500".repeat(50)));
|
|
362
|
+
printTodayGroup("Overdue", t.overdue, "red");
|
|
363
|
+
printTodayGroup("Today", t.today, "green");
|
|
364
|
+
printTodayGroup("Inbox", t.inbox, "gray");
|
|
365
|
+
if (t.overdue.count > 0) {
|
|
366
|
+
console.log(chalk3.gray(`
|
|
367
|
+
Run "exponential actions overdue" to see why the overdue pile is that size.`));
|
|
368
|
+
}
|
|
369
|
+
console.log();
|
|
370
|
+
}
|
|
371
|
+
function outputOverdueTriageJson(t) {
|
|
372
|
+
console.log(JSON.stringify(t, null, 2));
|
|
373
|
+
}
|
|
374
|
+
function outputOverdueTriagePretty(t) {
|
|
375
|
+
if (t.totalOverdue === 0) {
|
|
376
|
+
console.log(chalk3.green("Nothing overdue."));
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
console.log(chalk3.bold(`
|
|
380
|
+
Overdue: ${t.totalOverdue}`));
|
|
381
|
+
console.log(chalk3.gray("\u2500".repeat(50)));
|
|
382
|
+
if (t.cohorts.length > 0) {
|
|
383
|
+
console.log(
|
|
384
|
+
`
|
|
385
|
+
${chalk3.yellow.bold(`${t.cohortCount} of these were bulk-created`)} ${chalk3.gray("\u2014 stamped with one identical timestamp, so almost certainly never individually due.")}`
|
|
386
|
+
);
|
|
387
|
+
for (const c of t.cohorts) {
|
|
388
|
+
console.log(
|
|
389
|
+
`
|
|
390
|
+
${chalk3.bold(`${c.count} actions`)} stamped ${chalk3.gray(c.stampedAt.toISOString())} ${chalk3.red(`(${c.daysOverdue}d overdue)`)}`
|
|
391
|
+
);
|
|
392
|
+
if (c.projectNames.length > 0) {
|
|
393
|
+
console.log(` ${chalk3.cyan("Projects:")} ${c.projectNames.join(", ")}`);
|
|
394
|
+
}
|
|
395
|
+
for (const a of c.actions.slice(0, 4)) {
|
|
396
|
+
console.log(chalk3.gray(` \xB7 ${a.name.substring(0, 62)}`));
|
|
397
|
+
}
|
|
398
|
+
if (c.actions.length > 4) {
|
|
399
|
+
console.log(chalk3.gray(` \xB7 \u2026 and ${c.actions.length - 4} more`));
|
|
400
|
+
}
|
|
401
|
+
console.log(
|
|
402
|
+
chalk3.gray(` Amnesty: `) + `exponential actions defer --ids ${c.actionIds.slice(0, 2).join(",")}${c.actionIds.length > 2 ? ",\u2026" : ""}`
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (t.loose.length > 0) {
|
|
407
|
+
console.log(`
|
|
408
|
+
${chalk3.bold(`${t.loose.length} individually dated`)} ${chalk3.gray("\u2014 real debt, oldest first.")}`);
|
|
409
|
+
for (const a of t.loose) {
|
|
410
|
+
const project = a.projectName ? chalk3.cyan(` [${a.projectName}]`) : "";
|
|
411
|
+
console.log(` ${chalk3.red(`${String(a.daysOverdue).padStart(3)}d`)} ${a.name.substring(0, 62)}${project}`);
|
|
412
|
+
console.log(chalk3.gray(` ID: ${a.id}`));
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
console.log();
|
|
416
|
+
}
|
|
332
417
|
function outputProjectsJson(projects) {
|
|
333
418
|
const output = {
|
|
334
419
|
projects: projects.map(transformProject),
|
|
@@ -1312,6 +1397,22 @@ var VALID_PRIORITIES = [
|
|
|
1312
1397
|
"Watch",
|
|
1313
1398
|
"Someday Maybe"
|
|
1314
1399
|
];
|
|
1400
|
+
function parseNullableDate(raw, flag) {
|
|
1401
|
+
if (raw === void 0) return void 0;
|
|
1402
|
+
if (raw === "null") return null;
|
|
1403
|
+
const parsed = new Date(raw);
|
|
1404
|
+
if (isNaN(parsed.getTime())) {
|
|
1405
|
+
throw new Error(`Invalid --${flag} value "${raw}". Use YYYY-MM-DD, an ISO datetime, or "null".`);
|
|
1406
|
+
}
|
|
1407
|
+
return parsed;
|
|
1408
|
+
}
|
|
1409
|
+
function parseIds(raw) {
|
|
1410
|
+
const ids = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1411
|
+
if (ids.length === 0) {
|
|
1412
|
+
throw new Error("No action IDs supplied. Pass --ids id1,id2");
|
|
1413
|
+
}
|
|
1414
|
+
return ids;
|
|
1415
|
+
}
|
|
1315
1416
|
function createActionsCommand() {
|
|
1316
1417
|
const actions = new Command2("actions").description("Manage actions/tasks");
|
|
1317
1418
|
actions.command("list").description("List all actions").option("--project <id>", "Filter by project ID").option("--status <status>", "Filter by kanban status (BACKLOG, TODO, IN_PROGRESS, IN_REVIEW, DONE)").option("--assignee <id>", "Filter by assignee ID").action(async (options, cmd) => {
|
|
@@ -1337,16 +1438,76 @@ function createActionsCommand() {
|
|
|
1337
1438
|
handleError(error, useJson);
|
|
1338
1439
|
}
|
|
1339
1440
|
});
|
|
1340
|
-
actions.command("today").description("
|
|
1441
|
+
actions.command("today").description("What's on your plate: overdue, today, and inbox").option("--workspace <id>", "Filter by workspace ID").option("--due-only", "Only actions whose dueDate is today (the pre-1.8 shape \u2014 excludes overdue)").action(async (options, cmd) => {
|
|
1442
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1443
|
+
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
1444
|
+
try {
|
|
1445
|
+
const client = getClient();
|
|
1446
|
+
if (options.dueOnly) {
|
|
1447
|
+
const actions2 = await client.actions.getToday(options.workspace);
|
|
1448
|
+
if (useJson) {
|
|
1449
|
+
outputActionsJson(actions2, { workspaceId: options.workspace });
|
|
1450
|
+
} else {
|
|
1451
|
+
outputActionsPretty(actions2);
|
|
1452
|
+
}
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
const todays = await client.actions.getTodaysActions(options.workspace);
|
|
1456
|
+
if (useJson) {
|
|
1457
|
+
outputTodaysActionsJson(todays, { workspaceId: options.workspace });
|
|
1458
|
+
} else {
|
|
1459
|
+
outputTodaysActionsPretty(todays);
|
|
1460
|
+
}
|
|
1461
|
+
} catch (error) {
|
|
1462
|
+
handleError(error, useJson);
|
|
1463
|
+
}
|
|
1464
|
+
});
|
|
1465
|
+
actions.command("overdue").description("Why the overdue pile is that size: bulk-created cohorts vs real debt").option("--workspace <id>", "Filter by workspace ID").action(async (options, cmd) => {
|
|
1341
1466
|
const globalOpts = cmd.optsWithGlobals();
|
|
1342
1467
|
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
1343
1468
|
try {
|
|
1344
1469
|
const client = getClient();
|
|
1345
|
-
const
|
|
1470
|
+
const triage = await client.actions.getOverdueTriage(options.workspace);
|
|
1346
1471
|
if (useJson) {
|
|
1347
|
-
|
|
1472
|
+
outputOverdueTriageJson(triage);
|
|
1348
1473
|
} else {
|
|
1349
|
-
|
|
1474
|
+
outputOverdueTriagePretty(triage);
|
|
1475
|
+
}
|
|
1476
|
+
} catch (error) {
|
|
1477
|
+
handleError(error, useJson);
|
|
1478
|
+
}
|
|
1479
|
+
});
|
|
1480
|
+
actions.command("defer").description("Amnesty: clear dates so actions fall back to their project backlog untimed").requiredOption("--ids <ids>", "Comma-separated action IDs").action(async (options, cmd) => {
|
|
1481
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1482
|
+
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
1483
|
+
try {
|
|
1484
|
+
const ids = parseIds(options.ids);
|
|
1485
|
+
const client = getClient();
|
|
1486
|
+
const result = await client.actions.bulkDefer(ids);
|
|
1487
|
+
if (useJson) {
|
|
1488
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1489
|
+
} else {
|
|
1490
|
+
console.log(`\u2713 ${result.message}`);
|
|
1491
|
+
}
|
|
1492
|
+
} catch (error) {
|
|
1493
|
+
handleError(error, useJson);
|
|
1494
|
+
}
|
|
1495
|
+
});
|
|
1496
|
+
actions.command("reschedule").description('Move actions to a new do-date (use "actions defer" if they were never really due)').requiredOption("--ids <ids>", "Comma-separated action IDs").requiredOption("--to <date>", 'New do-date (YYYY-MM-DD, or "today")').action(async (options, cmd) => {
|
|
1497
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1498
|
+
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
1499
|
+
try {
|
|
1500
|
+
const ids = parseIds(options.ids);
|
|
1501
|
+
const when = options.to === "today" ? /* @__PURE__ */ new Date() : new Date(options.to);
|
|
1502
|
+
if (isNaN(when.getTime())) {
|
|
1503
|
+
throw new Error('Invalid date format. Use YYYY-MM-DD or "today"');
|
|
1504
|
+
}
|
|
1505
|
+
const client = getClient();
|
|
1506
|
+
const result = await client.actions.bulkReschedule(ids, when);
|
|
1507
|
+
if (useJson) {
|
|
1508
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1509
|
+
} else {
|
|
1510
|
+
console.log(`\u2713 Rescheduled ${result.count} action${result.count === 1 ? "" : "s"}`);
|
|
1350
1511
|
}
|
|
1351
1512
|
} catch (error) {
|
|
1352
1513
|
handleError(error, useJson);
|
|
@@ -1395,7 +1556,7 @@ function createActionsCommand() {
|
|
|
1395
1556
|
handleError(error, useJson);
|
|
1396
1557
|
}
|
|
1397
1558
|
});
|
|
1398
|
-
actions.command("update").description("Update an existing action").requiredOption("--id <id>", "Action ID to update").option("-n, --name <name>", "New action name").option("-d, --description <text>", "New description").option("-p, --project <id>", "Move to project ID").option("--priority <priority>", "Priority (Quick, Scheduled, 1st Priority, etc.)").option("--status <status>", "Status (ACTIVE, COMPLETED, CANCELLED)").option("--kanban <status>", "Kanban status (BACKLOG, TODO, IN_PROGRESS, IN_REVIEW, DONE)").option("--due <date>", 'Due date (YYYY-MM-DD or "null" to clear)').action(async (options, cmd) => {
|
|
1559
|
+
actions.command("update").description("Update an existing action").requiredOption("--id <id>", "Action ID to update").option("-n, --name <name>", "New action name").option("-d, --description <text>", "New description").option("-p, --project <id>", "Move to project ID").option("--priority <priority>", "Priority (Quick, Scheduled, 1st Priority, etc.)").option("--status <status>", "Status (ACTIVE, COMPLETED, CANCELLED)").option("--kanban <status>", "Kanban status (BACKLOG, TODO, IN_PROGRESS, IN_REVIEW, DONE)").option("--due <date>", 'Due date (YYYY-MM-DD or "null" to clear)').option("--scheduled-start <datetime>", 'Do-date: when you plan to work on it (YYYY-MM-DD, ISO datetime, or "null" to clear). This is what /today partitions on.').option("--scheduled-end <datetime>", 'End of the time block (YYYY-MM-DD, ISO datetime, or "null" to clear)').action(async (options, cmd) => {
|
|
1399
1560
|
const globalOpts = cmd.optsWithGlobals();
|
|
1400
1561
|
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
1401
1562
|
try {
|
|
@@ -1411,6 +1572,8 @@ function createActionsCommand() {
|
|
|
1411
1572
|
throw new Error("Invalid due date format. Use YYYY-MM-DD");
|
|
1412
1573
|
}
|
|
1413
1574
|
}
|
|
1575
|
+
const scheduledStart = parseNullableDate(options.scheduledStart, "scheduled-start");
|
|
1576
|
+
const scheduledEnd = parseNullableDate(options.scheduledEnd, "scheduled-end");
|
|
1414
1577
|
const client = getClient();
|
|
1415
1578
|
const action = await client.actions.update({
|
|
1416
1579
|
id: options.id,
|
|
@@ -1420,7 +1583,9 @@ function createActionsCommand() {
|
|
|
1420
1583
|
priority: options.priority,
|
|
1421
1584
|
status: options.status,
|
|
1422
1585
|
kanbanStatus: options.kanban,
|
|
1423
|
-
dueDate
|
|
1586
|
+
dueDate,
|
|
1587
|
+
scheduledStart,
|
|
1588
|
+
scheduledEnd
|
|
1424
1589
|
});
|
|
1425
1590
|
if (useJson) {
|
|
1426
1591
|
outputActionJson(action);
|
|
@@ -2034,7 +2199,7 @@ function createEpicsCommand() {
|
|
|
2034
2199
|
const globalOpts = cmd.optsWithGlobals();
|
|
2035
2200
|
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
2036
2201
|
try {
|
|
2037
|
-
const
|
|
2202
|
+
const parseNullableDate2 = (v) => {
|
|
2038
2203
|
if (v === void 0) return void 0;
|
|
2039
2204
|
if (v === "null") return null;
|
|
2040
2205
|
const d = new Date(v);
|
|
@@ -2053,8 +2218,8 @@ function createEpicsCommand() {
|
|
|
2053
2218
|
description,
|
|
2054
2219
|
status,
|
|
2055
2220
|
priority,
|
|
2056
|
-
startDate:
|
|
2057
|
-
targetDate:
|
|
2221
|
+
startDate: parseNullableDate2(options.start),
|
|
2222
|
+
targetDate: parseNullableDate2(options.target)
|
|
2058
2223
|
});
|
|
2059
2224
|
if (useJson) outputEpicJson(epic);
|
|
2060
2225
|
else {
|
|
@@ -2500,6 +2665,17 @@ var FEATURE_STATUSES = [
|
|
|
2500
2665
|
"DEPRECATED",
|
|
2501
2666
|
"ARCHIVED"
|
|
2502
2667
|
];
|
|
2668
|
+
function summarizeTicket(ticket) {
|
|
2669
|
+
return {
|
|
2670
|
+
id: ticket.id,
|
|
2671
|
+
shortId: ticket.shortId ?? null,
|
|
2672
|
+
number: ticket.number ?? null,
|
|
2673
|
+
title: ticket.title
|
|
2674
|
+
};
|
|
2675
|
+
}
|
|
2676
|
+
function ticketLabel(ticket) {
|
|
2677
|
+
return ticket.shortId ?? (ticket.number != null ? `#${ticket.number}` : ticket.id);
|
|
2678
|
+
}
|
|
2503
2679
|
function validateFeatureStatus(value) {
|
|
2504
2680
|
if (!value) return void 0;
|
|
2505
2681
|
if (!FEATURE_STATUSES.includes(value)) {
|
|
@@ -2606,6 +2782,119 @@ function createFeaturesCommand() {
|
|
|
2606
2782
|
}
|
|
2607
2783
|
}
|
|
2608
2784
|
);
|
|
2785
|
+
features.command("delete <id>").description(
|
|
2786
|
+
"Delete a feature. Refuses if it still has tickets \u2014 deleting a feature does not delete its tickets, it unlinks them (Ticket.featureId is SetNull), silently orphaning them in the product backlog."
|
|
2787
|
+
).option(
|
|
2788
|
+
"--with-tickets",
|
|
2789
|
+
"Delete the feature's tickets first, then the feature (destructive)"
|
|
2790
|
+
).option(
|
|
2791
|
+
"--orphan-tickets",
|
|
2792
|
+
"Delete the feature anyway, leaving its tickets in the backlog unlinked"
|
|
2793
|
+
).action(
|
|
2794
|
+
async (id, options, cmd) => {
|
|
2795
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
2796
|
+
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
2797
|
+
try {
|
|
2798
|
+
if (options.withTickets && options.orphanTickets) {
|
|
2799
|
+
throw new Error(
|
|
2800
|
+
"--with-tickets and --orphan-tickets are mutually exclusive"
|
|
2801
|
+
);
|
|
2802
|
+
}
|
|
2803
|
+
const client = getClient();
|
|
2804
|
+
const feature = await client.features.get(id);
|
|
2805
|
+
const tickets = feature._count?.tickets === 0 ? [] : await client.tickets.list({
|
|
2806
|
+
productId: feature.productId,
|
|
2807
|
+
featureId: feature.id
|
|
2808
|
+
});
|
|
2809
|
+
if (tickets.length > 0 && !options.withTickets && !options.orphanTickets) {
|
|
2810
|
+
const reason = `has ${tickets.length} ticket${tickets.length === 1 ? "" : "s"}`;
|
|
2811
|
+
if (useJson) {
|
|
2812
|
+
console.log(
|
|
2813
|
+
JSON.stringify(
|
|
2814
|
+
{
|
|
2815
|
+
deleted: false,
|
|
2816
|
+
id: feature.id,
|
|
2817
|
+
reason,
|
|
2818
|
+
tickets: tickets.map(summarizeTicket)
|
|
2819
|
+
},
|
|
2820
|
+
null,
|
|
2821
|
+
2
|
|
2822
|
+
)
|
|
2823
|
+
);
|
|
2824
|
+
} else {
|
|
2825
|
+
console.error(
|
|
2826
|
+
`
|
|
2827
|
+
Refusing to delete "${feature.name}": it ${reason}.
|
|
2828
|
+
Deleting the feature would leave them unlinked in the product backlog.
|
|
2829
|
+
--with-tickets delete those tickets first, then the feature
|
|
2830
|
+
--orphan-tickets delete the feature anyway and unlink them`
|
|
2831
|
+
);
|
|
2832
|
+
for (const t of tickets) {
|
|
2833
|
+
console.error(` - ${ticketLabel(t)} ${t.title}`);
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
process.exitCode = 1;
|
|
2837
|
+
return;
|
|
2838
|
+
}
|
|
2839
|
+
const ticketsDeleted = [];
|
|
2840
|
+
const ticketsFailed = [];
|
|
2841
|
+
if (options.withTickets) {
|
|
2842
|
+
for (const ticket of tickets) {
|
|
2843
|
+
try {
|
|
2844
|
+
await client.tickets.delete(ticket.id);
|
|
2845
|
+
ticketsDeleted.push(summarizeTicket(ticket));
|
|
2846
|
+
} catch (error) {
|
|
2847
|
+
ticketsFailed.push({
|
|
2848
|
+
...summarizeTicket(ticket),
|
|
2849
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2850
|
+
});
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
if (ticketsFailed.length > 0) {
|
|
2855
|
+
const reason = `failed to delete ${ticketsFailed.length} of ${tickets.length} tickets`;
|
|
2856
|
+
if (useJson) {
|
|
2857
|
+
console.log(
|
|
2858
|
+
JSON.stringify(
|
|
2859
|
+
{ deleted: false, id: feature.id, reason, ticketsDeleted, ticketsFailed },
|
|
2860
|
+
null,
|
|
2861
|
+
2
|
|
2862
|
+
)
|
|
2863
|
+
);
|
|
2864
|
+
} else {
|
|
2865
|
+
console.error(`
|
|
2866
|
+
Aborted: ${reason}. Feature "${feature.name}" was NOT deleted.`);
|
|
2867
|
+
for (const t of ticketsFailed) {
|
|
2868
|
+
console.error(` - ${t.shortId ?? t.id}: ${t.error}`);
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
process.exitCode = 1;
|
|
2872
|
+
return;
|
|
2873
|
+
}
|
|
2874
|
+
await client.features.delete(feature.id);
|
|
2875
|
+
const ticketsOrphaned = options.orphanTickets ? tickets.map(summarizeTicket) : [];
|
|
2876
|
+
if (useJson) {
|
|
2877
|
+
console.log(
|
|
2878
|
+
JSON.stringify(
|
|
2879
|
+
{ deleted: true, id: feature.id, ticketsDeleted, ticketsOrphaned },
|
|
2880
|
+
null,
|
|
2881
|
+
2
|
|
2882
|
+
)
|
|
2883
|
+
);
|
|
2884
|
+
} else {
|
|
2885
|
+
console.log("\n\u2713 Feature deleted");
|
|
2886
|
+
if (ticketsDeleted.length > 0) {
|
|
2887
|
+
console.log(` ${ticketsDeleted.length} ticket(s) deleted with it`);
|
|
2888
|
+
}
|
|
2889
|
+
if (ticketsOrphaned.length > 0) {
|
|
2890
|
+
console.log(` ${ticketsOrphaned.length} ticket(s) left unlinked in the backlog`);
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
} catch (error) {
|
|
2894
|
+
handleError(error, useJson);
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
);
|
|
2609
2898
|
features.command("link-page").description("Link a Knowledge page (PRD, spec, research) to a feature").requiredOption("--feature <id>", "Feature CUID").requiredOption("--page <id>", "Page CUID").option("--scope <id>", "Feature scope CUID to pin the page to").action(
|
|
2610
2899
|
async (options, cmd) => {
|
|
2611
2900
|
const globalOpts = cmd.optsWithGlobals();
|
|
@@ -3064,8 +3353,86 @@ function createProjectsCommand() {
|
|
|
3064
3353
|
return projects;
|
|
3065
3354
|
}
|
|
3066
3355
|
|
|
3067
|
-
// src/commands/
|
|
3356
|
+
// src/commands/search.ts
|
|
3068
3357
|
import { Command as Command16 } from "commander";
|
|
3358
|
+
import chalk6 from "chalk";
|
|
3359
|
+
var TYPE_LABELS = {
|
|
3360
|
+
workspace: "Workspaces",
|
|
3361
|
+
project: "Projects",
|
|
3362
|
+
action: "Actions",
|
|
3363
|
+
goal: "Goals",
|
|
3364
|
+
keyResult: "Key Results",
|
|
3365
|
+
outcome: "Outcomes",
|
|
3366
|
+
ticket: "Tickets",
|
|
3367
|
+
feature: "Features",
|
|
3368
|
+
epic: "Epics",
|
|
3369
|
+
page: "Pages",
|
|
3370
|
+
meeting: "Meetings",
|
|
3371
|
+
contact: "Contacts",
|
|
3372
|
+
organization: "Organizations",
|
|
3373
|
+
product: "Products"
|
|
3374
|
+
};
|
|
3375
|
+
function createSearchCommand() {
|
|
3376
|
+
const search = new Command16("search").description(
|
|
3377
|
+
"Global search across projects, actions, goals, workspaces and more \u2014 same coverage as the app's Cmd+K palette"
|
|
3378
|
+
).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
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
3380
|
+
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
3381
|
+
try {
|
|
3382
|
+
const client = getClient();
|
|
3383
|
+
let workspaceId;
|
|
3384
|
+
if (options.workspace) {
|
|
3385
|
+
const workspaces = await client.workspaces.list();
|
|
3386
|
+
const match = workspaces.find(
|
|
3387
|
+
(w) => w.slug === options.workspace || w.id === options.workspace
|
|
3388
|
+
);
|
|
3389
|
+
if (!match) {
|
|
3390
|
+
console.error(chalk6.red(`Workspace "${options.workspace}" not found.`));
|
|
3391
|
+
console.log(chalk6.gray("Available workspaces:"));
|
|
3392
|
+
workspaces.forEach((w) => {
|
|
3393
|
+
console.log(chalk6.gray(` - ${w.slug} (${w.name})`));
|
|
3394
|
+
});
|
|
3395
|
+
process.exit(1);
|
|
3396
|
+
}
|
|
3397
|
+
workspaceId = match.id;
|
|
3398
|
+
}
|
|
3399
|
+
const limit = options.limit ? Number(options.limit) : void 0;
|
|
3400
|
+
const result = await client.search.global({ query, workspaceId, limit });
|
|
3401
|
+
if (useJson) {
|
|
3402
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3403
|
+
return;
|
|
3404
|
+
}
|
|
3405
|
+
if (result.results.length === 0) {
|
|
3406
|
+
console.log(chalk6.gray(`No matches for "${result.query}".`));
|
|
3407
|
+
return;
|
|
3408
|
+
}
|
|
3409
|
+
const byType = /* @__PURE__ */ new Map();
|
|
3410
|
+
for (const r of result.results) {
|
|
3411
|
+
const bucket = byType.get(r.type) ?? [];
|
|
3412
|
+
bucket.push(r);
|
|
3413
|
+
byType.set(r.type, bucket);
|
|
3414
|
+
}
|
|
3415
|
+
for (const [type, rows] of byType) {
|
|
3416
|
+
console.log(chalk6.bold(`
|
|
3417
|
+
${TYPE_LABELS[type] ?? type}`));
|
|
3418
|
+
for (const row of rows) {
|
|
3419
|
+
const parts = [chalk6.white(row.title)];
|
|
3420
|
+
if (row.subtitle) parts.push(chalk6.gray(row.subtitle));
|
|
3421
|
+
if (row.workspace) parts.push(chalk6.cyan(`@${row.workspace.slug}`));
|
|
3422
|
+
console.log(` ${parts.join(chalk6.gray(" \xB7 "))}`);
|
|
3423
|
+
console.log(chalk6.gray(` ${row.id}${row.url ? ` ${row.url}` : ""}`));
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
console.log();
|
|
3427
|
+
} catch (error) {
|
|
3428
|
+
handleError(error, useJson);
|
|
3429
|
+
}
|
|
3430
|
+
});
|
|
3431
|
+
return search;
|
|
3432
|
+
}
|
|
3433
|
+
|
|
3434
|
+
// src/commands/tickets.ts
|
|
3435
|
+
import { Command as Command17 } from "commander";
|
|
3069
3436
|
function collectRepeatable(value, previous) {
|
|
3070
3437
|
return [...previous, value];
|
|
3071
3438
|
}
|
|
@@ -3108,7 +3475,7 @@ function validateTicketType(value) {
|
|
|
3108
3475
|
return value;
|
|
3109
3476
|
}
|
|
3110
3477
|
function createTicketsCommand() {
|
|
3111
|
-
const tickets = new
|
|
3478
|
+
const tickets = new Command17("tickets").description(
|
|
3112
3479
|
"Manage product backlog tickets. Tickets live under a product, link to features/epics, depend on each other, and can be implemented via actions."
|
|
3113
3480
|
);
|
|
3114
3481
|
tickets.command("list").description(
|
|
@@ -3422,7 +3789,7 @@ function createTicketsCommand() {
|
|
|
3422
3789
|
handleError(error, useJson);
|
|
3423
3790
|
}
|
|
3424
3791
|
});
|
|
3425
|
-
const comment = new
|
|
3792
|
+
const comment = new Command17("comment").description("Manage comments on a ticket");
|
|
3426
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) => {
|
|
3427
3794
|
const globalOpts = cmd.optsWithGlobals();
|
|
3428
3795
|
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
@@ -3461,10 +3828,10 @@ function createTicketsCommand() {
|
|
|
3461
3828
|
}
|
|
3462
3829
|
|
|
3463
3830
|
// src/commands/workspaces.ts
|
|
3464
|
-
import { Command as
|
|
3465
|
-
import
|
|
3831
|
+
import { Command as Command18 } from "commander";
|
|
3832
|
+
import chalk7 from "chalk";
|
|
3466
3833
|
function createWorkspacesCommand() {
|
|
3467
|
-
const workspaces = new
|
|
3834
|
+
const workspaces = new Command18("workspaces").description("Manage workspaces");
|
|
3468
3835
|
workspaces.command("list").description("List all workspaces").action(async (_options, cmd) => {
|
|
3469
3836
|
const globalOpts = cmd.optsWithGlobals();
|
|
3470
3837
|
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
@@ -3477,7 +3844,7 @@ function createWorkspacesCommand() {
|
|
|
3477
3844
|
const config = getConfig();
|
|
3478
3845
|
outputWorkspacesPretty(workspaces2);
|
|
3479
3846
|
if (config.defaultWorkspaceSlug) {
|
|
3480
|
-
console.log(
|
|
3847
|
+
console.log(chalk7.gray(`Default workspace: ${config.defaultWorkspaceSlug}`));
|
|
3481
3848
|
}
|
|
3482
3849
|
}
|
|
3483
3850
|
} catch (error) {
|
|
@@ -3490,10 +3857,10 @@ function createWorkspacesCommand() {
|
|
|
3490
3857
|
const workspaces2 = await client.workspaces.list();
|
|
3491
3858
|
const workspace = workspaces2.find((w) => w.slug === slug);
|
|
3492
3859
|
if (!workspace) {
|
|
3493
|
-
console.error(
|
|
3494
|
-
console.log(
|
|
3860
|
+
console.error(chalk7.red(`Workspace with slug "${slug}" not found.`));
|
|
3861
|
+
console.log(chalk7.gray("Available workspaces:"));
|
|
3495
3862
|
workspaces2.forEach((w) => {
|
|
3496
|
-
console.log(
|
|
3863
|
+
console.log(chalk7.gray(` - ${w.slug} (${w.name})`));
|
|
3497
3864
|
});
|
|
3498
3865
|
process.exit(1);
|
|
3499
3866
|
}
|
|
@@ -3501,7 +3868,7 @@ function createWorkspacesCommand() {
|
|
|
3501
3868
|
defaultWorkspaceId: workspace.id,
|
|
3502
3869
|
defaultWorkspaceSlug: workspace.slug
|
|
3503
3870
|
});
|
|
3504
|
-
console.log(
|
|
3871
|
+
console.log(chalk7.green(`Default workspace set to: ${workspace.name} (${workspace.slug})`));
|
|
3505
3872
|
} catch (error) {
|
|
3506
3873
|
handleError(error);
|
|
3507
3874
|
}
|
|
@@ -3510,7 +3877,7 @@ function createWorkspacesCommand() {
|
|
|
3510
3877
|
}
|
|
3511
3878
|
|
|
3512
3879
|
// src/index.ts
|
|
3513
|
-
var program = new
|
|
3880
|
+
var program = new Command19();
|
|
3514
3881
|
program.name("exponential").description(
|
|
3515
3882
|
[
|
|
3516
3883
|
"CLI to interact with Exponential productivity app.",
|
|
@@ -3531,6 +3898,7 @@ program.addCommand(createOrganizationsCommand());
|
|
|
3531
3898
|
program.addCommand(createPagesCommand());
|
|
3532
3899
|
program.addCommand(createProductsCommand());
|
|
3533
3900
|
program.addCommand(createProjectsCommand());
|
|
3901
|
+
program.addCommand(createSearchCommand());
|
|
3534
3902
|
program.addCommand(createTicketsCommand());
|
|
3535
3903
|
program.addCommand(createWorkspacesCommand());
|
|
3536
3904
|
program.parse();
|