@markmdev/pebble 0.1.15 → 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 +503 -10
- 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 +1 -1
- package/dist/ui/assets/index-BeE0zkRT.css +0 -1
- package/dist/ui/assets/index-BxTP1gqC.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",
|
|
@@ -86,6 +86,17 @@ function getMainWorktreeRoot() {
|
|
|
86
86
|
return null;
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
+
function getCurrentWorktreeName() {
|
|
90
|
+
try {
|
|
91
|
+
const toplevel = execSync("git rev-parse --show-toplevel", {
|
|
92
|
+
encoding: "utf-8",
|
|
93
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
94
|
+
}).trim();
|
|
95
|
+
return path.basename(toplevel);
|
|
96
|
+
} catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
89
100
|
|
|
90
101
|
// src/cli/lib/storage.ts
|
|
91
102
|
var PEBBLE_DIR = ".pebble";
|
|
@@ -187,9 +198,21 @@ function getIssuesPath(pebbleDir) {
|
|
|
187
198
|
const dir = pebbleDir ?? getPebbleDir();
|
|
188
199
|
return path2.join(dir, ISSUES_FILE);
|
|
189
200
|
}
|
|
201
|
+
function getEventSource() {
|
|
202
|
+
const worktreeName = getCurrentWorktreeName();
|
|
203
|
+
if (worktreeName) {
|
|
204
|
+
return worktreeName;
|
|
205
|
+
}
|
|
206
|
+
const pebbleDir = discoverPebbleDir();
|
|
207
|
+
if (pebbleDir) {
|
|
208
|
+
return path2.basename(path2.dirname(pebbleDir));
|
|
209
|
+
}
|
|
210
|
+
return "unknown";
|
|
211
|
+
}
|
|
190
212
|
function appendEvent(event, pebbleDir) {
|
|
191
213
|
const issuesPath = getIssuesPath(pebbleDir);
|
|
192
|
-
const
|
|
214
|
+
const eventWithSource = event.source ? event : { ...event, source: getEventSource() };
|
|
215
|
+
const line = JSON.stringify(eventWithSource) + "\n";
|
|
193
216
|
fs.appendFileSync(issuesPath, line, "utf-8");
|
|
194
217
|
}
|
|
195
218
|
function readEventsFromFile(filePath) {
|
|
@@ -270,7 +293,8 @@ function computeState(events) {
|
|
|
270
293
|
verifies: createEvent.data.verifies,
|
|
271
294
|
comments: [],
|
|
272
295
|
createdAt: event.timestamp,
|
|
273
|
-
updatedAt: event.timestamp
|
|
296
|
+
updatedAt: event.timestamp,
|
|
297
|
+
lastSource: event.source
|
|
274
298
|
};
|
|
275
299
|
issues.set(event.issueId, issue);
|
|
276
300
|
break;
|
|
@@ -304,6 +328,7 @@ function computeState(events) {
|
|
|
304
328
|
issue.relatedTo = updateEvent.data.relatedTo;
|
|
305
329
|
}
|
|
306
330
|
issue.updatedAt = event.timestamp;
|
|
331
|
+
if (event.source) issue.lastSource = event.source;
|
|
307
332
|
}
|
|
308
333
|
break;
|
|
309
334
|
}
|
|
@@ -312,6 +337,7 @@ function computeState(events) {
|
|
|
312
337
|
if (issue) {
|
|
313
338
|
issue.status = "closed";
|
|
314
339
|
issue.updatedAt = event.timestamp;
|
|
340
|
+
if (event.source) issue.lastSource = event.source;
|
|
315
341
|
}
|
|
316
342
|
break;
|
|
317
343
|
}
|
|
@@ -320,6 +346,7 @@ function computeState(events) {
|
|
|
320
346
|
if (issue) {
|
|
321
347
|
issue.status = "open";
|
|
322
348
|
issue.updatedAt = event.timestamp;
|
|
349
|
+
if (event.source) issue.lastSource = event.source;
|
|
323
350
|
}
|
|
324
351
|
break;
|
|
325
352
|
}
|
|
@@ -329,6 +356,27 @@ function computeState(events) {
|
|
|
329
356
|
if (issue) {
|
|
330
357
|
issue.comments.push(commentEvent.data);
|
|
331
358
|
issue.updatedAt = event.timestamp;
|
|
359
|
+
if (event.source) issue.lastSource = event.source;
|
|
360
|
+
}
|
|
361
|
+
break;
|
|
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;
|
|
332
380
|
}
|
|
333
381
|
break;
|
|
334
382
|
}
|
|
@@ -336,10 +384,13 @@ function computeState(events) {
|
|
|
336
384
|
}
|
|
337
385
|
return issues;
|
|
338
386
|
}
|
|
339
|
-
function getIssues(filters) {
|
|
387
|
+
function getIssues(filters, includeDeleted = false) {
|
|
340
388
|
const events = readEvents();
|
|
341
389
|
const state = computeState(events);
|
|
342
390
|
let issues = Array.from(state.values());
|
|
391
|
+
if (!includeDeleted) {
|
|
392
|
+
issues = issues.filter((i) => !i.deleted);
|
|
393
|
+
}
|
|
343
394
|
if (filters) {
|
|
344
395
|
if (filters.status !== void 0) {
|
|
345
396
|
issues = issues.filter((i) => i.status === filters.status);
|
|
@@ -356,10 +407,14 @@ function getIssues(filters) {
|
|
|
356
407
|
}
|
|
357
408
|
return issues;
|
|
358
409
|
}
|
|
359
|
-
function getIssue(id) {
|
|
410
|
+
function getIssue(id, includeDeleted = false) {
|
|
360
411
|
const events = readEvents();
|
|
361
412
|
const state = computeState(events);
|
|
362
|
-
|
|
413
|
+
const issue = state.get(id);
|
|
414
|
+
if (issue && issue.deleted && !includeDeleted) {
|
|
415
|
+
return void 0;
|
|
416
|
+
}
|
|
417
|
+
return issue;
|
|
363
418
|
}
|
|
364
419
|
function resolveId(partial) {
|
|
365
420
|
const events = readEvents();
|
|
@@ -402,6 +457,9 @@ function getReady() {
|
|
|
402
457
|
const state = computeState(events);
|
|
403
458
|
const issues = Array.from(state.values());
|
|
404
459
|
return issues.filter((issue) => {
|
|
460
|
+
if (issue.deleted) {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
405
463
|
if (issue.status === "closed" || issue.status === "pending_verification") {
|
|
406
464
|
return false;
|
|
407
465
|
}
|
|
@@ -425,6 +483,9 @@ function getBlocked() {
|
|
|
425
483
|
const state = computeState(events);
|
|
426
484
|
const issues = Array.from(state.values());
|
|
427
485
|
return issues.filter((issue) => {
|
|
486
|
+
if (issue.deleted) {
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
428
489
|
if (issue.status === "closed") {
|
|
429
490
|
return false;
|
|
430
491
|
}
|
|
@@ -588,6 +649,20 @@ function getAncestryChain(issueId, state) {
|
|
|
588
649
|
}
|
|
589
650
|
return chain;
|
|
590
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
|
+
}
|
|
591
666
|
|
|
592
667
|
// src/shared/time.ts
|
|
593
668
|
import { formatDistanceToNow, parseISO } from "date-fns";
|
|
@@ -903,7 +978,7 @@ function formatIssueListVerbose(issues, sectionHeader) {
|
|
|
903
978
|
lines.push(`${issue.id}: ${issue.title}`);
|
|
904
979
|
lines.push(` Type: ${formatType(issue.type)} | Priority: P${issue.priority} | Created: ${formatRelativeTime(issue.createdAt)}`);
|
|
905
980
|
if (ancestry.length > 0) {
|
|
906
|
-
const chain = [...ancestry].reverse().map((a) => a.
|
|
981
|
+
const chain = [...ancestry].reverse().map((a) => a.title).join(" \u2192 ");
|
|
907
982
|
lines.push(` Ancestry: ${chain}`);
|
|
908
983
|
}
|
|
909
984
|
if (blocking.length > 0) {
|
|
@@ -1384,6 +1459,220 @@ function reopenCommand(program2) {
|
|
|
1384
1459
|
});
|
|
1385
1460
|
}
|
|
1386
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
|
+
|
|
1387
1676
|
// src/cli/commands/claim.ts
|
|
1388
1677
|
function claimCommand(program2) {
|
|
1389
1678
|
program2.command("claim <ids...>").description("Claim issues (set status to in_progress). Supports multiple IDs.").action(async (ids) => {
|
|
@@ -2205,7 +2494,8 @@ function findIssueInSources(issueId, filePaths) {
|
|
|
2205
2494
|
return { issue: found, targetFile };
|
|
2206
2495
|
}
|
|
2207
2496
|
function appendEventToFile(event, filePath) {
|
|
2208
|
-
const
|
|
2497
|
+
const eventWithSource = event.source ? event : { ...event, source: getEventSource() };
|
|
2498
|
+
const line = JSON.stringify(eventWithSource) + "\n";
|
|
2209
2499
|
fs2.appendFileSync(filePath, line, "utf-8");
|
|
2210
2500
|
}
|
|
2211
2501
|
function uiCommand(program2) {
|
|
@@ -2865,6 +3155,151 @@ data: ${message}
|
|
|
2865
3155
|
res.status(500).json({ error: error.message });
|
|
2866
3156
|
}
|
|
2867
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
|
+
});
|
|
2868
3303
|
app.post("/api/issues/:id/comments", (req, res) => {
|
|
2869
3304
|
try {
|
|
2870
3305
|
let issue;
|
|
@@ -3372,6 +3807,35 @@ function countVerifications(epicId) {
|
|
|
3372
3807
|
}
|
|
3373
3808
|
return { total, done };
|
|
3374
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
|
+
}
|
|
3375
3839
|
function formatSummaryPretty(summaries, sectionHeader) {
|
|
3376
3840
|
if (summaries.length === 0) {
|
|
3377
3841
|
return "No epics found.";
|
|
@@ -3449,6 +3913,28 @@ function summaryCommand(program2) {
|
|
|
3449
3913
|
}
|
|
3450
3914
|
return;
|
|
3451
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
|
+
});
|
|
3452
3938
|
const openEpics = allEpics.filter((e) => e.status !== "closed");
|
|
3453
3939
|
const seventyTwoHoursAgo = Date.now() - 72 * 60 * 60 * 1e3;
|
|
3454
3940
|
const closedEpics = allEpics.filter(
|
|
@@ -3466,7 +3952,12 @@ function summaryCommand(program2) {
|
|
|
3466
3952
|
const closedSummaries = limitedClosed.map(buildSummary);
|
|
3467
3953
|
if (pretty) {
|
|
3468
3954
|
const output = [];
|
|
3955
|
+
const inProgressOutput = formatInProgressPretty(inProgressSummaries);
|
|
3956
|
+
if (inProgressOutput) {
|
|
3957
|
+
output.push(inProgressOutput);
|
|
3958
|
+
}
|
|
3469
3959
|
if (openSummaries.length > 0) {
|
|
3960
|
+
if (output.length > 0) output.push("");
|
|
3470
3961
|
output.push(formatSummaryPretty(openSummaries, "Open Epics"));
|
|
3471
3962
|
}
|
|
3472
3963
|
if (closedSummaries.length > 0) {
|
|
@@ -3474,11 +3965,11 @@ function summaryCommand(program2) {
|
|
|
3474
3965
|
output.push(formatSummaryPretty(closedSummaries, "Recently Closed Epics (last 72h)"));
|
|
3475
3966
|
}
|
|
3476
3967
|
if (output.length === 0) {
|
|
3477
|
-
output.push("No epics found.");
|
|
3968
|
+
output.push("No issues in progress and no epics found.");
|
|
3478
3969
|
}
|
|
3479
3970
|
console.log(output.join("\n"));
|
|
3480
3971
|
} else {
|
|
3481
|
-
console.log(formatJson({ open: openSummaries, closed: closedSummaries }));
|
|
3972
|
+
console.log(formatJson({ inProgress: inProgressSummaries, open: openSummaries, closed: closedSummaries }));
|
|
3482
3973
|
}
|
|
3483
3974
|
} catch (error) {
|
|
3484
3975
|
outputError(error, pretty);
|
|
@@ -3757,6 +4248,8 @@ createCommand(program);
|
|
|
3757
4248
|
updateCommand(program);
|
|
3758
4249
|
closeCommand(program);
|
|
3759
4250
|
reopenCommand(program);
|
|
4251
|
+
deleteCommand(program);
|
|
4252
|
+
restoreCommand(program);
|
|
3760
4253
|
claimCommand(program);
|
|
3761
4254
|
listCommand(program);
|
|
3762
4255
|
showCommand(program);
|