@markmdev/pebble 0.1.6 → 0.1.7
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 +206 -14
- package/dist/cli/index.js.map +1 -1
- package/dist/ui/assets/index-CAcD3W8c.css +1 -0
- package/dist/ui/assets/index-D7K46Jfk.js +317 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-B9ZwNZIm.js +0 -317
- package/dist/ui/assets/index-C8VlXm0M.css +0 -1
package/dist/cli/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import { Command } from "commander";
|
|
|
12
12
|
// src/shared/types.ts
|
|
13
13
|
var ISSUE_TYPES = ["task", "bug", "epic", "verification"];
|
|
14
14
|
var PRIORITIES = [0, 1, 2, 3, 4];
|
|
15
|
-
var STATUSES = ["open", "in_progress", "blocked", "closed"];
|
|
15
|
+
var STATUSES = ["open", "in_progress", "blocked", "pending_verification", "closed"];
|
|
16
16
|
var EVENT_TYPES = ["create", "update", "close", "reopen", "comment"];
|
|
17
17
|
var PRIORITY_LABELS = {
|
|
18
18
|
0: "critical",
|
|
@@ -25,6 +25,7 @@ var STATUS_LABELS = {
|
|
|
25
25
|
open: "Open",
|
|
26
26
|
in_progress: "In Progress",
|
|
27
27
|
blocked: "Blocked",
|
|
28
|
+
pending_verification: "Pending Verification",
|
|
28
29
|
closed: "Closed"
|
|
29
30
|
};
|
|
30
31
|
var TYPE_LABELS = {
|
|
@@ -183,6 +184,7 @@ function computeState(events) {
|
|
|
183
184
|
description: createEvent.data.description,
|
|
184
185
|
parent: createEvent.data.parent,
|
|
185
186
|
blockedBy: [],
|
|
187
|
+
relatedTo: [],
|
|
186
188
|
verifies: createEvent.data.verifies,
|
|
187
189
|
comments: [],
|
|
188
190
|
createdAt: event.timestamp,
|
|
@@ -216,6 +218,9 @@ function computeState(events) {
|
|
|
216
218
|
if (updateEvent.data.blockedBy !== void 0) {
|
|
217
219
|
issue.blockedBy = updateEvent.data.blockedBy;
|
|
218
220
|
}
|
|
221
|
+
if (updateEvent.data.relatedTo !== void 0) {
|
|
222
|
+
issue.relatedTo = updateEvent.data.relatedTo;
|
|
223
|
+
}
|
|
219
224
|
issue.updatedAt = event.timestamp;
|
|
220
225
|
}
|
|
221
226
|
break;
|
|
@@ -315,7 +320,7 @@ function getReady() {
|
|
|
315
320
|
const state = computeState(events);
|
|
316
321
|
const issues = Array.from(state.values());
|
|
317
322
|
return issues.filter((issue) => {
|
|
318
|
-
if (issue.status === "closed") {
|
|
323
|
+
if (issue.status === "closed" || issue.status === "pending_verification") {
|
|
319
324
|
return false;
|
|
320
325
|
}
|
|
321
326
|
for (const blockerId of issue.blockedBy) {
|
|
@@ -456,6 +461,36 @@ function getNewlyUnblocked(closedIssueId) {
|
|
|
456
461
|
}
|
|
457
462
|
return result;
|
|
458
463
|
}
|
|
464
|
+
function getRelated(issueId) {
|
|
465
|
+
const issue = getIssue(issueId);
|
|
466
|
+
if (!issue) {
|
|
467
|
+
return [];
|
|
468
|
+
}
|
|
469
|
+
const events = readEvents();
|
|
470
|
+
const state = computeState(events);
|
|
471
|
+
return issue.relatedTo.map((id) => state.get(id)).filter((i) => i !== void 0);
|
|
472
|
+
}
|
|
473
|
+
function hasOpenBlockersById(issueId) {
|
|
474
|
+
const issue = getIssue(issueId);
|
|
475
|
+
if (!issue) {
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
const events = readEvents();
|
|
479
|
+
const state = computeState(events);
|
|
480
|
+
return issue.blockedBy.some((blockerId) => {
|
|
481
|
+
const blocker = state.get(blockerId);
|
|
482
|
+
return blocker && blocker.status !== "closed";
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
function getOpenBlockers(issueId) {
|
|
486
|
+
const issue = getIssue(issueId);
|
|
487
|
+
if (!issue) {
|
|
488
|
+
return [];
|
|
489
|
+
}
|
|
490
|
+
const events = readEvents();
|
|
491
|
+
const state = computeState(events);
|
|
492
|
+
return issue.blockedBy.map((id) => state.get(id)).filter((i) => i !== void 0 && i.status !== "closed");
|
|
493
|
+
}
|
|
459
494
|
|
|
460
495
|
// src/cli/lib/output.ts
|
|
461
496
|
function formatJson(data) {
|
|
@@ -547,7 +582,7 @@ function formatIssueListPretty(issues) {
|
|
|
547
582
|
lines.push(`Total: ${issues.length} issue(s)`);
|
|
548
583
|
return lines.join("\n");
|
|
549
584
|
}
|
|
550
|
-
function formatDepsPretty(issueId, blockedBy, blocking) {
|
|
585
|
+
function formatDepsPretty(issueId, blockedBy, blocking, related = []) {
|
|
551
586
|
const lines = [];
|
|
552
587
|
lines.push(`Dependencies for ${issueId}`);
|
|
553
588
|
lines.push("\u2500".repeat(40));
|
|
@@ -570,6 +605,16 @@ function formatDepsPretty(issueId, blockedBy, blocking) {
|
|
|
570
605
|
lines.push(` \u25CB ${issue.id} - ${truncate(issue.title, 30)}`);
|
|
571
606
|
}
|
|
572
607
|
}
|
|
608
|
+
lines.push("");
|
|
609
|
+
lines.push("Related:");
|
|
610
|
+
if (related.length === 0) {
|
|
611
|
+
lines.push(" (none)");
|
|
612
|
+
} else {
|
|
613
|
+
for (const issue of related) {
|
|
614
|
+
const status = issue.status === "closed" ? "\u2713" : "\u25CB";
|
|
615
|
+
lines.push(` ${status} ${issue.id} - ${truncate(issue.title, 30)}`);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
573
618
|
return lines.join("\n");
|
|
574
619
|
}
|
|
575
620
|
function formatError(error) {
|
|
@@ -832,6 +877,12 @@ function updateCommand(program2) {
|
|
|
832
877
|
results.push({ id, success: false, error: `Issue not found: ${id}` });
|
|
833
878
|
continue;
|
|
834
879
|
}
|
|
880
|
+
if (data.status === "in_progress" && hasOpenBlockersById(resolvedId)) {
|
|
881
|
+
const blockers = getOpenBlockers(resolvedId);
|
|
882
|
+
const blockerIds = blockers.map((b) => b.id).join(", ");
|
|
883
|
+
results.push({ id: resolvedId, success: false, error: `Cannot set to in_progress - blocked by: ${blockerIds}` });
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
835
886
|
const event = {
|
|
836
887
|
type: "update",
|
|
837
888
|
issueId: resolvedId,
|
|
@@ -914,6 +965,25 @@ function closeCommand(program2) {
|
|
|
914
965
|
};
|
|
915
966
|
appendEvent(commentEvent, pebbleDir);
|
|
916
967
|
}
|
|
968
|
+
const pendingVerifications = getVerifications(resolvedId).filter((v) => v.status !== "closed");
|
|
969
|
+
if (pendingVerifications.length > 0) {
|
|
970
|
+
const updateEvent = {
|
|
971
|
+
type: "update",
|
|
972
|
+
issueId: resolvedId,
|
|
973
|
+
timestamp,
|
|
974
|
+
data: {
|
|
975
|
+
status: "pending_verification"
|
|
976
|
+
}
|
|
977
|
+
};
|
|
978
|
+
appendEvent(updateEvent, pebbleDir);
|
|
979
|
+
results.push({
|
|
980
|
+
id: resolvedId,
|
|
981
|
+
success: true,
|
|
982
|
+
status: "pending_verification",
|
|
983
|
+
pendingVerifications: pendingVerifications.map((v) => ({ id: v.id, title: v.title }))
|
|
984
|
+
});
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
917
987
|
const closeEvent = {
|
|
918
988
|
type: "close",
|
|
919
989
|
issueId: resolvedId,
|
|
@@ -927,6 +997,7 @@ function closeCommand(program2) {
|
|
|
927
997
|
results.push({
|
|
928
998
|
id: resolvedId,
|
|
929
999
|
success: true,
|
|
1000
|
+
status: "closed",
|
|
930
1001
|
unblocked: unblocked.length > 0 ? unblocked.map((i) => ({ id: i.id, title: i.title })) : void 0
|
|
931
1002
|
});
|
|
932
1003
|
} catch (error) {
|
|
@@ -937,18 +1008,29 @@ function closeCommand(program2) {
|
|
|
937
1008
|
const result = results[0];
|
|
938
1009
|
if (result.success) {
|
|
939
1010
|
if (pretty) {
|
|
940
|
-
|
|
941
|
-
|
|
1011
|
+
if (result.status === "pending_verification") {
|
|
1012
|
+
console.log(`\u23F3 ${result.id} \u2192 pending_verification`);
|
|
942
1013
|
console.log(`
|
|
1014
|
+
Pending verifications:`);
|
|
1015
|
+
for (const v of result.pendingVerifications || []) {
|
|
1016
|
+
console.log(` \u2022 ${v.id} - ${v.title}`);
|
|
1017
|
+
}
|
|
1018
|
+
} else {
|
|
1019
|
+
console.log(`\u2713 ${result.id}`);
|
|
1020
|
+
if (result.unblocked && result.unblocked.length > 0) {
|
|
1021
|
+
console.log(`
|
|
943
1022
|
Unblocked:`);
|
|
944
|
-
|
|
945
|
-
|
|
1023
|
+
for (const u of result.unblocked) {
|
|
1024
|
+
console.log(` \u2192 ${u.id} - ${u.title}`);
|
|
1025
|
+
}
|
|
946
1026
|
}
|
|
947
1027
|
}
|
|
948
1028
|
} else {
|
|
949
1029
|
console.log(formatJson({
|
|
950
1030
|
id: result.id,
|
|
951
1031
|
success: true,
|
|
1032
|
+
status: result.status,
|
|
1033
|
+
...result.pendingVerifications && { pendingVerifications: result.pendingVerifications },
|
|
952
1034
|
...result.unblocked && { unblocked: result.unblocked }
|
|
953
1035
|
}));
|
|
954
1036
|
}
|
|
@@ -959,10 +1041,17 @@ Unblocked:`);
|
|
|
959
1041
|
if (pretty) {
|
|
960
1042
|
for (const result of results) {
|
|
961
1043
|
if (result.success) {
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
for (const
|
|
965
|
-
console.log(` \
|
|
1044
|
+
if (result.status === "pending_verification") {
|
|
1045
|
+
console.log(`\u23F3 ${result.id} \u2192 pending_verification`);
|
|
1046
|
+
for (const v of result.pendingVerifications || []) {
|
|
1047
|
+
console.log(` \u2022 ${v.id} - ${v.title}`);
|
|
1048
|
+
}
|
|
1049
|
+
} else {
|
|
1050
|
+
console.log(`\u2713 ${result.id}`);
|
|
1051
|
+
if (result.unblocked && result.unblocked.length > 0) {
|
|
1052
|
+
for (const u of result.unblocked) {
|
|
1053
|
+
console.log(` \u2192 ${u.id} - ${u.title}`);
|
|
1054
|
+
}
|
|
966
1055
|
}
|
|
967
1056
|
}
|
|
968
1057
|
} else {
|
|
@@ -973,7 +1062,9 @@ Unblocked:`);
|
|
|
973
1062
|
console.log(formatJson(results.map((r) => ({
|
|
974
1063
|
id: r.id,
|
|
975
1064
|
success: r.success,
|
|
1065
|
+
status: r.status,
|
|
976
1066
|
...r.error && { error: r.error },
|
|
1067
|
+
...r.pendingVerifications && { pendingVerifications: r.pendingVerifications },
|
|
977
1068
|
...r.unblocked && { unblocked: r.unblocked }
|
|
978
1069
|
}))));
|
|
979
1070
|
}
|
|
@@ -1041,6 +1132,12 @@ function claimCommand(program2) {
|
|
|
1041
1132
|
results.push({ id: resolvedId, success: false, error: `Cannot claim closed issue: ${resolvedId}` });
|
|
1042
1133
|
continue;
|
|
1043
1134
|
}
|
|
1135
|
+
if (hasOpenBlockersById(resolvedId)) {
|
|
1136
|
+
const blockers = getOpenBlockers(resolvedId);
|
|
1137
|
+
const blockerIds = blockers.map((b) => b.id).join(", ");
|
|
1138
|
+
results.push({ id: resolvedId, success: false, error: `Cannot claim blocked issue. Blocked by: ${blockerIds}` });
|
|
1139
|
+
continue;
|
|
1140
|
+
}
|
|
1044
1141
|
const event = {
|
|
1045
1142
|
type: "update",
|
|
1046
1143
|
issueId: resolvedId,
|
|
@@ -1264,6 +1361,99 @@ function depCommand(program2) {
|
|
|
1264
1361
|
outputError(error, pretty);
|
|
1265
1362
|
}
|
|
1266
1363
|
});
|
|
1364
|
+
dep.command("relate <id1> <id2>").description("Add a bidirectional related link between two issues").action(async (id1, id2) => {
|
|
1365
|
+
const pretty = program2.opts().pretty ?? false;
|
|
1366
|
+
try {
|
|
1367
|
+
const pebbleDir = getOrCreatePebbleDir();
|
|
1368
|
+
const resolvedId1 = resolveId(id1);
|
|
1369
|
+
const resolvedId2 = resolveId(id2);
|
|
1370
|
+
const issue1 = getIssue(resolvedId1);
|
|
1371
|
+
if (!issue1) {
|
|
1372
|
+
throw new Error(`Issue not found: ${id1}`);
|
|
1373
|
+
}
|
|
1374
|
+
const issue2 = getIssue(resolvedId2);
|
|
1375
|
+
if (!issue2) {
|
|
1376
|
+
throw new Error(`Issue not found: ${id2}`);
|
|
1377
|
+
}
|
|
1378
|
+
if (resolvedId1 === resolvedId2) {
|
|
1379
|
+
throw new Error("Cannot relate issue to itself");
|
|
1380
|
+
}
|
|
1381
|
+
if (issue1.relatedTo.includes(resolvedId2)) {
|
|
1382
|
+
throw new Error(`Issues are already related: ${resolvedId1} \u2194 ${resolvedId2}`);
|
|
1383
|
+
}
|
|
1384
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1385
|
+
const event1 = {
|
|
1386
|
+
type: "update",
|
|
1387
|
+
issueId: resolvedId1,
|
|
1388
|
+
timestamp,
|
|
1389
|
+
data: {
|
|
1390
|
+
relatedTo: [...issue1.relatedTo, resolvedId2]
|
|
1391
|
+
}
|
|
1392
|
+
};
|
|
1393
|
+
const event2 = {
|
|
1394
|
+
type: "update",
|
|
1395
|
+
issueId: resolvedId2,
|
|
1396
|
+
timestamp,
|
|
1397
|
+
data: {
|
|
1398
|
+
relatedTo: [...issue2.relatedTo, resolvedId1]
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1401
|
+
appendEvent(event1, pebbleDir);
|
|
1402
|
+
appendEvent(event2, pebbleDir);
|
|
1403
|
+
if (pretty) {
|
|
1404
|
+
console.log(`\u2713 ${resolvedId1} \u2194 ${resolvedId2}`);
|
|
1405
|
+
} else {
|
|
1406
|
+
console.log(formatJson({ id1: resolvedId1, id2: resolvedId2, related: true }));
|
|
1407
|
+
}
|
|
1408
|
+
} catch (error) {
|
|
1409
|
+
outputError(error, pretty);
|
|
1410
|
+
}
|
|
1411
|
+
});
|
|
1412
|
+
dep.command("unrelate <id1> <id2>").description("Remove a bidirectional related link between two issues").action(async (id1, id2) => {
|
|
1413
|
+
const pretty = program2.opts().pretty ?? false;
|
|
1414
|
+
try {
|
|
1415
|
+
const pebbleDir = getOrCreatePebbleDir();
|
|
1416
|
+
const resolvedId1 = resolveId(id1);
|
|
1417
|
+
const resolvedId2 = resolveId(id2);
|
|
1418
|
+
const issue1 = getIssue(resolvedId1);
|
|
1419
|
+
if (!issue1) {
|
|
1420
|
+
throw new Error(`Issue not found: ${id1}`);
|
|
1421
|
+
}
|
|
1422
|
+
const issue2 = getIssue(resolvedId2);
|
|
1423
|
+
if (!issue2) {
|
|
1424
|
+
throw new Error(`Issue not found: ${id2}`);
|
|
1425
|
+
}
|
|
1426
|
+
if (!issue1.relatedTo.includes(resolvedId2)) {
|
|
1427
|
+
throw new Error(`Issues are not related: ${resolvedId1} \u2194 ${resolvedId2}`);
|
|
1428
|
+
}
|
|
1429
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1430
|
+
const event1 = {
|
|
1431
|
+
type: "update",
|
|
1432
|
+
issueId: resolvedId1,
|
|
1433
|
+
timestamp,
|
|
1434
|
+
data: {
|
|
1435
|
+
relatedTo: issue1.relatedTo.filter((id) => id !== resolvedId2)
|
|
1436
|
+
}
|
|
1437
|
+
};
|
|
1438
|
+
const event2 = {
|
|
1439
|
+
type: "update",
|
|
1440
|
+
issueId: resolvedId2,
|
|
1441
|
+
timestamp,
|
|
1442
|
+
data: {
|
|
1443
|
+
relatedTo: issue2.relatedTo.filter((id) => id !== resolvedId1)
|
|
1444
|
+
}
|
|
1445
|
+
};
|
|
1446
|
+
appendEvent(event1, pebbleDir);
|
|
1447
|
+
appendEvent(event2, pebbleDir);
|
|
1448
|
+
if (pretty) {
|
|
1449
|
+
console.log(`\u2713 ${resolvedId1} \u21AE ${resolvedId2}`);
|
|
1450
|
+
} else {
|
|
1451
|
+
console.log(formatJson({ id1: resolvedId1, id2: resolvedId2, related: false }));
|
|
1452
|
+
}
|
|
1453
|
+
} catch (error) {
|
|
1454
|
+
outputError(error, pretty);
|
|
1455
|
+
}
|
|
1456
|
+
});
|
|
1267
1457
|
dep.command("list <id>").description("List dependencies for an issue").action(async (id) => {
|
|
1268
1458
|
const pretty = program2.opts().pretty ?? false;
|
|
1269
1459
|
try {
|
|
@@ -1274,13 +1464,15 @@ function depCommand(program2) {
|
|
|
1274
1464
|
}
|
|
1275
1465
|
const blockedBy = getBlockers(resolvedId);
|
|
1276
1466
|
const blocking = getBlocking(resolvedId);
|
|
1467
|
+
const related = getRelated(resolvedId);
|
|
1277
1468
|
if (pretty) {
|
|
1278
|
-
console.log(formatDepsPretty(resolvedId, blockedBy, blocking));
|
|
1469
|
+
console.log(formatDepsPretty(resolvedId, blockedBy, blocking, related));
|
|
1279
1470
|
} else {
|
|
1280
1471
|
console.log(formatJson({
|
|
1281
1472
|
issueId: resolvedId,
|
|
1282
1473
|
blockedBy: blockedBy.map((i) => ({ id: i.id, title: i.title, status: i.status })),
|
|
1283
|
-
blocking: blocking.map((i) => ({ id: i.id, title: i.title, status: i.status }))
|
|
1474
|
+
blocking: blocking.map((i) => ({ id: i.id, title: i.title, status: i.status })),
|
|
1475
|
+
related: related.map((i) => ({ id: i.id, title: i.title, status: i.status }))
|
|
1284
1476
|
}));
|
|
1285
1477
|
}
|
|
1286
1478
|
} catch (error) {
|
|
@@ -1910,7 +2102,7 @@ data: ${message}
|
|
|
1910
2102
|
return;
|
|
1911
2103
|
}
|
|
1912
2104
|
if (updates.status) {
|
|
1913
|
-
const validStatuses = ["open", "in_progress", "blocked"];
|
|
2105
|
+
const validStatuses = ["open", "in_progress", "blocked", "pending_verification"];
|
|
1914
2106
|
if (!validStatuses.includes(updates.status)) {
|
|
1915
2107
|
res.status(400).json({
|
|
1916
2108
|
error: `Invalid status: ${updates.status}. Use close endpoint to close issues.`
|