@markmdev/pebble 0.1.11 → 0.1.13
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 +227 -80
- package/dist/cli/index.js.map +1 -1
- package/dist/ui/assets/index-BeE0zkRT.css +1 -0
- package/dist/ui/assets/index-BxTP1gqC.js +332 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-B0G5JRaP.js +0 -327
- package/dist/ui/assets/index-BVQN8evO.css +0 -1
package/dist/cli/index.js
CHANGED
|
@@ -37,7 +37,7 @@ var TYPE_LABELS = {
|
|
|
37
37
|
|
|
38
38
|
// src/cli/lib/storage.ts
|
|
39
39
|
import * as fs from "fs";
|
|
40
|
-
import * as
|
|
40
|
+
import * as path2 from "path";
|
|
41
41
|
|
|
42
42
|
// src/cli/lib/id.ts
|
|
43
43
|
import * as crypto from "crypto";
|
|
@@ -59,23 +59,78 @@ function derivePrefix(folderName) {
|
|
|
59
59
|
return clean.slice(0, 4).toUpperCase().padEnd(4, "X");
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// src/cli/lib/git.ts
|
|
63
|
+
import { execSync } from "child_process";
|
|
64
|
+
import path from "path";
|
|
65
|
+
function getMainWorktreeRoot() {
|
|
66
|
+
try {
|
|
67
|
+
const gitCommonDir = execSync("git rev-parse --git-common-dir", {
|
|
68
|
+
encoding: "utf-8",
|
|
69
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
70
|
+
}).trim();
|
|
71
|
+
const gitDir = execSync("git rev-parse --git-dir", {
|
|
72
|
+
encoding: "utf-8",
|
|
73
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
74
|
+
}).trim();
|
|
75
|
+
const normalizedCommon = path.resolve(gitCommonDir);
|
|
76
|
+
const normalizedGit = path.resolve(gitDir);
|
|
77
|
+
if (normalizedCommon === normalizedGit) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const mainRoot = path.dirname(normalizedCommon);
|
|
81
|
+
return mainRoot;
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
62
87
|
// src/cli/lib/storage.ts
|
|
63
88
|
var PEBBLE_DIR = ".pebble";
|
|
64
89
|
var ISSUES_FILE = "issues.jsonl";
|
|
65
90
|
var CONFIG_FILE = "config.json";
|
|
91
|
+
function getConfigSafe(pebbleDir) {
|
|
92
|
+
try {
|
|
93
|
+
const configPath = path2.join(pebbleDir, CONFIG_FILE);
|
|
94
|
+
if (!fs.existsSync(configPath)) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
98
|
+
return JSON.parse(content);
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function resolveWorktreePebbleDir(localPebbleDir) {
|
|
104
|
+
if (process.env.PEBBLE_LOCAL === "1") {
|
|
105
|
+
return localPebbleDir;
|
|
106
|
+
}
|
|
107
|
+
const config = getConfigSafe(localPebbleDir);
|
|
108
|
+
if (config?.useMainTreePebble === false) {
|
|
109
|
+
return localPebbleDir;
|
|
110
|
+
}
|
|
111
|
+
const mainRoot = getMainWorktreeRoot();
|
|
112
|
+
if (!mainRoot) {
|
|
113
|
+
return localPebbleDir;
|
|
114
|
+
}
|
|
115
|
+
const mainPebble = path2.join(mainRoot, PEBBLE_DIR);
|
|
116
|
+
if (fs.existsSync(mainPebble) && fs.statSync(mainPebble).isDirectory()) {
|
|
117
|
+
return mainPebble;
|
|
118
|
+
}
|
|
119
|
+
return localPebbleDir;
|
|
120
|
+
}
|
|
66
121
|
function discoverPebbleDir(startDir = process.cwd()) {
|
|
67
|
-
let currentDir =
|
|
68
|
-
const root =
|
|
122
|
+
let currentDir = path2.resolve(startDir);
|
|
123
|
+
const root = path2.parse(currentDir).root;
|
|
69
124
|
while (currentDir !== root) {
|
|
70
|
-
const pebbleDir =
|
|
125
|
+
const pebbleDir = path2.join(currentDir, PEBBLE_DIR);
|
|
71
126
|
if (fs.existsSync(pebbleDir) && fs.statSync(pebbleDir).isDirectory()) {
|
|
72
|
-
return pebbleDir;
|
|
127
|
+
return resolveWorktreePebbleDir(pebbleDir);
|
|
73
128
|
}
|
|
74
|
-
currentDir =
|
|
129
|
+
currentDir = path2.dirname(currentDir);
|
|
75
130
|
}
|
|
76
|
-
const rootPebble =
|
|
131
|
+
const rootPebble = path2.join(root, PEBBLE_DIR);
|
|
77
132
|
if (fs.existsSync(rootPebble) && fs.statSync(rootPebble).isDirectory()) {
|
|
78
|
-
return rootPebble;
|
|
133
|
+
return resolveWorktreePebbleDir(rootPebble);
|
|
79
134
|
}
|
|
80
135
|
return null;
|
|
81
136
|
}
|
|
@@ -87,23 +142,35 @@ function getPebbleDir() {
|
|
|
87
142
|
return dir;
|
|
88
143
|
}
|
|
89
144
|
function ensurePebbleDir(baseDir = process.cwd()) {
|
|
90
|
-
|
|
145
|
+
let targetDir = baseDir;
|
|
146
|
+
if (process.env.PEBBLE_LOCAL !== "1") {
|
|
147
|
+
const mainRoot = getMainWorktreeRoot();
|
|
148
|
+
if (mainRoot) {
|
|
149
|
+
const mainPebble = path2.join(mainRoot, PEBBLE_DIR);
|
|
150
|
+
if (fs.existsSync(mainPebble) && fs.statSync(mainPebble).isDirectory()) {
|
|
151
|
+
return mainPebble;
|
|
152
|
+
}
|
|
153
|
+
targetDir = mainRoot;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const pebbleDir = path2.join(targetDir, PEBBLE_DIR);
|
|
91
157
|
if (!fs.existsSync(pebbleDir)) {
|
|
92
158
|
fs.mkdirSync(pebbleDir, { recursive: true });
|
|
93
|
-
const folderName =
|
|
159
|
+
const folderName = path2.basename(targetDir);
|
|
94
160
|
const config = {
|
|
95
161
|
prefix: derivePrefix(folderName),
|
|
96
|
-
version: "0.1.0"
|
|
162
|
+
version: "0.1.0",
|
|
163
|
+
useMainTreePebble: true
|
|
97
164
|
};
|
|
98
165
|
setConfig(config, pebbleDir);
|
|
99
|
-
const issuesPath =
|
|
166
|
+
const issuesPath = path2.join(pebbleDir, ISSUES_FILE);
|
|
100
167
|
fs.writeFileSync(issuesPath, "", "utf-8");
|
|
101
168
|
}
|
|
102
169
|
return pebbleDir;
|
|
103
170
|
}
|
|
104
171
|
function getIssuesPath(pebbleDir) {
|
|
105
172
|
const dir = pebbleDir ?? getPebbleDir();
|
|
106
|
-
return
|
|
173
|
+
return path2.join(dir, ISSUES_FILE);
|
|
107
174
|
}
|
|
108
175
|
function appendEvent(event, pebbleDir) {
|
|
109
176
|
const issuesPath = getIssuesPath(pebbleDir);
|
|
@@ -129,7 +196,7 @@ function readEvents(pebbleDir) {
|
|
|
129
196
|
if (!dir) {
|
|
130
197
|
return [];
|
|
131
198
|
}
|
|
132
|
-
const issuesPath =
|
|
199
|
+
const issuesPath = path2.join(dir, ISSUES_FILE);
|
|
133
200
|
if (!fs.existsSync(issuesPath)) {
|
|
134
201
|
return [];
|
|
135
202
|
}
|
|
@@ -145,7 +212,7 @@ function readEvents(pebbleDir) {
|
|
|
145
212
|
}
|
|
146
213
|
function getConfigPath(pebbleDir) {
|
|
147
214
|
const dir = pebbleDir ?? getPebbleDir();
|
|
148
|
-
return
|
|
215
|
+
return path2.join(dir, CONFIG_FILE);
|
|
149
216
|
}
|
|
150
217
|
function getConfig(pebbleDir) {
|
|
151
218
|
const configPath = getConfigPath(pebbleDir);
|
|
@@ -157,7 +224,7 @@ function getConfig(pebbleDir) {
|
|
|
157
224
|
}
|
|
158
225
|
function setConfig(config, pebbleDir) {
|
|
159
226
|
const dir = pebbleDir ?? getPebbleDir();
|
|
160
|
-
const configPath =
|
|
227
|
+
const configPath = path2.join(dir, CONFIG_FILE);
|
|
161
228
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
162
229
|
}
|
|
163
230
|
function getOrCreatePebbleDir() {
|
|
@@ -491,6 +558,21 @@ function getOpenBlockers(issueId) {
|
|
|
491
558
|
const state = computeState(events);
|
|
492
559
|
return issue.blockedBy.map((id) => state.get(id)).filter((i) => i !== void 0 && i.status !== "closed");
|
|
493
560
|
}
|
|
561
|
+
function getComputedState() {
|
|
562
|
+
const events = readEvents();
|
|
563
|
+
return computeState(events);
|
|
564
|
+
}
|
|
565
|
+
function getAncestryChain(issueId, state) {
|
|
566
|
+
const chain = [];
|
|
567
|
+
let current = state.get(issueId);
|
|
568
|
+
while (current?.parent) {
|
|
569
|
+
const parent = state.get(current.parent);
|
|
570
|
+
if (!parent) break;
|
|
571
|
+
chain.push({ id: parent.id, title: parent.title });
|
|
572
|
+
current = parent;
|
|
573
|
+
}
|
|
574
|
+
return chain;
|
|
575
|
+
}
|
|
494
576
|
|
|
495
577
|
// src/shared/time.ts
|
|
496
578
|
import { formatDistanceToNow, parseISO } from "date-fns";
|
|
@@ -536,7 +618,10 @@ function formatIssueDetailPretty(issue, ctx) {
|
|
|
536
618
|
lines.push(`Type: ${formatType(issue.type)}`);
|
|
537
619
|
lines.push(`Priority: ${formatPriority(issue.priority)}`);
|
|
538
620
|
lines.push(`Status: ${formatStatus(issue.status)}`);
|
|
539
|
-
if (
|
|
621
|
+
if (ctx.ancestry && ctx.ancestry.length > 0) {
|
|
622
|
+
const chain = [...ctx.ancestry].reverse().map((a) => a.id).join(" > ");
|
|
623
|
+
lines.push(`Ancestry: ${chain}`);
|
|
624
|
+
} else if (issue.parent) {
|
|
540
625
|
lines.push(`Parent: ${issue.parent}`);
|
|
541
626
|
}
|
|
542
627
|
if (issue.description) {
|
|
@@ -602,7 +687,8 @@ function outputIssueDetail(issue, ctx, pretty) {
|
|
|
602
687
|
blocking: ctx.blocking.map((i) => i.id),
|
|
603
688
|
children: ctx.children.map((i) => ({ id: i.id, title: i.title, status: i.status })),
|
|
604
689
|
verifications: ctx.verifications.map((i) => ({ id: i.id, title: i.title, status: i.status })),
|
|
605
|
-
related: ctx.related.map((i) => i.id)
|
|
690
|
+
related: ctx.related.map((i) => i.id),
|
|
691
|
+
...ctx.ancestry && ctx.ancestry.length > 0 && { ancestry: ctx.ancestry }
|
|
606
692
|
};
|
|
607
693
|
console.log(formatJson(output));
|
|
608
694
|
}
|
|
@@ -798,11 +884,12 @@ function formatIssueListVerbose(issues, sectionHeader) {
|
|
|
798
884
|
lines.push("");
|
|
799
885
|
}
|
|
800
886
|
for (const info of issues) {
|
|
801
|
-
const { issue, blocking, children, verifications, blockers,
|
|
887
|
+
const { issue, blocking, children, verifications, blockers, ancestry } = info;
|
|
802
888
|
lines.push(`${issue.id}: ${issue.title}`);
|
|
803
889
|
lines.push(` Type: ${formatType(issue.type)} | Priority: P${issue.priority} | Created: ${formatRelativeTime(issue.createdAt)}`);
|
|
804
|
-
if (
|
|
805
|
-
|
|
890
|
+
if (ancestry.length > 0) {
|
|
891
|
+
const chain = [...ancestry].reverse().map((a) => a.id).join(" > ");
|
|
892
|
+
lines.push(` Ancestry: ${chain}`);
|
|
806
893
|
}
|
|
807
894
|
if (blocking.length > 0) {
|
|
808
895
|
lines.push(` Blocking: ${blocking.join(", ")}`);
|
|
@@ -824,13 +911,13 @@ function outputIssueListVerbose(issues, pretty, sectionHeader, limitInfo) {
|
|
|
824
911
|
console.log(formatLimitMessage(limitInfo));
|
|
825
912
|
}
|
|
826
913
|
} else {
|
|
827
|
-
const output = issues.map(({ issue, blocking, children, verifications, blockers,
|
|
914
|
+
const output = issues.map(({ issue, blocking, children, verifications, blockers, ancestry }) => ({
|
|
828
915
|
...issue,
|
|
829
916
|
blocking,
|
|
830
917
|
childrenCount: issue.type === "epic" ? children : void 0,
|
|
831
918
|
verificationsCount: verifications,
|
|
832
919
|
...blockers && { openBlockers: blockers },
|
|
833
|
-
...
|
|
920
|
+
...ancestry.length > 0 && { ancestry }
|
|
834
921
|
}));
|
|
835
922
|
if (limitInfo?.limited) {
|
|
836
923
|
console.log(formatJson({ issues: output, _meta: limitInfo }));
|
|
@@ -1137,11 +1224,31 @@ function closeCommand(program2) {
|
|
|
1137
1224
|
};
|
|
1138
1225
|
appendEvent(closeEvent, pebbleDir);
|
|
1139
1226
|
const unblocked = getNewlyUnblocked(resolvedId);
|
|
1227
|
+
let autoClosed;
|
|
1228
|
+
if (issue.verifies) {
|
|
1229
|
+
const targetIssue = getIssue(issue.verifies);
|
|
1230
|
+
if (targetIssue && targetIssue.status === "pending_verification") {
|
|
1231
|
+
const remainingVerifications = getVerifications(issue.verifies).filter((v) => v.status !== "closed");
|
|
1232
|
+
if (remainingVerifications.length === 0) {
|
|
1233
|
+
const autoCloseEvent = {
|
|
1234
|
+
type: "close",
|
|
1235
|
+
issueId: issue.verifies,
|
|
1236
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1237
|
+
data: {
|
|
1238
|
+
reason: "All verifications completed"
|
|
1239
|
+
}
|
|
1240
|
+
};
|
|
1241
|
+
appendEvent(autoCloseEvent, pebbleDir);
|
|
1242
|
+
autoClosed = { id: targetIssue.id, title: targetIssue.title };
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1140
1246
|
results.push({
|
|
1141
1247
|
id: resolvedId,
|
|
1142
1248
|
success: true,
|
|
1143
1249
|
status: "closed",
|
|
1144
|
-
unblocked: unblocked.length > 0 ? unblocked.map((i) => ({ id: i.id, title: i.title })) : void 0
|
|
1250
|
+
unblocked: unblocked.length > 0 ? unblocked.map((i) => ({ id: i.id, title: i.title })) : void 0,
|
|
1251
|
+
autoClosed
|
|
1145
1252
|
});
|
|
1146
1253
|
} catch (error) {
|
|
1147
1254
|
results.push({ id, success: false, error: error.message });
|
|
@@ -1169,6 +1276,10 @@ Unblocked:`);
|
|
|
1169
1276
|
console.log(` \u2192 ${u.id} - ${u.title}`);
|
|
1170
1277
|
}
|
|
1171
1278
|
}
|
|
1279
|
+
if (result.autoClosed) {
|
|
1280
|
+
console.log(`
|
|
1281
|
+
\u2713 ${result.autoClosed.id} auto-closed (all verifications complete)`);
|
|
1282
|
+
}
|
|
1172
1283
|
}
|
|
1173
1284
|
} else {
|
|
1174
1285
|
console.log(formatJson({
|
|
@@ -1177,7 +1288,8 @@ Unblocked:`);
|
|
|
1177
1288
|
status: result.status,
|
|
1178
1289
|
...result.pendingVerifications && { pendingVerifications: result.pendingVerifications },
|
|
1179
1290
|
...result.pendingVerifications && { hint: `pb verifications ${result.id}` },
|
|
1180
|
-
...result.unblocked && { unblocked: result.unblocked }
|
|
1291
|
+
...result.unblocked && { unblocked: result.unblocked },
|
|
1292
|
+
...result.autoClosed && { autoClosed: result.autoClosed }
|
|
1181
1293
|
}));
|
|
1182
1294
|
}
|
|
1183
1295
|
} else {
|
|
@@ -1200,6 +1312,9 @@ Unblocked:`);
|
|
|
1200
1312
|
console.log(` \u2192 ${u.id} - ${u.title}`);
|
|
1201
1313
|
}
|
|
1202
1314
|
}
|
|
1315
|
+
if (result.autoClosed) {
|
|
1316
|
+
console.log(` \u2713 ${result.autoClosed.id} auto-closed (all verifications complete)`);
|
|
1317
|
+
}
|
|
1203
1318
|
}
|
|
1204
1319
|
} else {
|
|
1205
1320
|
console.log(`\u2717 ${result.id}: ${result.error}`);
|
|
@@ -1213,7 +1328,8 @@ Unblocked:`);
|
|
|
1213
1328
|
...r.error && { error: r.error },
|
|
1214
1329
|
...r.pendingVerifications && { pendingVerifications: r.pendingVerifications },
|
|
1215
1330
|
...r.pendingVerifications && { hint: `pb verifications ${r.id}` },
|
|
1216
|
-
...r.unblocked && { unblocked: r.unblocked }
|
|
1331
|
+
...r.unblocked && { unblocked: r.unblocked },
|
|
1332
|
+
...r.autoClosed && { autoClosed: r.autoClosed }
|
|
1217
1333
|
}))));
|
|
1218
1334
|
}
|
|
1219
1335
|
}
|
|
@@ -1374,20 +1490,15 @@ function listCommand(program2) {
|
|
|
1374
1490
|
limited: limit > 0 && total > limit
|
|
1375
1491
|
};
|
|
1376
1492
|
if (options.verbose) {
|
|
1493
|
+
const state = getComputedState();
|
|
1377
1494
|
const verboseIssues = issues.map((issue) => {
|
|
1378
|
-
|
|
1495
|
+
return {
|
|
1379
1496
|
issue,
|
|
1380
1497
|
blocking: getBlocking(issue.id).map((i) => i.id),
|
|
1381
1498
|
children: getChildren(issue.id).length,
|
|
1382
|
-
verifications: getVerifications(issue.id).length
|
|
1499
|
+
verifications: getVerifications(issue.id).length,
|
|
1500
|
+
ancestry: getAncestryChain(issue.id, state)
|
|
1383
1501
|
};
|
|
1384
|
-
if (issue.parent) {
|
|
1385
|
-
const parentIssue = getIssue(issue.parent);
|
|
1386
|
-
if (parentIssue) {
|
|
1387
|
-
info.parent = { id: parentIssue.id, title: parentIssue.title };
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
return info;
|
|
1391
1502
|
});
|
|
1392
1503
|
let sectionHeader = "Issues";
|
|
1393
1504
|
if (filters.status) {
|
|
@@ -1424,7 +1535,9 @@ function showCommand(program2) {
|
|
|
1424
1535
|
const children = issue.type === "epic" ? getChildren(resolvedId) : [];
|
|
1425
1536
|
const verifications = getVerifications(resolvedId);
|
|
1426
1537
|
const related = getRelated(resolvedId);
|
|
1427
|
-
|
|
1538
|
+
const state = getComputedState();
|
|
1539
|
+
const ancestry = getAncestryChain(resolvedId, state);
|
|
1540
|
+
outputIssueDetail(issue, { blocking, children, verifications, related, ancestry }, pretty);
|
|
1428
1541
|
} catch (error) {
|
|
1429
1542
|
outputError(error, pretty);
|
|
1430
1543
|
}
|
|
@@ -1457,20 +1570,15 @@ function readyCommand(program2) {
|
|
|
1457
1570
|
limited: limit > 0 && total > limit
|
|
1458
1571
|
};
|
|
1459
1572
|
if (options.verbose) {
|
|
1573
|
+
const state = getComputedState();
|
|
1460
1574
|
const verboseIssues = issues.map((issue) => {
|
|
1461
|
-
|
|
1575
|
+
return {
|
|
1462
1576
|
issue,
|
|
1463
1577
|
blocking: getBlocking(issue.id).map((i) => i.id),
|
|
1464
1578
|
children: getChildren(issue.id).length,
|
|
1465
|
-
verifications: getVerifications(issue.id).length
|
|
1579
|
+
verifications: getVerifications(issue.id).length,
|
|
1580
|
+
ancestry: getAncestryChain(issue.id, state)
|
|
1466
1581
|
};
|
|
1467
|
-
if (issue.parent) {
|
|
1468
|
-
const parentIssue = getIssue(issue.parent);
|
|
1469
|
-
if (parentIssue) {
|
|
1470
|
-
info.parent = { id: parentIssue.id, title: parentIssue.title };
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
return info;
|
|
1474
1582
|
});
|
|
1475
1583
|
outputIssueListVerbose(verboseIssues, pretty, "Ready Issues", limitInfo);
|
|
1476
1584
|
} else {
|
|
@@ -1501,23 +1609,18 @@ function blockedCommand(program2) {
|
|
|
1501
1609
|
limited: limit > 0 && total > limit
|
|
1502
1610
|
};
|
|
1503
1611
|
if (options.verbose) {
|
|
1612
|
+
const state = getComputedState();
|
|
1504
1613
|
const verboseIssues = issues.map((issue) => {
|
|
1505
1614
|
const allBlockers = getBlockers(issue.id);
|
|
1506
1615
|
const openBlockers = allBlockers.filter((b) => b.status !== "closed").map((b) => b.id);
|
|
1507
|
-
|
|
1616
|
+
return {
|
|
1508
1617
|
issue,
|
|
1509
1618
|
blocking: getBlocking(issue.id).map((i) => i.id),
|
|
1510
1619
|
children: getChildren(issue.id).length,
|
|
1511
1620
|
verifications: getVerifications(issue.id).length,
|
|
1512
|
-
blockers: openBlockers
|
|
1621
|
+
blockers: openBlockers,
|
|
1622
|
+
ancestry: getAncestryChain(issue.id, state)
|
|
1513
1623
|
};
|
|
1514
|
-
if (issue.parent) {
|
|
1515
|
-
const parentIssue = getIssue(issue.parent);
|
|
1516
|
-
if (parentIssue) {
|
|
1517
|
-
info.parent = { id: parentIssue.id, title: parentIssue.title };
|
|
1518
|
-
}
|
|
1519
|
-
}
|
|
1520
|
-
return info;
|
|
1521
1624
|
});
|
|
1522
1625
|
outputIssueListVerbose(verboseIssues, pretty, "Blocked Issues", limitInfo);
|
|
1523
1626
|
} else {
|
|
@@ -2007,7 +2110,7 @@ function formatGraphPretty(issues) {
|
|
|
2007
2110
|
import express from "express";
|
|
2008
2111
|
import cors from "cors";
|
|
2009
2112
|
import { fileURLToPath } from "url";
|
|
2010
|
-
import
|
|
2113
|
+
import path3 from "path";
|
|
2011
2114
|
import fs2 from "fs";
|
|
2012
2115
|
import net from "net";
|
|
2013
2116
|
import open from "open";
|
|
@@ -2102,14 +2205,14 @@ function uiCommand(program2) {
|
|
|
2102
2205
|
console.error("Error: --files option requires at least one path");
|
|
2103
2206
|
process.exit(1);
|
|
2104
2207
|
}
|
|
2105
|
-
issueFiles = issueFiles.map((p) =>
|
|
2208
|
+
issueFiles = issueFiles.map((p) => path3.resolve(process.cwd(), p));
|
|
2106
2209
|
console.log(`Multi-worktree mode: watching ${issueFiles.length} file(s)`);
|
|
2107
2210
|
for (const f of issueFiles) {
|
|
2108
2211
|
console.log(` - ${f}`);
|
|
2109
2212
|
}
|
|
2110
2213
|
} else {
|
|
2111
2214
|
const pebbleDir = getOrCreatePebbleDir();
|
|
2112
|
-
issueFiles = [
|
|
2215
|
+
issueFiles = [path3.join(pebbleDir, "issues.jsonl")];
|
|
2113
2216
|
}
|
|
2114
2217
|
if (!options.files) {
|
|
2115
2218
|
getOrCreatePebbleDir();
|
|
@@ -2132,7 +2235,7 @@ function uiCommand(program2) {
|
|
|
2132
2235
|
res.status(400).json({ error: "path is required" });
|
|
2133
2236
|
return;
|
|
2134
2237
|
}
|
|
2135
|
-
const resolved =
|
|
2238
|
+
const resolved = path3.resolve(process.cwd(), filePath);
|
|
2136
2239
|
if (!fs2.existsSync(resolved)) {
|
|
2137
2240
|
res.status(400).json({ error: `File not found: ${filePath}` });
|
|
2138
2241
|
return;
|
|
@@ -2170,10 +2273,10 @@ function uiCommand(program2) {
|
|
|
2170
2273
|
});
|
|
2171
2274
|
app.get("/api/worktrees", (_req, res) => {
|
|
2172
2275
|
try {
|
|
2173
|
-
const { execSync } = __require("child_process");
|
|
2276
|
+
const { execSync: execSync2 } = __require("child_process");
|
|
2174
2277
|
let worktreeOutput;
|
|
2175
2278
|
try {
|
|
2176
|
-
worktreeOutput =
|
|
2279
|
+
worktreeOutput = execSync2("git worktree list --porcelain", {
|
|
2177
2280
|
encoding: "utf-8",
|
|
2178
2281
|
cwd: process.cwd()
|
|
2179
2282
|
});
|
|
@@ -2195,7 +2298,7 @@ function uiCommand(program2) {
|
|
|
2195
2298
|
}
|
|
2196
2299
|
}
|
|
2197
2300
|
if (worktreePath) {
|
|
2198
|
-
const issuesFile =
|
|
2301
|
+
const issuesFile = path3.join(worktreePath, ".pebble", "issues.jsonl");
|
|
2199
2302
|
const hasIssues = fs2.existsSync(issuesFile);
|
|
2200
2303
|
const isActive = issueFiles.includes(issuesFile);
|
|
2201
2304
|
let issueCount = 0;
|
|
@@ -2289,7 +2392,7 @@ data: ${message}
|
|
|
2289
2392
|
}
|
|
2290
2393
|
targetFile = issueFiles[targetIndex];
|
|
2291
2394
|
}
|
|
2292
|
-
const pebbleDir = targetFile ?
|
|
2395
|
+
const pebbleDir = targetFile ? path3.dirname(targetFile) : getOrCreatePebbleDir();
|
|
2293
2396
|
const config = getConfig(pebbleDir);
|
|
2294
2397
|
const { title, type, priority, description, parent } = req.body;
|
|
2295
2398
|
if (!title || typeof title !== "string") {
|
|
@@ -2480,7 +2583,7 @@ data: ${message}
|
|
|
2480
2583
|
return;
|
|
2481
2584
|
}
|
|
2482
2585
|
issue = localIssue;
|
|
2483
|
-
targetFile =
|
|
2586
|
+
targetFile = path3.join(pebbleDir, "issues.jsonl");
|
|
2484
2587
|
}
|
|
2485
2588
|
const { title, type, priority, status, description, parent, relatedTo } = req.body;
|
|
2486
2589
|
const updates = {};
|
|
@@ -2608,7 +2711,7 @@ data: ${message}
|
|
|
2608
2711
|
return;
|
|
2609
2712
|
}
|
|
2610
2713
|
issue = localIssue;
|
|
2611
|
-
targetFile =
|
|
2714
|
+
targetFile = path3.join(pebbleDir, "issues.jsonl");
|
|
2612
2715
|
}
|
|
2613
2716
|
if (issue.status === "closed") {
|
|
2614
2717
|
res.status(400).json({ error: "Issue is already closed" });
|
|
@@ -2651,11 +2754,49 @@ data: ${message}
|
|
|
2651
2754
|
data: { reason }
|
|
2652
2755
|
};
|
|
2653
2756
|
appendEventToFile(event, targetFile);
|
|
2757
|
+
let autoClosed;
|
|
2758
|
+
if (issue.verifies) {
|
|
2759
|
+
let targetIssue;
|
|
2760
|
+
let targetVerifications = [];
|
|
2761
|
+
if (isMultiWorktree()) {
|
|
2762
|
+
const found = findIssueInSources(issue.verifies, issueFiles);
|
|
2763
|
+
if (found) {
|
|
2764
|
+
targetIssue = found.issue;
|
|
2765
|
+
const allIssues = mergeIssuesFromFiles(issueFiles);
|
|
2766
|
+
targetVerifications = allIssues.filter(
|
|
2767
|
+
(i) => i.verifies === issue.verifies && i.status !== "closed"
|
|
2768
|
+
);
|
|
2769
|
+
}
|
|
2770
|
+
} else {
|
|
2771
|
+
targetIssue = getIssue(issue.verifies);
|
|
2772
|
+
targetVerifications = getVerifications(issue.verifies).filter((v) => v.status !== "closed");
|
|
2773
|
+
}
|
|
2774
|
+
if (targetIssue && targetIssue.status === "pending_verification" && targetVerifications.length === 0) {
|
|
2775
|
+
const autoCloseEvent = {
|
|
2776
|
+
type: "close",
|
|
2777
|
+
issueId: issue.verifies,
|
|
2778
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2779
|
+
data: { reason: "All verifications completed" }
|
|
2780
|
+
};
|
|
2781
|
+
if (isMultiWorktree()) {
|
|
2782
|
+
const targetFound = findIssueInSources(issue.verifies, issueFiles);
|
|
2783
|
+
if (targetFound) {
|
|
2784
|
+
appendEventToFile(autoCloseEvent, targetFound.targetFile);
|
|
2785
|
+
}
|
|
2786
|
+
} else {
|
|
2787
|
+
const pebbleDir = getOrCreatePebbleDir();
|
|
2788
|
+
appendEventToFile(autoCloseEvent, path3.join(pebbleDir, "issues.jsonl"));
|
|
2789
|
+
}
|
|
2790
|
+
autoClosed = { id: targetIssue.id, title: targetIssue.title };
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2654
2793
|
if (isMultiWorktree()) {
|
|
2655
2794
|
const updated = findIssueInSources(issueId, issueFiles);
|
|
2656
|
-
|
|
2795
|
+
const result = updated?.issue || { ...issue, status: "closed", updatedAt: timestamp };
|
|
2796
|
+
res.json(autoClosed ? { ...result, _autoClosed: autoClosed } : result);
|
|
2657
2797
|
} else {
|
|
2658
|
-
|
|
2798
|
+
const result = getIssue(issueId);
|
|
2799
|
+
res.json(autoClosed ? { ...result, _autoClosed: autoClosed } : result);
|
|
2659
2800
|
}
|
|
2660
2801
|
} catch (error) {
|
|
2661
2802
|
res.status(500).json({ error: error.message });
|
|
@@ -2684,7 +2825,7 @@ data: ${message}
|
|
|
2684
2825
|
return;
|
|
2685
2826
|
}
|
|
2686
2827
|
issue = localIssue;
|
|
2687
|
-
targetFile =
|
|
2828
|
+
targetFile = path3.join(pebbleDir, "issues.jsonl");
|
|
2688
2829
|
}
|
|
2689
2830
|
if (issue.status !== "closed") {
|
|
2690
2831
|
res.status(400).json({ error: "Issue is not closed" });
|
|
@@ -2732,7 +2873,7 @@ data: ${message}
|
|
|
2732
2873
|
return;
|
|
2733
2874
|
}
|
|
2734
2875
|
issue = localIssue;
|
|
2735
|
-
targetFile =
|
|
2876
|
+
targetFile = path3.join(pebbleDir, "issues.jsonl");
|
|
2736
2877
|
}
|
|
2737
2878
|
const { text, author } = req.body;
|
|
2738
2879
|
if (!text || typeof text !== "string" || text.trim() === "") {
|
|
@@ -2784,7 +2925,7 @@ data: ${message}
|
|
|
2784
2925
|
return;
|
|
2785
2926
|
}
|
|
2786
2927
|
issue = localIssue;
|
|
2787
|
-
targetFile =
|
|
2928
|
+
targetFile = path3.join(pebbleDir, "issues.jsonl");
|
|
2788
2929
|
}
|
|
2789
2930
|
const { blockerId } = req.body;
|
|
2790
2931
|
if (!blockerId) {
|
|
@@ -2858,7 +2999,7 @@ data: ${message}
|
|
|
2858
2999
|
return;
|
|
2859
3000
|
}
|
|
2860
3001
|
issue = localIssue;
|
|
2861
|
-
targetFile =
|
|
3002
|
+
targetFile = path3.join(pebbleDir, "issues.jsonl");
|
|
2862
3003
|
}
|
|
2863
3004
|
let resolvedBlockerId;
|
|
2864
3005
|
if (isMultiWorktree()) {
|
|
@@ -2897,11 +3038,11 @@ data: ${message}
|
|
|
2897
3038
|
}
|
|
2898
3039
|
});
|
|
2899
3040
|
const __filename2 = fileURLToPath(import.meta.url);
|
|
2900
|
-
const __dirname2 =
|
|
2901
|
-
const uiPath =
|
|
3041
|
+
const __dirname2 = path3.dirname(__filename2);
|
|
3042
|
+
const uiPath = path3.resolve(__dirname2, "../ui");
|
|
2902
3043
|
app.use(express.static(uiPath));
|
|
2903
3044
|
app.get("*", (_req, res) => {
|
|
2904
|
-
res.sendFile(
|
|
3045
|
+
res.sendFile(path3.join(uiPath, "index.html"));
|
|
2905
3046
|
});
|
|
2906
3047
|
const requestedPort = parseInt(options.port, 10);
|
|
2907
3048
|
const actualPort = await findAvailablePort(requestedPort);
|
|
@@ -2925,7 +3066,7 @@ data: ${message}
|
|
|
2925
3066
|
// src/cli/commands/import.ts
|
|
2926
3067
|
import * as fs3 from "fs";
|
|
2927
3068
|
import * as readline from "readline";
|
|
2928
|
-
import * as
|
|
3069
|
+
import * as path4 from "path";
|
|
2929
3070
|
function importCommand(program2) {
|
|
2930
3071
|
program2.command("import <file>").description("Import issues from a Beads issues.jsonl file").option("--dry-run", "Show what would be imported without writing").option("--prefix <prefix>", "Override the ID prefix (default: derive from folder name)").action(async (file, options) => {
|
|
2931
3072
|
const pretty = program2.opts().pretty ?? false;
|
|
@@ -2938,7 +3079,7 @@ function importCommand(program2) {
|
|
|
2938
3079
|
console.log(pretty ? "No issues found in file." : formatJson({ imported: 0 }));
|
|
2939
3080
|
return;
|
|
2940
3081
|
}
|
|
2941
|
-
const prefix = options.prefix ?? derivePrefix(
|
|
3082
|
+
const prefix = options.prefix ?? derivePrefix(path4.basename(process.cwd()));
|
|
2942
3083
|
const { events, idMap, stats } = convertToPebbleEvents(beadsIssues, prefix);
|
|
2943
3084
|
if (options.dryRun) {
|
|
2944
3085
|
if (pretty) {
|
|
@@ -3116,7 +3257,7 @@ function generateSuffix() {
|
|
|
3116
3257
|
|
|
3117
3258
|
// src/cli/commands/merge.ts
|
|
3118
3259
|
import * as fs4 from "fs";
|
|
3119
|
-
import * as
|
|
3260
|
+
import * as path5 from "path";
|
|
3120
3261
|
function mergeEvents(filePaths) {
|
|
3121
3262
|
const seen = /* @__PURE__ */ new Map();
|
|
3122
3263
|
for (const filePath of filePaths) {
|
|
@@ -3159,7 +3300,7 @@ function mergeCommand(program2) {
|
|
|
3159
3300
|
const pretty = program2.opts().pretty ?? false;
|
|
3160
3301
|
const filePaths = [];
|
|
3161
3302
|
for (const file of files) {
|
|
3162
|
-
const resolved =
|
|
3303
|
+
const resolved = path5.resolve(process.cwd(), file);
|
|
3163
3304
|
if (!fs4.existsSync(resolved)) {
|
|
3164
3305
|
console.error(`Error: File not found: ${file}`);
|
|
3165
3306
|
process.exit(1);
|
|
@@ -3561,7 +3702,7 @@ function verificationsCommand(program2) {
|
|
|
3561
3702
|
}
|
|
3562
3703
|
|
|
3563
3704
|
// src/cli/commands/init.ts
|
|
3564
|
-
import * as
|
|
3705
|
+
import * as path6 from "path";
|
|
3565
3706
|
function initCommand(program2) {
|
|
3566
3707
|
program2.command("init").description("Initialize a new .pebble directory in the current directory").option("--force", "Re-initialize even if .pebble already exists").action((options) => {
|
|
3567
3708
|
const existing = discoverPebbleDir();
|
|
@@ -3577,8 +3718,8 @@ function initCommand(program2) {
|
|
|
3577
3718
|
console.log(JSON.stringify({
|
|
3578
3719
|
initialized: true,
|
|
3579
3720
|
path: pebbleDir,
|
|
3580
|
-
configPath:
|
|
3581
|
-
issuesPath:
|
|
3721
|
+
configPath: path6.join(pebbleDir, "config.json"),
|
|
3722
|
+
issuesPath: path6.join(pebbleDir, "issues.jsonl")
|
|
3582
3723
|
}));
|
|
3583
3724
|
});
|
|
3584
3725
|
}
|
|
@@ -3588,6 +3729,12 @@ var program = new Command();
|
|
|
3588
3729
|
program.name("pebble").description("A lightweight JSONL-based issue tracker").version("0.1.0");
|
|
3589
3730
|
program.option("-P, --pretty", "Human-readable output (default: JSON)");
|
|
3590
3731
|
program.option("--json", "JSON output (this is the default, flag not needed)");
|
|
3732
|
+
program.option("--local", "Use local .pebble directory even in a git worktree");
|
|
3733
|
+
program.hook("preAction", () => {
|
|
3734
|
+
if (program.opts().local) {
|
|
3735
|
+
process.env.PEBBLE_LOCAL = "1";
|
|
3736
|
+
}
|
|
3737
|
+
});
|
|
3591
3738
|
createCommand(program);
|
|
3592
3739
|
updateCommand(program);
|
|
3593
3740
|
closeCommand(program);
|