@hcrosse/opencode-pr-tracker 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/dist/server.js +8 -4
- package/dist/server.js.map +3 -3
- package/dist/tui.js +177 -33
- package/dist/tui.js.map +4 -4
- package/package.json +1 -1
package/dist/tui.js
CHANGED
|
@@ -1584,6 +1584,8 @@ var segmentPattern = /^[A-Za-z0-9._-]+$/;
|
|
|
1584
1584
|
function parsePullRequestUrl(input) {
|
|
1585
1585
|
if (input.trim() !== input)
|
|
1586
1586
|
return invalidPullRequestUrl;
|
|
1587
|
+
if (input.includes("\\"))
|
|
1588
|
+
return invalidPullRequestUrl;
|
|
1587
1589
|
if (!input.startsWith("https://"))
|
|
1588
1590
|
return invalidPullRequestUrl;
|
|
1589
1591
|
const authorityEnd = input.indexOf("/", "https://".length);
|
|
@@ -1616,15 +1618,17 @@ function parsePullRequestUrl(input) {
|
|
|
1616
1618
|
if (segments.length !== 5 || segments[0] !== "" || segments[3] !== "pull") {
|
|
1617
1619
|
return invalidPullRequestUrl;
|
|
1618
1620
|
}
|
|
1619
|
-
const
|
|
1620
|
-
const
|
|
1621
|
+
const rawOwner = segments[1];
|
|
1622
|
+
const rawRepository = segments[2];
|
|
1621
1623
|
const numberText = segments[4];
|
|
1622
|
-
if (
|
|
1624
|
+
if (rawOwner === undefined || rawRepository === undefined || numberText === undefined || !segmentPattern.test(rawOwner) || !segmentPattern.test(rawRepository) || !/^\d+$/.test(numberText)) {
|
|
1623
1625
|
return invalidPullRequestUrl;
|
|
1624
1626
|
}
|
|
1625
1627
|
const number = Number(numberText);
|
|
1626
1628
|
if (!Number.isSafeInteger(number) || number <= 0)
|
|
1627
1629
|
return invalidPullRequestUrl;
|
|
1630
|
+
const owner = rawOwner.toLowerCase();
|
|
1631
|
+
const repository = rawRepository.toLowerCase();
|
|
1628
1632
|
const url = `https://github.com/${owner}/${repository}/pull/${number}`;
|
|
1629
1633
|
const value = { url, owner, repository, number };
|
|
1630
1634
|
return { ok: true, value };
|
|
@@ -2313,7 +2317,7 @@ function startSessionPolling(input) {
|
|
|
2313
2317
|
let timerRegistered = false;
|
|
2314
2318
|
let stopped = false;
|
|
2315
2319
|
let inFlight;
|
|
2316
|
-
let
|
|
2320
|
+
let queued;
|
|
2317
2321
|
function project(attachments) {
|
|
2318
2322
|
return attachments.map((attachment) => ({
|
|
2319
2323
|
attachment,
|
|
@@ -2325,11 +2329,14 @@ function startSessionPolling(input) {
|
|
|
2325
2329
|
async function poll() {
|
|
2326
2330
|
const attachments = await input.store.list(input.sessionID);
|
|
2327
2331
|
if (stopped)
|
|
2328
|
-
return
|
|
2332
|
+
return {
|
|
2333
|
+
ok: true,
|
|
2334
|
+
value: "stopped"
|
|
2335
|
+
};
|
|
2329
2336
|
if (!attachments.ok) {
|
|
2330
2337
|
input.publish([]);
|
|
2331
2338
|
input.onStateFailure(attachments.error);
|
|
2332
|
-
return;
|
|
2339
|
+
return attachments;
|
|
2333
2340
|
}
|
|
2334
2341
|
const attachedUrls = new Set(attachments.value.map((attachment) => attachment.pullRequest.url));
|
|
2335
2342
|
for (const url of statuses.keys()) {
|
|
@@ -2337,6 +2344,11 @@ function startSessionPolling(input) {
|
|
|
2337
2344
|
statuses.delete(url);
|
|
2338
2345
|
}
|
|
2339
2346
|
input.publish(project(attachments.value));
|
|
2347
|
+
if (attachments.value.length === 0)
|
|
2348
|
+
return {
|
|
2349
|
+
ok: true,
|
|
2350
|
+
value: "no_attachments"
|
|
2351
|
+
};
|
|
2340
2352
|
const refreshable = attachments.value.filter((attachment) => {
|
|
2341
2353
|
const previous = statuses.get(attachment.pullRequest.url);
|
|
2342
2354
|
return previous?.tag !== "Available" || previous.state.tag !== "Merged";
|
|
@@ -2345,12 +2357,17 @@ function startSessionPolling(input) {
|
|
|
2345
2357
|
signal: controller.signal
|
|
2346
2358
|
});
|
|
2347
2359
|
if (stopped)
|
|
2348
|
-
return
|
|
2360
|
+
return {
|
|
2361
|
+
ok: true,
|
|
2362
|
+
value: "stopped"
|
|
2363
|
+
};
|
|
2349
2364
|
let batchDiagnostic;
|
|
2365
|
+
let failure;
|
|
2350
2366
|
if (!batch.ok) {
|
|
2351
2367
|
if (batch.error.tag === "GitHubCancelled")
|
|
2352
|
-
return;
|
|
2368
|
+
return batch;
|
|
2353
2369
|
batchDiagnostic = batch.error.tag === "GitHubBatchLimitExceeded" ? "GitHubUnavailable" : batch.error.tag;
|
|
2370
|
+
failure = batch.error;
|
|
2354
2371
|
}
|
|
2355
2372
|
for (const [index, attachment] of refreshable.entries()) {
|
|
2356
2373
|
const previous = statuses.get(attachment.pullRequest.url);
|
|
@@ -2360,6 +2377,8 @@ function startSessionPolling(input) {
|
|
|
2360
2377
|
continue;
|
|
2361
2378
|
}
|
|
2362
2379
|
const diagnostic = result === undefined ? batchDiagnostic ?? "GitHubUnavailable" : result.error.tag;
|
|
2380
|
+
if (result !== undefined && !result.ok)
|
|
2381
|
+
failure ??= result.error;
|
|
2363
2382
|
statuses.set(attachment.pullRequest.url, previous?.tag === "Available" ? {
|
|
2364
2383
|
...previous,
|
|
2365
2384
|
stale: true,
|
|
@@ -2371,24 +2390,60 @@ function startSessionPolling(input) {
|
|
|
2371
2390
|
}
|
|
2372
2391
|
if (!stopped)
|
|
2373
2392
|
input.publish(project(attachments.value));
|
|
2393
|
+
return failure === undefined ? {
|
|
2394
|
+
ok: true,
|
|
2395
|
+
value: "refreshed"
|
|
2396
|
+
} : {
|
|
2397
|
+
ok: false,
|
|
2398
|
+
error: failure
|
|
2399
|
+
};
|
|
2400
|
+
}
|
|
2401
|
+
function startQueuedRefresh() {
|
|
2402
|
+
inFlight = undefined;
|
|
2403
|
+
const next = queued;
|
|
2404
|
+
queued = undefined;
|
|
2405
|
+
if (next === undefined)
|
|
2406
|
+
return;
|
|
2407
|
+
if (stopped) {
|
|
2408
|
+
next.resolve({
|
|
2409
|
+
ok: true,
|
|
2410
|
+
value: "stopped"
|
|
2411
|
+
});
|
|
2412
|
+
return;
|
|
2413
|
+
}
|
|
2414
|
+
requestRefresh().then((result) => next.resolve(result), (error) => next.reject(error));
|
|
2374
2415
|
}
|
|
2375
|
-
function
|
|
2416
|
+
function requestRefresh() {
|
|
2376
2417
|
if (stopped)
|
|
2377
|
-
return Promise.resolve(
|
|
2418
|
+
return Promise.resolve({
|
|
2419
|
+
ok: true,
|
|
2420
|
+
value: "stopped"
|
|
2421
|
+
});
|
|
2378
2422
|
if (inFlight) {
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2423
|
+
if (queued !== undefined)
|
|
2424
|
+
return queued.promise;
|
|
2425
|
+
let resolve;
|
|
2426
|
+
let reject;
|
|
2427
|
+
const promise = new Promise((resolvePromise, rejectPromise) => {
|
|
2428
|
+
resolve = resolvePromise;
|
|
2429
|
+
reject = rejectPromise;
|
|
2430
|
+
});
|
|
2431
|
+
queued = {
|
|
2432
|
+
promise,
|
|
2433
|
+
resolve,
|
|
2434
|
+
reject
|
|
2435
|
+
};
|
|
2436
|
+
return promise;
|
|
2437
|
+
}
|
|
2438
|
+
const current = poll();
|
|
2439
|
+
inFlight = current;
|
|
2440
|
+
current.then(startQueuedRefresh, startQueuedRefresh);
|
|
2441
|
+
return current;
|
|
2442
|
+
}
|
|
2443
|
+
function scheduledRefresh() {
|
|
2444
|
+
return requestRefresh().then(() => {
|
|
2388
2445
|
return;
|
|
2389
2446
|
});
|
|
2390
|
-
inFlight = wrapped;
|
|
2391
|
-
return wrapped;
|
|
2392
2447
|
}
|
|
2393
2448
|
return {
|
|
2394
2449
|
start() {
|
|
@@ -2396,13 +2451,14 @@ function startSessionPolling(input) {
|
|
|
2396
2451
|
return Promise.resolve();
|
|
2397
2452
|
if (!timerRegistered) {
|
|
2398
2453
|
timer = scheduler.setInterval(() => {
|
|
2399
|
-
|
|
2454
|
+
scheduledRefresh().catch(input.onError);
|
|
2400
2455
|
}, pollIntervalMilliseconds);
|
|
2401
2456
|
timerRegistered = true;
|
|
2402
2457
|
}
|
|
2403
|
-
return
|
|
2458
|
+
return scheduledRefresh();
|
|
2404
2459
|
},
|
|
2405
|
-
refresh,
|
|
2460
|
+
refresh: scheduledRefresh,
|
|
2461
|
+
forceRefresh: requestRefresh,
|
|
2406
2462
|
stop() {
|
|
2407
2463
|
if (stopped)
|
|
2408
2464
|
return;
|
|
@@ -2450,7 +2506,18 @@ function createRefreshBus() {
|
|
|
2450
2506
|
return {
|
|
2451
2507
|
emit(sessionID) {
|
|
2452
2508
|
for (const listener of listeners.get(sessionID) ?? [])
|
|
2453
|
-
listener();
|
|
2509
|
+
listener.refresh();
|
|
2510
|
+
},
|
|
2511
|
+
async forceRefresh(sessionID) {
|
|
2512
|
+
const sessionListeners = listeners.get(sessionID);
|
|
2513
|
+
if (sessionListeners === undefined || sessionListeners.size === 0)
|
|
2514
|
+
return;
|
|
2515
|
+
const settled = await Promise.allSettled([...sessionListeners].map((listener) => listener.forceRefresh()));
|
|
2516
|
+
const rejected = settled.find((result) => result.status === "rejected");
|
|
2517
|
+
if (rejected !== undefined)
|
|
2518
|
+
throw rejected.reason;
|
|
2519
|
+
const results = settled.flatMap((result) => result.status === "fulfilled" ? [result.value] : []);
|
|
2520
|
+
return results.find((result) => !result.ok) ?? results.find((result) => result.ok && result.value === "refreshed") ?? results[0];
|
|
2454
2521
|
},
|
|
2455
2522
|
subscribe(sessionID, listener) {
|
|
2456
2523
|
const sessionListeners = listeners.get(sessionID) ?? new Set;
|
|
@@ -2599,8 +2666,18 @@ function PullRequestSidebar(props) {
|
|
|
2599
2666
|
onError: () => setFailure("Unable to refresh pull request status")
|
|
2600
2667
|
});
|
|
2601
2668
|
polling.start().catch(() => setFailure("Unable to refresh pull request status"));
|
|
2602
|
-
const unsubscribe = props.refreshBus.subscribe(props.sessionID,
|
|
2603
|
-
|
|
2669
|
+
const unsubscribe = props.refreshBus.subscribe(props.sessionID, {
|
|
2670
|
+
refresh() {
|
|
2671
|
+
polling.refresh().catch(() => setFailure("Unable to refresh pull request status"));
|
|
2672
|
+
},
|
|
2673
|
+
async forceRefresh() {
|
|
2674
|
+
try {
|
|
2675
|
+
return await polling.forceRefresh();
|
|
2676
|
+
} catch (error) {
|
|
2677
|
+
setFailure("Unable to refresh pull request status");
|
|
2678
|
+
throw error;
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2604
2681
|
});
|
|
2605
2682
|
const onAbort = () => polling.stop();
|
|
2606
2683
|
props.api.lifecycle.signal.addEventListener("abort", onAbort, {
|
|
@@ -2691,10 +2768,8 @@ function PullRequestSidebar(props) {
|
|
|
2691
2768
|
})();
|
|
2692
2769
|
}
|
|
2693
2770
|
function registerTui(api, dependencies) {
|
|
2694
|
-
const refreshBus = createRefreshBus();
|
|
2695
|
-
api.event.on("session.updated", (event) => refreshBus.emit(event.properties.sessionID));
|
|
2696
|
-
api.event.on("message.updated", (event) => refreshBus.emit(event.properties.sessionID));
|
|
2697
|
-
api.event.on("message.part.updated", (event) => refreshBus.emit(event.properties.sessionID));
|
|
2771
|
+
const refreshBus = dependencies.refreshBus ?? createRefreshBus();
|
|
2772
|
+
const disposeEvents = [api.event.on("session.updated", (event) => refreshBus.emit(event.properties.sessionID)), api.event.on("message.updated", (event) => refreshBus.emit(event.properties.sessionID)), api.event.on("message.part.updated", (event) => refreshBus.emit(event.properties.sessionID))];
|
|
2698
2773
|
const disposeCommands = api.keymap.registerLayer({
|
|
2699
2774
|
commands: [{
|
|
2700
2775
|
name: "pr.attach",
|
|
@@ -2825,10 +2900,79 @@ function registerTui(api, dependencies) {
|
|
|
2825
2900
|
});
|
|
2826
2901
|
refreshBus.emit(sessionID);
|
|
2827
2902
|
}
|
|
2903
|
+
}, {
|
|
2904
|
+
name: "pr.sync",
|
|
2905
|
+
title: "Sync pull request status",
|
|
2906
|
+
category: "Plugin",
|
|
2907
|
+
namespace: "palette",
|
|
2908
|
+
slashName: "pr-sync",
|
|
2909
|
+
async run() {
|
|
2910
|
+
const sessionID = currentSessionID(api);
|
|
2911
|
+
if (sessionID === undefined) {
|
|
2912
|
+
api.ui.toast({
|
|
2913
|
+
variant: "warning",
|
|
2914
|
+
title: "Pull request tracker",
|
|
2915
|
+
message: "Open a session first"
|
|
2916
|
+
});
|
|
2917
|
+
return;
|
|
2918
|
+
}
|
|
2919
|
+
try {
|
|
2920
|
+
const result = await refreshBus.forceRefresh(sessionID);
|
|
2921
|
+
if (result === undefined) {
|
|
2922
|
+
api.ui.toast({
|
|
2923
|
+
variant: "warning",
|
|
2924
|
+
title: "Pull request tracker",
|
|
2925
|
+
message: "Pull request sidebar is not available"
|
|
2926
|
+
});
|
|
2927
|
+
return;
|
|
2928
|
+
}
|
|
2929
|
+
if (!result.ok) {
|
|
2930
|
+
api.ui.toast({
|
|
2931
|
+
variant: "error",
|
|
2932
|
+
title: "Pull request tracker",
|
|
2933
|
+
message: result.error.message
|
|
2934
|
+
});
|
|
2935
|
+
return;
|
|
2936
|
+
}
|
|
2937
|
+
switch (result.value) {
|
|
2938
|
+
case "refreshed":
|
|
2939
|
+
api.ui.toast({
|
|
2940
|
+
variant: "success",
|
|
2941
|
+
title: "Pull request tracker",
|
|
2942
|
+
message: "Pull request status synced"
|
|
2943
|
+
});
|
|
2944
|
+
return;
|
|
2945
|
+
case "no_attachments":
|
|
2946
|
+
api.ui.toast({
|
|
2947
|
+
variant: "info",
|
|
2948
|
+
title: "Pull request tracker",
|
|
2949
|
+
message: "No pull requests are attached"
|
|
2950
|
+
});
|
|
2951
|
+
return;
|
|
2952
|
+
case "stopped":
|
|
2953
|
+
api.ui.toast({
|
|
2954
|
+
variant: "error",
|
|
2955
|
+
title: "Pull request tracker",
|
|
2956
|
+
message: "Unable to refresh pull request status"
|
|
2957
|
+
});
|
|
2958
|
+
return;
|
|
2959
|
+
}
|
|
2960
|
+
} catch {
|
|
2961
|
+
api.ui.toast({
|
|
2962
|
+
variant: "error",
|
|
2963
|
+
title: "Pull request tracker",
|
|
2964
|
+
message: "Unable to refresh pull request status"
|
|
2965
|
+
});
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2828
2968
|
}],
|
|
2829
2969
|
bindings: []
|
|
2830
2970
|
});
|
|
2831
|
-
api.lifecycle.onDispose(
|
|
2971
|
+
api.lifecycle.onDispose(() => {
|
|
2972
|
+
disposeCommands();
|
|
2973
|
+
for (const disposeEvent of disposeEvents)
|
|
2974
|
+
disposeEvent();
|
|
2975
|
+
});
|
|
2832
2976
|
api.slots.register({
|
|
2833
2977
|
order: 250,
|
|
2834
2978
|
slots: {
|
|
@@ -2865,4 +3009,4 @@ export {
|
|
|
2865
3009
|
attachPullRequest
|
|
2866
3010
|
};
|
|
2867
3011
|
|
|
2868
|
-
//# debugId=
|
|
3012
|
+
//# debugId=4A2A207709F4E99D64756E2164756E21
|