exponential-cli 1.8.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 +298 -9
- 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
|
@@ -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) => {
|
|
1341
1442
|
const globalOpts = cmd.optsWithGlobals();
|
|
1342
1443
|
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
1343
1444
|
try {
|
|
1344
1445
|
const client = getClient();
|
|
1345
|
-
|
|
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);
|
|
1346
1456
|
if (useJson) {
|
|
1347
|
-
|
|
1457
|
+
outputTodaysActionsJson(todays, { workspaceId: options.workspace });
|
|
1348
1458
|
} else {
|
|
1349
|
-
|
|
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) => {
|
|
1466
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1467
|
+
const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
|
|
1468
|
+
try {
|
|
1469
|
+
const client = getClient();
|
|
1470
|
+
const triage = await client.actions.getOverdueTriage(options.workspace);
|
|
1471
|
+
if (useJson) {
|
|
1472
|
+
outputOverdueTriageJson(triage);
|
|
1473
|
+
} else {
|
|
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();
|