@markmdev/pebble 0.1.16 → 0.1.17
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/cli/index.js +471 -7
- package/dist/cli/index.js.map +1 -1
- package/dist/ui/assets/index-CDNXn0_Q.css +1 -0
- package/dist/ui/assets/index-CFM4dSQb.js +332 -0
- package/dist/ui/index.html +2 -2
- package/package.json +2 -2
- package/dist/ui/assets/index-BeE0zkRT.css +0 -1
- package/dist/ui/assets/index-Dfn4BuYf.js +0 -332
package/dist/cli/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import { dirname as dirname2, join as join3 } from "path";
|
|
|
16
16
|
var ISSUE_TYPES = ["task", "bug", "epic", "verification"];
|
|
17
17
|
var PRIORITIES = [0, 1, 2, 3, 4];
|
|
18
18
|
var STATUSES = ["open", "in_progress", "blocked", "pending_verification", "closed"];
|
|
19
|
-
var EVENT_TYPES = ["create", "update", "close", "reopen", "comment"];
|
|
19
|
+
var EVENT_TYPES = ["create", "update", "close", "reopen", "comment", "delete", "restore"];
|
|
20
20
|
var PRIORITY_LABELS = {
|
|
21
21
|
0: "critical",
|
|
22
22
|
1: "high",
|
|
@@ -360,14 +360,37 @@ function computeState(events) {
|
|
|
360
360
|
}
|
|
361
361
|
break;
|
|
362
362
|
}
|
|
363
|
+
case "delete": {
|
|
364
|
+
const issue = issues.get(event.issueId);
|
|
365
|
+
if (issue) {
|
|
366
|
+
issue.deleted = true;
|
|
367
|
+
issue.deletedAt = event.timestamp;
|
|
368
|
+
issue.updatedAt = event.timestamp;
|
|
369
|
+
if (event.source) issue.lastSource = event.source;
|
|
370
|
+
}
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
case "restore": {
|
|
374
|
+
const issue = issues.get(event.issueId);
|
|
375
|
+
if (issue) {
|
|
376
|
+
issue.deleted = false;
|
|
377
|
+
issue.deletedAt = void 0;
|
|
378
|
+
issue.updatedAt = event.timestamp;
|
|
379
|
+
if (event.source) issue.lastSource = event.source;
|
|
380
|
+
}
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
363
383
|
}
|
|
364
384
|
}
|
|
365
385
|
return issues;
|
|
366
386
|
}
|
|
367
|
-
function getIssues(filters) {
|
|
387
|
+
function getIssues(filters, includeDeleted = false) {
|
|
368
388
|
const events = readEvents();
|
|
369
389
|
const state = computeState(events);
|
|
370
390
|
let issues = Array.from(state.values());
|
|
391
|
+
if (!includeDeleted) {
|
|
392
|
+
issues = issues.filter((i) => !i.deleted);
|
|
393
|
+
}
|
|
371
394
|
if (filters) {
|
|
372
395
|
if (filters.status !== void 0) {
|
|
373
396
|
issues = issues.filter((i) => i.status === filters.status);
|
|
@@ -384,10 +407,14 @@ function getIssues(filters) {
|
|
|
384
407
|
}
|
|
385
408
|
return issues;
|
|
386
409
|
}
|
|
387
|
-
function getIssue(id) {
|
|
410
|
+
function getIssue(id, includeDeleted = false) {
|
|
388
411
|
const events = readEvents();
|
|
389
412
|
const state = computeState(events);
|
|
390
|
-
|
|
413
|
+
const issue = state.get(id);
|
|
414
|
+
if (issue && issue.deleted && !includeDeleted) {
|
|
415
|
+
return void 0;
|
|
416
|
+
}
|
|
417
|
+
return issue;
|
|
391
418
|
}
|
|
392
419
|
function resolveId(partial) {
|
|
393
420
|
const events = readEvents();
|
|
@@ -430,6 +457,9 @@ function getReady() {
|
|
|
430
457
|
const state = computeState(events);
|
|
431
458
|
const issues = Array.from(state.values());
|
|
432
459
|
return issues.filter((issue) => {
|
|
460
|
+
if (issue.deleted) {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
433
463
|
if (issue.status === "closed" || issue.status === "pending_verification") {
|
|
434
464
|
return false;
|
|
435
465
|
}
|
|
@@ -453,6 +483,9 @@ function getBlocked() {
|
|
|
453
483
|
const state = computeState(events);
|
|
454
484
|
const issues = Array.from(state.values());
|
|
455
485
|
return issues.filter((issue) => {
|
|
486
|
+
if (issue.deleted) {
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
456
489
|
if (issue.status === "closed") {
|
|
457
490
|
return false;
|
|
458
491
|
}
|
|
@@ -616,6 +649,20 @@ function getAncestryChain(issueId, state) {
|
|
|
616
649
|
}
|
|
617
650
|
return chain;
|
|
618
651
|
}
|
|
652
|
+
function getDescendants(issueId, state) {
|
|
653
|
+
const issueState = state ?? getComputedState();
|
|
654
|
+
const descendants = [];
|
|
655
|
+
function collectDescendants(parentId) {
|
|
656
|
+
for (const issue of issueState.values()) {
|
|
657
|
+
if (issue.parent === parentId && !issue.deleted) {
|
|
658
|
+
descendants.push(issue);
|
|
659
|
+
collectDescendants(issue.id);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
collectDescendants(issueId);
|
|
664
|
+
return descendants;
|
|
665
|
+
}
|
|
619
666
|
|
|
620
667
|
// src/shared/time.ts
|
|
621
668
|
import { formatDistanceToNow, parseISO } from "date-fns";
|
|
@@ -931,7 +978,7 @@ function formatIssueListVerbose(issues, sectionHeader) {
|
|
|
931
978
|
lines.push(`${issue.id}: ${issue.title}`);
|
|
932
979
|
lines.push(` Type: ${formatType(issue.type)} | Priority: P${issue.priority} | Created: ${formatRelativeTime(issue.createdAt)}`);
|
|
933
980
|
if (ancestry.length > 0) {
|
|
934
|
-
const chain = [...ancestry].reverse().map((a) => a.
|
|
981
|
+
const chain = [...ancestry].reverse().map((a) => a.title).join(" \u2192 ");
|
|
935
982
|
lines.push(` Ancestry: ${chain}`);
|
|
936
983
|
}
|
|
937
984
|
if (blocking.length > 0) {
|
|
@@ -1412,6 +1459,220 @@ function reopenCommand(program2) {
|
|
|
1412
1459
|
});
|
|
1413
1460
|
}
|
|
1414
1461
|
|
|
1462
|
+
// src/cli/commands/delete.ts
|
|
1463
|
+
function collectReferenceCleanup(deletedId, deletedIds, state, updates) {
|
|
1464
|
+
for (const [id, issue] of state) {
|
|
1465
|
+
if (deletedIds.has(id)) continue;
|
|
1466
|
+
if (issue.deleted) continue;
|
|
1467
|
+
let entry = updates.get(id);
|
|
1468
|
+
const currentBlockedBy = entry?.blockedBy ?? issue.blockedBy;
|
|
1469
|
+
if (currentBlockedBy.includes(deletedId)) {
|
|
1470
|
+
if (!entry) {
|
|
1471
|
+
entry = {};
|
|
1472
|
+
updates.set(id, entry);
|
|
1473
|
+
}
|
|
1474
|
+
entry.blockedBy = currentBlockedBy.filter((bid) => bid !== deletedId);
|
|
1475
|
+
}
|
|
1476
|
+
const currentRelatedTo = entry?.relatedTo ?? issue.relatedTo;
|
|
1477
|
+
if (currentRelatedTo.includes(deletedId)) {
|
|
1478
|
+
if (!entry) {
|
|
1479
|
+
entry = {};
|
|
1480
|
+
updates.set(id, entry);
|
|
1481
|
+
}
|
|
1482
|
+
entry.relatedTo = currentRelatedTo.filter((rid) => rid !== deletedId);
|
|
1483
|
+
}
|
|
1484
|
+
if (issue.parent === deletedId) {
|
|
1485
|
+
if (!entry) {
|
|
1486
|
+
entry = {};
|
|
1487
|
+
updates.set(id, entry);
|
|
1488
|
+
}
|
|
1489
|
+
entry.parent = "";
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
function emitReferenceCleanup(updates, pebbleDir, timestamp) {
|
|
1494
|
+
for (const [id, data] of updates) {
|
|
1495
|
+
if (Object.keys(data).length > 0) {
|
|
1496
|
+
const updateEvent = {
|
|
1497
|
+
type: "update",
|
|
1498
|
+
issueId: id,
|
|
1499
|
+
timestamp,
|
|
1500
|
+
data
|
|
1501
|
+
};
|
|
1502
|
+
appendEvent(updateEvent, pebbleDir);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
function deleteCommand(program2) {
|
|
1507
|
+
program2.command("delete <ids...>").description("Delete issues (soft delete). Epics cascade-delete their children.").option("-r, --reason <reason>", "Reason for deleting").action(async (ids, options) => {
|
|
1508
|
+
const pretty = program2.opts().pretty ?? false;
|
|
1509
|
+
try {
|
|
1510
|
+
const pebbleDir = getOrCreatePebbleDir();
|
|
1511
|
+
const state = getComputedState();
|
|
1512
|
+
const allIds = ids.flatMap((id) => id.split(",").map((s) => s.trim()).filter(Boolean));
|
|
1513
|
+
if (allIds.length === 0) {
|
|
1514
|
+
throw new Error("No issue IDs provided");
|
|
1515
|
+
}
|
|
1516
|
+
const results = [];
|
|
1517
|
+
const toDelete = [];
|
|
1518
|
+
const alreadyQueued = /* @__PURE__ */ new Set();
|
|
1519
|
+
for (const id of allIds) {
|
|
1520
|
+
try {
|
|
1521
|
+
const resolvedId = resolveId(id);
|
|
1522
|
+
const issue = state.get(resolvedId);
|
|
1523
|
+
if (!issue) {
|
|
1524
|
+
results.push({ id, success: false, error: `Issue not found: ${id}` });
|
|
1525
|
+
continue;
|
|
1526
|
+
}
|
|
1527
|
+
if (issue.deleted) {
|
|
1528
|
+
results.push({ id: resolvedId, success: false, error: `Issue is already deleted: ${resolvedId}` });
|
|
1529
|
+
continue;
|
|
1530
|
+
}
|
|
1531
|
+
if (!alreadyQueued.has(resolvedId)) {
|
|
1532
|
+
toDelete.push({ id: resolvedId, cascade: false });
|
|
1533
|
+
alreadyQueued.add(resolvedId);
|
|
1534
|
+
}
|
|
1535
|
+
const descendants = getDescendants(resolvedId, state);
|
|
1536
|
+
for (const desc of descendants) {
|
|
1537
|
+
if (!alreadyQueued.has(desc.id) && !desc.deleted) {
|
|
1538
|
+
toDelete.push({ id: desc.id, cascade: true });
|
|
1539
|
+
alreadyQueued.add(desc.id);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
const verifications = getVerifications(resolvedId);
|
|
1543
|
+
for (const v of verifications) {
|
|
1544
|
+
if (!alreadyQueued.has(v.id) && !v.deleted) {
|
|
1545
|
+
toDelete.push({ id: v.id, cascade: true });
|
|
1546
|
+
alreadyQueued.add(v.id);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
} catch (error) {
|
|
1550
|
+
results.push({ id, success: false, error: error.message });
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1554
|
+
const deletedIds = new Set(toDelete.map((d) => d.id));
|
|
1555
|
+
const referenceUpdates = /* @__PURE__ */ new Map();
|
|
1556
|
+
for (const { id } of toDelete) {
|
|
1557
|
+
collectReferenceCleanup(id, deletedIds, state, referenceUpdates);
|
|
1558
|
+
}
|
|
1559
|
+
emitReferenceCleanup(referenceUpdates, pebbleDir, timestamp);
|
|
1560
|
+
for (const { id, cascade } of toDelete) {
|
|
1561
|
+
const issue = state.get(id);
|
|
1562
|
+
if (!issue) continue;
|
|
1563
|
+
const deleteEvent = {
|
|
1564
|
+
type: "delete",
|
|
1565
|
+
issueId: id,
|
|
1566
|
+
timestamp,
|
|
1567
|
+
data: {
|
|
1568
|
+
reason: options.reason,
|
|
1569
|
+
cascade: cascade || void 0,
|
|
1570
|
+
previousStatus: issue.status
|
|
1571
|
+
}
|
|
1572
|
+
};
|
|
1573
|
+
appendEvent(deleteEvent, pebbleDir);
|
|
1574
|
+
results.push({ id, success: true, cascade: cascade || void 0 });
|
|
1575
|
+
}
|
|
1576
|
+
if (pretty) {
|
|
1577
|
+
const primary = results.filter((r) => r.success && !r.cascade);
|
|
1578
|
+
const cascaded = results.filter((r) => r.success && r.cascade);
|
|
1579
|
+
const failed = results.filter((r) => !r.success);
|
|
1580
|
+
for (const result of primary) {
|
|
1581
|
+
console.log(`\u{1F5D1}\uFE0F ${result.id} deleted`);
|
|
1582
|
+
}
|
|
1583
|
+
for (const result of cascaded) {
|
|
1584
|
+
console.log(` \u2514\u2500 ${result.id} deleted (cascade)`);
|
|
1585
|
+
}
|
|
1586
|
+
for (const result of failed) {
|
|
1587
|
+
console.log(`\u2717 ${result.id}: ${result.error}`);
|
|
1588
|
+
}
|
|
1589
|
+
} else {
|
|
1590
|
+
console.log(formatJson({
|
|
1591
|
+
deleted: results.filter((r) => r.success).map((r) => ({ id: r.id, cascade: r.cascade ?? false })),
|
|
1592
|
+
...results.some((r) => !r.success) && {
|
|
1593
|
+
errors: results.filter((r) => !r.success).map((r) => ({ id: r.id, error: r.error }))
|
|
1594
|
+
}
|
|
1595
|
+
}));
|
|
1596
|
+
}
|
|
1597
|
+
} catch (error) {
|
|
1598
|
+
outputError(error, pretty);
|
|
1599
|
+
}
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
// src/cli/commands/restore.ts
|
|
1604
|
+
function restoreCommand(program2) {
|
|
1605
|
+
program2.command("restore <ids...>").description("Restore deleted issues").option("-r, --reason <reason>", "Reason for restoring").action(async (ids, options) => {
|
|
1606
|
+
const pretty = program2.opts().pretty ?? false;
|
|
1607
|
+
try {
|
|
1608
|
+
const pebbleDir = getOrCreatePebbleDir();
|
|
1609
|
+
const allIds = ids.flatMap((id) => id.split(",").map((s) => s.trim()).filter(Boolean));
|
|
1610
|
+
if (allIds.length === 0) {
|
|
1611
|
+
throw new Error("No issue IDs provided");
|
|
1612
|
+
}
|
|
1613
|
+
const results = [];
|
|
1614
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1615
|
+
for (const id of allIds) {
|
|
1616
|
+
try {
|
|
1617
|
+
const resolvedId = resolveId(id);
|
|
1618
|
+
const issue = getIssue(resolvedId, true);
|
|
1619
|
+
if (!issue) {
|
|
1620
|
+
results.push({ id, success: false, error: `Issue not found: ${id}` });
|
|
1621
|
+
continue;
|
|
1622
|
+
}
|
|
1623
|
+
if (!issue.deleted) {
|
|
1624
|
+
results.push({ id: resolvedId, success: false, error: `Issue is not deleted: ${resolvedId}` });
|
|
1625
|
+
continue;
|
|
1626
|
+
}
|
|
1627
|
+
const restoreEvent = {
|
|
1628
|
+
type: "restore",
|
|
1629
|
+
issueId: resolvedId,
|
|
1630
|
+
timestamp,
|
|
1631
|
+
data: {
|
|
1632
|
+
reason: options.reason
|
|
1633
|
+
}
|
|
1634
|
+
};
|
|
1635
|
+
appendEvent(restoreEvent, pebbleDir);
|
|
1636
|
+
results.push({ id: resolvedId, success: true });
|
|
1637
|
+
} catch (error) {
|
|
1638
|
+
results.push({ id, success: false, error: error.message });
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
if (allIds.length === 1) {
|
|
1642
|
+
const result = results[0];
|
|
1643
|
+
if (result.success) {
|
|
1644
|
+
if (pretty) {
|
|
1645
|
+
console.log(`\u21A9\uFE0F ${result.id} restored`);
|
|
1646
|
+
} else {
|
|
1647
|
+
console.log(formatJson({ id: result.id, success: true }));
|
|
1648
|
+
}
|
|
1649
|
+
} else {
|
|
1650
|
+
throw new Error(result.error || "Unknown error");
|
|
1651
|
+
}
|
|
1652
|
+
} else {
|
|
1653
|
+
if (pretty) {
|
|
1654
|
+
for (const result of results) {
|
|
1655
|
+
if (result.success) {
|
|
1656
|
+
console.log(`\u21A9\uFE0F ${result.id} restored`);
|
|
1657
|
+
} else {
|
|
1658
|
+
console.log(`\u2717 ${result.id}: ${result.error}`);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
} else {
|
|
1662
|
+
console.log(formatJson({
|
|
1663
|
+
restored: results.filter((r) => r.success).map((r) => r.id),
|
|
1664
|
+
...results.some((r) => !r.success) && {
|
|
1665
|
+
errors: results.filter((r) => !r.success).map((r) => ({ id: r.id, error: r.error }))
|
|
1666
|
+
}
|
|
1667
|
+
}));
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
} catch (error) {
|
|
1671
|
+
outputError(error, pretty);
|
|
1672
|
+
}
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1415
1676
|
// src/cli/commands/claim.ts
|
|
1416
1677
|
function claimCommand(program2) {
|
|
1417
1678
|
program2.command("claim <ids...>").description("Claim issues (set status to in_progress). Supports multiple IDs.").action(async (ids) => {
|
|
@@ -2894,6 +3155,151 @@ data: ${message}
|
|
|
2894
3155
|
res.status(500).json({ error: error.message });
|
|
2895
3156
|
}
|
|
2896
3157
|
});
|
|
3158
|
+
app.post("/api/issues/:id/delete", (req, res) => {
|
|
3159
|
+
try {
|
|
3160
|
+
let issue;
|
|
3161
|
+
let issueId;
|
|
3162
|
+
let targetFile;
|
|
3163
|
+
if (isMultiWorktree()) {
|
|
3164
|
+
const found = findIssueInSources(req.params.id, issueFiles);
|
|
3165
|
+
if (!found) {
|
|
3166
|
+
res.status(404).json({ error: `Issue not found: ${req.params.id}` });
|
|
3167
|
+
return;
|
|
3168
|
+
}
|
|
3169
|
+
issue = found.issue;
|
|
3170
|
+
issueId = issue.id;
|
|
3171
|
+
targetFile = found.targetFile;
|
|
3172
|
+
} else {
|
|
3173
|
+
const pebbleDir = getOrCreatePebbleDir();
|
|
3174
|
+
issueId = resolveId(req.params.id);
|
|
3175
|
+
const localIssue = getIssue(issueId);
|
|
3176
|
+
if (!localIssue) {
|
|
3177
|
+
res.status(404).json({ error: `Issue not found: ${req.params.id}` });
|
|
3178
|
+
return;
|
|
3179
|
+
}
|
|
3180
|
+
issue = localIssue;
|
|
3181
|
+
targetFile = path3.join(pebbleDir, "issues.jsonl");
|
|
3182
|
+
}
|
|
3183
|
+
if (issue.deleted) {
|
|
3184
|
+
res.status(400).json({ error: "Issue is already deleted" });
|
|
3185
|
+
return;
|
|
3186
|
+
}
|
|
3187
|
+
const { reason } = req.body;
|
|
3188
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3189
|
+
const state = getComputedState();
|
|
3190
|
+
const toDelete = [];
|
|
3191
|
+
const alreadyQueued = /* @__PURE__ */ new Set();
|
|
3192
|
+
toDelete.push({ id: issueId, cascade: false });
|
|
3193
|
+
alreadyQueued.add(issueId);
|
|
3194
|
+
const descendants = getDescendants(issueId, state);
|
|
3195
|
+
for (const desc of descendants) {
|
|
3196
|
+
if (!alreadyQueued.has(desc.id) && !desc.deleted) {
|
|
3197
|
+
toDelete.push({ id: desc.id, cascade: true });
|
|
3198
|
+
alreadyQueued.add(desc.id);
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
const verifications = getVerifications(issueId);
|
|
3202
|
+
for (const v of verifications) {
|
|
3203
|
+
if (!alreadyQueued.has(v.id) && !v.deleted) {
|
|
3204
|
+
toDelete.push({ id: v.id, cascade: true });
|
|
3205
|
+
alreadyQueued.add(v.id);
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
3208
|
+
const cleanupReferences = (deletedId) => {
|
|
3209
|
+
for (const [id, iss] of state) {
|
|
3210
|
+
if (id === deletedId || iss.deleted) continue;
|
|
3211
|
+
const updates = {};
|
|
3212
|
+
if (iss.blockedBy.includes(deletedId)) {
|
|
3213
|
+
updates.blockedBy = iss.blockedBy.filter((bid) => bid !== deletedId);
|
|
3214
|
+
}
|
|
3215
|
+
if (iss.relatedTo.includes(deletedId)) {
|
|
3216
|
+
updates.relatedTo = iss.relatedTo.filter((rid) => rid !== deletedId);
|
|
3217
|
+
}
|
|
3218
|
+
if (iss.parent === deletedId) {
|
|
3219
|
+
updates.parent = "";
|
|
3220
|
+
}
|
|
3221
|
+
if (Object.keys(updates).length > 0) {
|
|
3222
|
+
const updateEvent = {
|
|
3223
|
+
type: "update",
|
|
3224
|
+
issueId: id,
|
|
3225
|
+
timestamp,
|
|
3226
|
+
data: updates
|
|
3227
|
+
};
|
|
3228
|
+
appendEventToFile(updateEvent, targetFile);
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
};
|
|
3232
|
+
for (const { id, cascade } of toDelete) {
|
|
3233
|
+
const iss = state.get(id);
|
|
3234
|
+
if (!iss) continue;
|
|
3235
|
+
cleanupReferences(id);
|
|
3236
|
+
const deleteEvent = {
|
|
3237
|
+
type: "delete",
|
|
3238
|
+
issueId: id,
|
|
3239
|
+
timestamp,
|
|
3240
|
+
data: {
|
|
3241
|
+
reason,
|
|
3242
|
+
cascade: cascade || void 0,
|
|
3243
|
+
previousStatus: iss.status
|
|
3244
|
+
}
|
|
3245
|
+
};
|
|
3246
|
+
appendEventToFile(deleteEvent, targetFile);
|
|
3247
|
+
}
|
|
3248
|
+
res.json({
|
|
3249
|
+
deleted: toDelete
|
|
3250
|
+
});
|
|
3251
|
+
} catch (error) {
|
|
3252
|
+
res.status(500).json({ error: error.message });
|
|
3253
|
+
}
|
|
3254
|
+
});
|
|
3255
|
+
app.post("/api/issues/:id/restore", (req, res) => {
|
|
3256
|
+
try {
|
|
3257
|
+
let issue;
|
|
3258
|
+
let issueId;
|
|
3259
|
+
let targetFile;
|
|
3260
|
+
if (isMultiWorktree()) {
|
|
3261
|
+
const found = findIssueInSources(req.params.id, issueFiles);
|
|
3262
|
+
if (!found) {
|
|
3263
|
+
res.status(404).json({ error: `Issue not found: ${req.params.id}` });
|
|
3264
|
+
return;
|
|
3265
|
+
}
|
|
3266
|
+
issue = found.issue;
|
|
3267
|
+
issueId = issue.id;
|
|
3268
|
+
targetFile = found.targetFile;
|
|
3269
|
+
} else {
|
|
3270
|
+
const pebbleDir = getOrCreatePebbleDir();
|
|
3271
|
+
issueId = resolveId(req.params.id);
|
|
3272
|
+
const localIssue = getIssue(issueId);
|
|
3273
|
+
if (!localIssue) {
|
|
3274
|
+
res.status(404).json({ error: `Issue not found: ${req.params.id}` });
|
|
3275
|
+
return;
|
|
3276
|
+
}
|
|
3277
|
+
issue = localIssue;
|
|
3278
|
+
targetFile = path3.join(pebbleDir, "issues.jsonl");
|
|
3279
|
+
}
|
|
3280
|
+
if (!issue.deleted) {
|
|
3281
|
+
res.status(400).json({ error: "Issue is not deleted" });
|
|
3282
|
+
return;
|
|
3283
|
+
}
|
|
3284
|
+
const { reason } = req.body;
|
|
3285
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3286
|
+
const event = {
|
|
3287
|
+
type: "restore",
|
|
3288
|
+
issueId,
|
|
3289
|
+
timestamp,
|
|
3290
|
+
data: { reason }
|
|
3291
|
+
};
|
|
3292
|
+
appendEventToFile(event, targetFile);
|
|
3293
|
+
if (isMultiWorktree()) {
|
|
3294
|
+
const updated = findIssueInSources(issueId, issueFiles);
|
|
3295
|
+
res.json(updated?.issue || { ...issue, deleted: false, deletedAt: void 0, updatedAt: timestamp });
|
|
3296
|
+
} else {
|
|
3297
|
+
res.json(getIssue(issueId));
|
|
3298
|
+
}
|
|
3299
|
+
} catch (error) {
|
|
3300
|
+
res.status(500).json({ error: error.message });
|
|
3301
|
+
}
|
|
3302
|
+
});
|
|
2897
3303
|
app.post("/api/issues/:id/comments", (req, res) => {
|
|
2898
3304
|
try {
|
|
2899
3305
|
let issue;
|
|
@@ -3401,6 +3807,35 @@ function countVerifications(epicId) {
|
|
|
3401
3807
|
}
|
|
3402
3808
|
return { total, done };
|
|
3403
3809
|
}
|
|
3810
|
+
function getIssueComments(issueId) {
|
|
3811
|
+
const events = readEvents();
|
|
3812
|
+
return events.filter((e) => e.type === "comment" && e.issueId === issueId).sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()).map((e) => ({ text: e.data.text, timestamp: e.timestamp, source: e.source }));
|
|
3813
|
+
}
|
|
3814
|
+
function formatInProgressPretty(issues) {
|
|
3815
|
+
if (issues.length === 0) return "";
|
|
3816
|
+
const lines = [];
|
|
3817
|
+
lines.push(`## In Progress (${issues.length})`);
|
|
3818
|
+
lines.push("");
|
|
3819
|
+
for (const issue of issues) {
|
|
3820
|
+
lines.push(`\u25B6 ${issue.id}: ${issue.title} [${issue.type}]`);
|
|
3821
|
+
lines.push(` Updated: ${formatRelativeTime(issue.updatedAt)}${issue.lastSource ? ` | Source: ${issue.lastSource}` : ""}`);
|
|
3822
|
+
if (issue.parent) {
|
|
3823
|
+
lines.push(` Parent: ${issue.parent.title}`);
|
|
3824
|
+
}
|
|
3825
|
+
if (issue.comments.length > 0) {
|
|
3826
|
+
const recentComments = issue.comments.slice(0, 3);
|
|
3827
|
+
for (const comment of recentComments) {
|
|
3828
|
+
const truncated = comment.text.length > 100 ? comment.text.substring(0, 100) + "..." : comment.text;
|
|
3829
|
+
lines.push(` \u2022 ${formatRelativeTime(comment.timestamp)}: ${truncated.replace(/\n/g, " ")}`);
|
|
3830
|
+
}
|
|
3831
|
+
if (issue.comments.length > 3) {
|
|
3832
|
+
lines.push(` (${issue.comments.length - 3} more comment${issue.comments.length - 3 === 1 ? "" : "s"})`);
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
3835
|
+
lines.push("");
|
|
3836
|
+
}
|
|
3837
|
+
return lines.join("\n");
|
|
3838
|
+
}
|
|
3404
3839
|
function formatSummaryPretty(summaries, sectionHeader) {
|
|
3405
3840
|
if (summaries.length === 0) {
|
|
3406
3841
|
return "No epics found.";
|
|
@@ -3478,6 +3913,28 @@ function summaryCommand(program2) {
|
|
|
3478
3913
|
}
|
|
3479
3914
|
return;
|
|
3480
3915
|
}
|
|
3916
|
+
const inProgressIssues = getIssues({ status: "in_progress" });
|
|
3917
|
+
inProgressIssues.sort(
|
|
3918
|
+
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
|
3919
|
+
);
|
|
3920
|
+
const inProgressSummaries = inProgressIssues.map((issue) => {
|
|
3921
|
+
const summary = {
|
|
3922
|
+
id: issue.id,
|
|
3923
|
+
title: issue.title,
|
|
3924
|
+
type: issue.type,
|
|
3925
|
+
createdAt: issue.createdAt,
|
|
3926
|
+
updatedAt: issue.updatedAt,
|
|
3927
|
+
lastSource: issue.lastSource,
|
|
3928
|
+
comments: getIssueComments(issue.id)
|
|
3929
|
+
};
|
|
3930
|
+
if (issue.parent) {
|
|
3931
|
+
const parentIssue = getIssue(issue.parent, true);
|
|
3932
|
+
if (parentIssue) {
|
|
3933
|
+
summary.parent = { id: parentIssue.id, title: parentIssue.title };
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
return summary;
|
|
3937
|
+
});
|
|
3481
3938
|
const openEpics = allEpics.filter((e) => e.status !== "closed");
|
|
3482
3939
|
const seventyTwoHoursAgo = Date.now() - 72 * 60 * 60 * 1e3;
|
|
3483
3940
|
const closedEpics = allEpics.filter(
|
|
@@ -3495,7 +3952,12 @@ function summaryCommand(program2) {
|
|
|
3495
3952
|
const closedSummaries = limitedClosed.map(buildSummary);
|
|
3496
3953
|
if (pretty) {
|
|
3497
3954
|
const output = [];
|
|
3955
|
+
const inProgressOutput = formatInProgressPretty(inProgressSummaries);
|
|
3956
|
+
if (inProgressOutput) {
|
|
3957
|
+
output.push(inProgressOutput);
|
|
3958
|
+
}
|
|
3498
3959
|
if (openSummaries.length > 0) {
|
|
3960
|
+
if (output.length > 0) output.push("");
|
|
3499
3961
|
output.push(formatSummaryPretty(openSummaries, "Open Epics"));
|
|
3500
3962
|
}
|
|
3501
3963
|
if (closedSummaries.length > 0) {
|
|
@@ -3503,11 +3965,11 @@ function summaryCommand(program2) {
|
|
|
3503
3965
|
output.push(formatSummaryPretty(closedSummaries, "Recently Closed Epics (last 72h)"));
|
|
3504
3966
|
}
|
|
3505
3967
|
if (output.length === 0) {
|
|
3506
|
-
output.push("No epics found.");
|
|
3968
|
+
output.push("No issues in progress and no epics found.");
|
|
3507
3969
|
}
|
|
3508
3970
|
console.log(output.join("\n"));
|
|
3509
3971
|
} else {
|
|
3510
|
-
console.log(formatJson({ open: openSummaries, closed: closedSummaries }));
|
|
3972
|
+
console.log(formatJson({ inProgress: inProgressSummaries, open: openSummaries, closed: closedSummaries }));
|
|
3511
3973
|
}
|
|
3512
3974
|
} catch (error) {
|
|
3513
3975
|
outputError(error, pretty);
|
|
@@ -3786,6 +4248,8 @@ createCommand(program);
|
|
|
3786
4248
|
updateCommand(program);
|
|
3787
4249
|
closeCommand(program);
|
|
3788
4250
|
reopenCommand(program);
|
|
4251
|
+
deleteCommand(program);
|
|
4252
|
+
restoreCommand(program);
|
|
3789
4253
|
claimCommand(program);
|
|
3790
4254
|
listCommand(program);
|
|
3791
4255
|
showCommand(program);
|