@hcrosse/opencode-pr-tracker 0.1.0 → 0.3.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 +29 -6
- package/dist/server.js +1006 -33
- package/dist/server.js.map +9 -6
- package/dist/tui.js +2520 -890
- package/dist/tui.js.map +15 -8
- package/package.json +2 -1
package/dist/tui.js
CHANGED
|
@@ -536,8 +536,8 @@ GFS4: `);
|
|
|
536
536
|
fs2.createReadStream = createReadStream;
|
|
537
537
|
fs2.createWriteStream = createWriteStream;
|
|
538
538
|
var fs$readFile = fs2.readFile;
|
|
539
|
-
fs2.readFile =
|
|
540
|
-
function
|
|
539
|
+
fs2.readFile = readFile2;
|
|
540
|
+
function readFile2(path, options, cb) {
|
|
541
541
|
if (typeof options === "function")
|
|
542
542
|
cb = options, options = null;
|
|
543
543
|
return go$readFile(path, options, cb);
|
|
@@ -1553,46 +1553,53 @@ var require_proper_lockfile = __commonJS((exports, module) => {
|
|
|
1553
1553
|
});
|
|
1554
1554
|
|
|
1555
1555
|
// src/tui.tsx
|
|
1556
|
-
import {
|
|
1556
|
+
import { createComponent as _$createComponent4 } from "@opentui/solid";
|
|
1557
|
+
|
|
1558
|
+
// src/feedback-tui.tsx
|
|
1559
|
+
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
1560
|
+
import { effect as _$effect } from "@opentui/solid";
|
|
1557
1561
|
import { createTextNode as _$createTextNode } from "@opentui/solid";
|
|
1558
1562
|
import { insertNode as _$insertNode } from "@opentui/solid";
|
|
1559
|
-
import { setProp as _$setProp } from "@opentui/solid";
|
|
1560
|
-
import { effect as _$effect } from "@opentui/solid";
|
|
1561
1563
|
import { insert as _$insert } from "@opentui/solid";
|
|
1564
|
+
import { setProp as _$setProp } from "@opentui/solid";
|
|
1562
1565
|
import { createElement as _$createElement } from "@opentui/solid";
|
|
1563
|
-
import {
|
|
1564
|
-
import {
|
|
1565
|
-
import { createSignal, onCleanup } from "solid-js";
|
|
1566
|
-
|
|
1567
|
-
// src/github.ts
|
|
1568
|
-
import { execFile } from "child_process";
|
|
1566
|
+
import { useKeyboard } from "@opentui/solid";
|
|
1567
|
+
import { createSignal } from "solid-js";
|
|
1569
1568
|
|
|
1570
1569
|
// src/exhaustive.ts
|
|
1571
1570
|
function casesHandled(value) {
|
|
1572
1571
|
throw new Error(`Unhandled case: ${String(value)}`);
|
|
1573
1572
|
}
|
|
1574
1573
|
|
|
1574
|
+
// src/github.ts
|
|
1575
|
+
import { execFile } from "child_process";
|
|
1576
|
+
|
|
1575
1577
|
// src/url.ts
|
|
1578
|
+
var expectedPullRequestUrl = "Expected https://github.com/<owner>/<repository>/pull/<positive-integer> or github.com/<owner>/<repository>/pull/<positive-integer>";
|
|
1576
1579
|
var invalidPullRequestUrl = {
|
|
1577
1580
|
ok: false,
|
|
1578
1581
|
error: {
|
|
1579
1582
|
tag: "InvalidPullRequestUrl",
|
|
1580
|
-
message:
|
|
1583
|
+
message: expectedPullRequestUrl
|
|
1581
1584
|
}
|
|
1582
1585
|
};
|
|
1583
1586
|
var segmentPattern = /^[A-Za-z0-9._-]+$/;
|
|
1587
|
+
var schemeLessPrefix = "github.com/";
|
|
1584
1588
|
function parsePullRequestUrl(input) {
|
|
1585
|
-
if (
|
|
1589
|
+
if (/\s/.test(input))
|
|
1590
|
+
return invalidPullRequestUrl;
|
|
1591
|
+
if (input.includes("\\"))
|
|
1586
1592
|
return invalidPullRequestUrl;
|
|
1587
|
-
|
|
1593
|
+
const candidate = input.slice(0, schemeLessPrefix.length).toLowerCase() === schemeLessPrefix ? `https://${input}` : input;
|
|
1594
|
+
if (!candidate.startsWith("https://"))
|
|
1588
1595
|
return invalidPullRequestUrl;
|
|
1589
|
-
const authorityEnd =
|
|
1596
|
+
const authorityEnd = candidate.indexOf("/", "https://".length);
|
|
1590
1597
|
if (authorityEnd === -1)
|
|
1591
1598
|
return invalidPullRequestUrl;
|
|
1592
|
-
if (
|
|
1599
|
+
if (candidate.slice("https://".length, authorityEnd).toLowerCase() !== "github.com") {
|
|
1593
1600
|
return invalidPullRequestUrl;
|
|
1594
1601
|
}
|
|
1595
|
-
const rawPath =
|
|
1602
|
+
const rawPath = candidate.slice(authorityEnd).split(/[?#]/, 1).join("");
|
|
1596
1603
|
for (const segment of rawPath.split("/")) {
|
|
1597
1604
|
let decoded;
|
|
1598
1605
|
try {
|
|
@@ -1605,7 +1612,7 @@ function parsePullRequestUrl(input) {
|
|
|
1605
1612
|
}
|
|
1606
1613
|
let parsed;
|
|
1607
1614
|
try {
|
|
1608
|
-
parsed = new URL(
|
|
1615
|
+
parsed = new URL(candidate);
|
|
1609
1616
|
} catch {
|
|
1610
1617
|
return invalidPullRequestUrl;
|
|
1611
1618
|
}
|
|
@@ -1616,15 +1623,17 @@ function parsePullRequestUrl(input) {
|
|
|
1616
1623
|
if (segments.length !== 5 || segments[0] !== "" || segments[3] !== "pull") {
|
|
1617
1624
|
return invalidPullRequestUrl;
|
|
1618
1625
|
}
|
|
1619
|
-
const
|
|
1620
|
-
const
|
|
1626
|
+
const rawOwner = segments[1];
|
|
1627
|
+
const rawRepository = segments[2];
|
|
1621
1628
|
const numberText = segments[4];
|
|
1622
|
-
if (
|
|
1629
|
+
if (rawOwner === undefined || rawRepository === undefined || numberText === undefined || !segmentPattern.test(rawOwner) || !segmentPattern.test(rawRepository) || !/^\d+$/.test(numberText)) {
|
|
1623
1630
|
return invalidPullRequestUrl;
|
|
1624
1631
|
}
|
|
1625
1632
|
const number = Number(numberText);
|
|
1626
1633
|
if (!Number.isSafeInteger(number) || number <= 0)
|
|
1627
1634
|
return invalidPullRequestUrl;
|
|
1635
|
+
const owner = rawOwner.toLowerCase();
|
|
1636
|
+
const repository = rawRepository.toLowerCase();
|
|
1628
1637
|
const url = `https://github.com/${owner}/${repository}/pull/${number}`;
|
|
1629
1638
|
const value = { url, owner, repository, number };
|
|
1630
1639
|
return { ok: true, value };
|
|
@@ -1641,6 +1650,13 @@ var invalidGitHubResponse = {
|
|
|
1641
1650
|
message: "GitHub returned an invalid pull request response"
|
|
1642
1651
|
}
|
|
1643
1652
|
};
|
|
1653
|
+
var pullRequestNotFound = {
|
|
1654
|
+
ok: false,
|
|
1655
|
+
error: {
|
|
1656
|
+
tag: "PullRequestNotFound",
|
|
1657
|
+
message: "Pull request does not exist or is not accessible"
|
|
1658
|
+
}
|
|
1659
|
+
};
|
|
1644
1660
|
var githubBatchLimitExceeded = {
|
|
1645
1661
|
ok: false,
|
|
1646
1662
|
error: {
|
|
@@ -1649,15 +1665,13 @@ var githubBatchLimitExceeded = {
|
|
|
1649
1665
|
message: "GitHub batch cannot contain more than 20 pull requests"
|
|
1650
1666
|
}
|
|
1651
1667
|
};
|
|
1652
|
-
var checkRunPending = new Set(["QUEUED", "IN_PROGRESS", "WAITING", "PENDING"]);
|
|
1653
|
-
var checkRunPassed = new Set(["SUCCESS"]);
|
|
1654
|
-
var checkRunFailed = new Set(["FAILURE", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED", "STARTUP_FAILURE", "STALE"]);
|
|
1655
|
-
var checkRunIgnored = new Set(["NEUTRAL", "SKIPPED"]);
|
|
1656
|
-
var statusContextPending = new Set(["EXPECTED", "PENDING"]);
|
|
1657
|
-
var statusContextPassed = new Set(["SUCCESS"]);
|
|
1658
|
-
var statusContextFailed = new Set(["ERROR", "FAILURE"]);
|
|
1659
1668
|
var maximumPullRequestsPerBatch = 20;
|
|
1660
|
-
var
|
|
1669
|
+
var maximumCheckContextsPerPage = 100;
|
|
1670
|
+
var statusContextOnlyFields = ["context", "state", "createdAt"];
|
|
1671
|
+
var checkRunOnlyFields = ["name", "status", "conclusion", "checkSuite"];
|
|
1672
|
+
var checkContextSelection = `nodes { __typename ... on StatusContext { id context state createdAt } ... on CheckRun { id name status conclusion checkSuite { id createdAt app { id } workflowRun { event runNumber runAttempt workflow { id } } } } } totalCount pageInfo { hasNextPage endCursor }`;
|
|
1673
|
+
var pullRequestSelection = `__typename ... on PullRequest { title state url mergedAt mergeable mergeStateStatus baseRef { branchProtectionRule { requiresStatusChecks requiresStrictStatusChecks } refUpdateRule { requiredStatusCheckContexts } rules(first: 100) { nodes { parameters { __typename ... on RequiredStatusChecksParameters { strictRequiredStatusChecksPolicy requiredStatusChecks { context } } } } totalCount pageInfo { hasNextPage } } } statusCheckRollup { contexts(first: ${maximumCheckContextsPerPage}) { ${checkContextSelection} } } }`;
|
|
1674
|
+
var continuationQuery = `query PullRequestContexts($url: URI!, $cursor: String!) { resource(url: $url) { __typename ... on PullRequest { url statusCheckRollup { contexts(first: ${maximumCheckContextsPerPage}, after: $cursor) { ${checkContextSelection} } } } } }`;
|
|
1661
1675
|
function isRecord(value) {
|
|
1662
1676
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1663
1677
|
}
|
|
@@ -1673,67 +1687,297 @@ function parseProcessExecutionFailed(value) {
|
|
|
1673
1687
|
cause: value.cause
|
|
1674
1688
|
};
|
|
1675
1689
|
}
|
|
1676
|
-
function
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
if (
|
|
1684
|
-
return
|
|
1685
|
-
|
|
1690
|
+
function parseNonBlankString(input) {
|
|
1691
|
+
return typeof input === "string" && input.trim() !== "" ? input : undefined;
|
|
1692
|
+
}
|
|
1693
|
+
function parseDate(input) {
|
|
1694
|
+
if (typeof input !== "string")
|
|
1695
|
+
return;
|
|
1696
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})[Tt](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(?:[Zz]|[+-](\d{2}):(\d{2}))$/.exec(input);
|
|
1697
|
+
if (match === null)
|
|
1698
|
+
return;
|
|
1699
|
+
const year = Number(match[1]);
|
|
1700
|
+
const month = Number(match[2]);
|
|
1701
|
+
const day = Number(match[3]);
|
|
1702
|
+
const hour = Number(match[4]);
|
|
1703
|
+
const minute = Number(match[5]);
|
|
1704
|
+
const second = Number(match[6]);
|
|
1705
|
+
const offsetHour = Number(match[8] ?? 0);
|
|
1706
|
+
const offsetMinute = Number(match[9] ?? 0);
|
|
1707
|
+
const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
1708
|
+
const daysInMonth = [31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1];
|
|
1709
|
+
if (daysInMonth === undefined || day < 1 || day > daysInMonth || hour > 23 || minute > 59 || second > 59 || offsetHour > 23 || offsetMinute > 59) {
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
if (Number.isNaN(new Date(input).valueOf()))
|
|
1713
|
+
return;
|
|
1714
|
+
const epochSeconds = new Date(input.replace(/\.\d+/, "")).valueOf() / 1000;
|
|
1715
|
+
if (!Number.isInteger(epochSeconds))
|
|
1716
|
+
return;
|
|
1717
|
+
return {
|
|
1718
|
+
epochSeconds,
|
|
1719
|
+
fractionalSeconds: (match[7] ?? "").replace(/0+$/, "")
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
function compareTimestamps(left, right) {
|
|
1723
|
+
if (left.epochSeconds < right.epochSeconds)
|
|
1724
|
+
return -1;
|
|
1725
|
+
if (left.epochSeconds > right.epochSeconds)
|
|
1726
|
+
return 1;
|
|
1727
|
+
const width = Math.max(left.fractionalSeconds.length, right.fractionalSeconds.length);
|
|
1728
|
+
const leftFraction = left.fractionalSeconds.padEnd(width, "0");
|
|
1729
|
+
const rightFraction = right.fractionalSeconds.padEnd(width, "0");
|
|
1730
|
+
if (leftFraction < rightFraction)
|
|
1731
|
+
return -1;
|
|
1732
|
+
if (leftFraction > rightFraction)
|
|
1733
|
+
return 1;
|
|
1734
|
+
return 0;
|
|
1735
|
+
}
|
|
1736
|
+
function parseStatusContextState(input) {
|
|
1737
|
+
switch (input) {
|
|
1738
|
+
case "EXPECTED":
|
|
1739
|
+
case "PENDING":
|
|
1740
|
+
case "SUCCESS":
|
|
1741
|
+
case "ERROR":
|
|
1742
|
+
case "FAILURE":
|
|
1743
|
+
return input;
|
|
1744
|
+
default:
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
function parseCheckRunStatus(input) {
|
|
1749
|
+
switch (input) {
|
|
1750
|
+
case "REQUESTED":
|
|
1751
|
+
case "QUEUED":
|
|
1752
|
+
case "IN_PROGRESS":
|
|
1753
|
+
case "COMPLETED":
|
|
1754
|
+
case "WAITING":
|
|
1755
|
+
case "PENDING":
|
|
1756
|
+
return input;
|
|
1757
|
+
default:
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
function parseCheckRunConclusion(input) {
|
|
1762
|
+
switch (input) {
|
|
1763
|
+
case null:
|
|
1764
|
+
case "SUCCESS":
|
|
1765
|
+
case "FAILURE":
|
|
1766
|
+
case "CANCELLED":
|
|
1767
|
+
case "TIMED_OUT":
|
|
1768
|
+
case "ACTION_REQUIRED":
|
|
1769
|
+
case "STARTUP_FAILURE":
|
|
1770
|
+
case "STALE":
|
|
1771
|
+
case "NEUTRAL":
|
|
1772
|
+
case "SKIPPED":
|
|
1773
|
+
return input;
|
|
1774
|
+
default:
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1686
1777
|
}
|
|
1687
|
-
function
|
|
1688
|
-
|
|
1778
|
+
function parseStatusContext(input) {
|
|
1779
|
+
const id = parseNonBlankString(input.id);
|
|
1780
|
+
const context = parseNonBlankString(input.context);
|
|
1781
|
+
const state = parseStatusContextState(input.state);
|
|
1782
|
+
const createdAt = parseDate(input.createdAt);
|
|
1783
|
+
if (id === undefined || context === undefined || state === undefined || createdAt === undefined) {
|
|
1689
1784
|
return invalidGitHubResponse;
|
|
1690
1785
|
}
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1786
|
+
return { ok: true, value: { tag: "StatusContext", id, context, state, createdAt } };
|
|
1787
|
+
}
|
|
1788
|
+
function parseCheckRun(input) {
|
|
1789
|
+
const id = parseNonBlankString(input.id);
|
|
1790
|
+
const name = parseNonBlankString(input.name);
|
|
1791
|
+
const status = parseCheckRunStatus(input.status);
|
|
1792
|
+
const conclusion = parseCheckRunConclusion(input.conclusion);
|
|
1793
|
+
if (id === undefined || name === undefined || status === undefined || conclusion === undefined || status !== "COMPLETED" && conclusion !== null || !isRecord(input.checkSuite)) {
|
|
1794
|
+
return invalidGitHubResponse;
|
|
1795
|
+
}
|
|
1796
|
+
const suiteId = parseNonBlankString(input.checkSuite.id);
|
|
1797
|
+
const suiteCreatedAt = parseDate(input.checkSuite.createdAt);
|
|
1798
|
+
if (suiteId === undefined || suiteCreatedAt === undefined)
|
|
1799
|
+
return invalidGitHubResponse;
|
|
1800
|
+
let sourceIdentity;
|
|
1801
|
+
if (input.checkSuite.app === null) {
|
|
1802
|
+
sourceIdentity = ["suite", suiteId];
|
|
1803
|
+
} else {
|
|
1804
|
+
if (!isRecord(input.checkSuite.app))
|
|
1805
|
+
return invalidGitHubResponse;
|
|
1806
|
+
const appId = parseNonBlankString(input.checkSuite.app.id);
|
|
1807
|
+
if (appId === undefined)
|
|
1808
|
+
return invalidGitHubResponse;
|
|
1809
|
+
sourceIdentity = ["app", appId];
|
|
1810
|
+
}
|
|
1811
|
+
let workflowRun;
|
|
1812
|
+
if (input.checkSuite.workflowRun === null) {
|
|
1813
|
+
workflowRun = undefined;
|
|
1814
|
+
} else {
|
|
1815
|
+
if (!isRecord(input.checkSuite.workflowRun) || !isRecord(input.checkSuite.workflowRun.workflow)) {
|
|
1696
1816
|
return invalidGitHubResponse;
|
|
1697
1817
|
}
|
|
1698
|
-
const
|
|
1699
|
-
|
|
1818
|
+
const event = parseNonBlankString(input.checkSuite.workflowRun.event);
|
|
1819
|
+
const workflowId = parseNonBlankString(input.checkSuite.workflowRun.workflow.id);
|
|
1820
|
+
const runNumber = input.checkSuite.workflowRun.runNumber;
|
|
1821
|
+
const runAttempt = input.checkSuite.workflowRun.runAttempt;
|
|
1822
|
+
if (event === undefined || workflowId === undefined || !Number.isInteger(runNumber) || Number(runNumber) <= 0 || !Number.isInteger(runAttempt) || Number(runAttempt) <= 0) {
|
|
1700
1823
|
return invalidGitHubResponse;
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
if (bucket !== undefined && Number(item.count) > 0)
|
|
1704
|
-
buckets.add(bucket);
|
|
1824
|
+
}
|
|
1825
|
+
workflowRun = { event, workflowId, runNumber: Number(runNumber), runAttempt: Number(runAttempt) };
|
|
1705
1826
|
}
|
|
1706
|
-
return
|
|
1827
|
+
return {
|
|
1828
|
+
ok: true,
|
|
1829
|
+
value: { tag: "CheckRun", id, name, status, conclusion, suiteId, suiteCreatedAt, sourceIdentity, workflowRun }
|
|
1830
|
+
};
|
|
1707
1831
|
}
|
|
1708
|
-
function
|
|
1709
|
-
if (input
|
|
1710
|
-
return { ok: true, value: "none" };
|
|
1711
|
-
if (!isRecord(input) || !isRecord(input.contexts))
|
|
1832
|
+
function parseCheckContexts(input) {
|
|
1833
|
+
if (!isRecord(input) || !Number.isInteger(input.totalCount) || Number(input.totalCount) < 0 || !isRecord(input.pageInfo) || typeof input.pageInfo.hasNextPage !== "boolean" || input.pageInfo.endCursor !== null && typeof input.pageInfo.endCursor !== "string") {
|
|
1712
1834
|
return invalidGitHubResponse;
|
|
1713
|
-
|
|
1714
|
-
const
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1835
|
+
}
|
|
1836
|
+
const nodes = input.nodes === null && input.totalCount === 0 ? [] : input.nodes;
|
|
1837
|
+
if (!Array.isArray(nodes) || nodes.length > maximumCheckContextsPerPage || nodes.length > Number(input.totalCount)) {
|
|
1838
|
+
return invalidGitHubResponse;
|
|
1839
|
+
}
|
|
1840
|
+
const contexts = [];
|
|
1841
|
+
const ids = new Set;
|
|
1842
|
+
for (const node of nodes) {
|
|
1843
|
+
if (!isRecord(node))
|
|
1844
|
+
return invalidGitHubResponse;
|
|
1845
|
+
let parsed;
|
|
1846
|
+
switch (node.__typename) {
|
|
1847
|
+
case "StatusContext":
|
|
1848
|
+
if (checkRunOnlyFields.some((field) => (field in node)))
|
|
1849
|
+
return invalidGitHubResponse;
|
|
1850
|
+
parsed = parseStatusContext(node);
|
|
1851
|
+
break;
|
|
1852
|
+
case "CheckRun":
|
|
1853
|
+
if (statusContextOnlyFields.some((field) => (field in node)))
|
|
1854
|
+
return invalidGitHubResponse;
|
|
1855
|
+
parsed = parseCheckRun(node);
|
|
1856
|
+
break;
|
|
1857
|
+
default:
|
|
1858
|
+
return invalidGitHubResponse;
|
|
1859
|
+
}
|
|
1860
|
+
if (!parsed.ok || ids.has(parsed.value.id))
|
|
1861
|
+
return invalidGitHubResponse;
|
|
1862
|
+
ids.add(parsed.value.id);
|
|
1863
|
+
contexts.push(parsed.value);
|
|
1864
|
+
}
|
|
1865
|
+
const nextCursor = input.pageInfo.hasNextPage ? parseNonBlankString(input.pageInfo.endCursor) : undefined;
|
|
1866
|
+
if (input.pageInfo.hasNextPage && nextCursor === undefined)
|
|
1867
|
+
return invalidGitHubResponse;
|
|
1868
|
+
if (nextCursor !== undefined && contexts.length === 0)
|
|
1869
|
+
return invalidGitHubResponse;
|
|
1870
|
+
return {
|
|
1871
|
+
ok: true,
|
|
1872
|
+
value: {
|
|
1873
|
+
contexts,
|
|
1874
|
+
totalCount: Number(input.totalCount),
|
|
1875
|
+
...nextCursor === undefined ? {} : { nextCursor }
|
|
1876
|
+
}
|
|
1877
|
+
};
|
|
1878
|
+
}
|
|
1879
|
+
function classifyStatusContext(state) {
|
|
1880
|
+
switch (state) {
|
|
1881
|
+
case "ERROR":
|
|
1882
|
+
case "FAILURE":
|
|
1883
|
+
return "failed";
|
|
1884
|
+
case "EXPECTED":
|
|
1885
|
+
case "PENDING":
|
|
1886
|
+
return "pending";
|
|
1887
|
+
case "SUCCESS":
|
|
1888
|
+
return "passed";
|
|
1889
|
+
default:
|
|
1890
|
+
return casesHandled(state);
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
function classifyCheckRun(checkRun) {
|
|
1894
|
+
if (checkRun.status !== "COMPLETED")
|
|
1895
|
+
return "pending";
|
|
1896
|
+
switch (checkRun.conclusion) {
|
|
1897
|
+
case "FAILURE":
|
|
1898
|
+
case "CANCELLED":
|
|
1899
|
+
case "TIMED_OUT":
|
|
1900
|
+
case "ACTION_REQUIRED":
|
|
1901
|
+
case "STARTUP_FAILURE":
|
|
1902
|
+
case "STALE":
|
|
1903
|
+
return "failed";
|
|
1904
|
+
case "SUCCESS":
|
|
1905
|
+
return "passed";
|
|
1906
|
+
case "NEUTRAL":
|
|
1907
|
+
case "SKIPPED":
|
|
1908
|
+
case null:
|
|
1909
|
+
return "ignored";
|
|
1910
|
+
default:
|
|
1911
|
+
return casesHandled(checkRun.conclusion);
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
function classifyContexts(contexts) {
|
|
1915
|
+
const statusContexts = new Map;
|
|
1916
|
+
const workflowChecks = new Map;
|
|
1917
|
+
const nonWorkflowChecks = new Map;
|
|
1918
|
+
for (const context of contexts) {
|
|
1919
|
+
if (context.tag === "StatusContext") {
|
|
1920
|
+
const identity2 = context.context.toLowerCase();
|
|
1921
|
+
const existing2 = statusContexts.get(identity2);
|
|
1922
|
+
const bucket2 = classifyStatusContext(context.state);
|
|
1923
|
+
const ordering2 = existing2 === undefined ? 1 : compareTimestamps(context.createdAt, existing2.createdAt);
|
|
1924
|
+
if (ordering2 > 0) {
|
|
1925
|
+
statusContexts.set(identity2, { createdAt: context.createdAt, buckets: [bucket2] });
|
|
1926
|
+
} else if (ordering2 === 0 && existing2 !== undefined) {
|
|
1927
|
+
existing2.buckets.push(bucket2);
|
|
1928
|
+
}
|
|
1929
|
+
continue;
|
|
1930
|
+
}
|
|
1931
|
+
const bucket = classifyCheckRun(context);
|
|
1932
|
+
if (context.workflowRun !== undefined) {
|
|
1933
|
+
const identity2 = JSON.stringify([
|
|
1934
|
+
"workflow",
|
|
1935
|
+
context.sourceIdentity,
|
|
1936
|
+
context.workflowRun.workflowId,
|
|
1937
|
+
context.workflowRun.event,
|
|
1938
|
+
context.name
|
|
1939
|
+
]);
|
|
1940
|
+
const existing2 = workflowChecks.get(identity2);
|
|
1941
|
+
const isNewer = existing2 === undefined || context.workflowRun.runNumber > existing2.runNumber || context.workflowRun.runNumber === existing2.runNumber && context.workflowRun.runAttempt > existing2.runAttempt;
|
|
1942
|
+
if (isNewer) {
|
|
1943
|
+
workflowChecks.set(identity2, {
|
|
1944
|
+
runNumber: context.workflowRun.runNumber,
|
|
1945
|
+
runAttempt: context.workflowRun.runAttempt,
|
|
1946
|
+
buckets: [bucket]
|
|
1947
|
+
});
|
|
1948
|
+
} else if (context.workflowRun.runNumber === existing2.runNumber && context.workflowRun.runAttempt === existing2.runAttempt) {
|
|
1949
|
+
existing2.buckets.push(bucket);
|
|
1950
|
+
}
|
|
1951
|
+
continue;
|
|
1952
|
+
}
|
|
1953
|
+
const identity = JSON.stringify(["check", context.sourceIdentity, context.name]);
|
|
1954
|
+
const existing = nonWorkflowChecks.get(identity);
|
|
1955
|
+
const ordering = existing === undefined ? 1 : compareTimestamps(context.suiteCreatedAt, existing.suiteCreatedAt);
|
|
1956
|
+
if (ordering > 0) {
|
|
1957
|
+
nonWorkflowChecks.set(identity, { suiteCreatedAt: context.suiteCreatedAt, buckets: [bucket] });
|
|
1958
|
+
} else if (ordering === 0 && existing !== undefined) {
|
|
1959
|
+
existing.buckets.push(bucket);
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
const buckets = new Set;
|
|
1963
|
+
for (const selection of [...statusContexts.values(), ...workflowChecks.values(), ...nonWorkflowChecks.values()]) {
|
|
1964
|
+
for (const bucket of selection.buckets)
|
|
1965
|
+
buckets.add(bucket);
|
|
1966
|
+
}
|
|
1730
1967
|
if (buckets.has("failed"))
|
|
1731
|
-
return
|
|
1968
|
+
return "failed";
|
|
1732
1969
|
if (buckets.has("pending"))
|
|
1733
|
-
return
|
|
1970
|
+
return "pending";
|
|
1734
1971
|
if (buckets.has("passed"))
|
|
1735
|
-
return
|
|
1736
|
-
return
|
|
1972
|
+
return "passed";
|
|
1973
|
+
return "none";
|
|
1974
|
+
}
|
|
1975
|
+
function parseStatusCheckRollup(input) {
|
|
1976
|
+
if (input === null)
|
|
1977
|
+
return { ok: true, value: null };
|
|
1978
|
+
if (!isRecord(input))
|
|
1979
|
+
return invalidGitHubResponse;
|
|
1980
|
+
return parseCheckContexts(input.contexts);
|
|
1737
1981
|
}
|
|
1738
1982
|
function samePullRequest(left, right) {
|
|
1739
1983
|
return left.number === right.number && left.owner.toLowerCase() === right.owner.toLowerCase() && left.repository.toLowerCase() === right.repository.toLowerCase();
|
|
@@ -1750,7 +1994,118 @@ function parseMergeability(input) {
|
|
|
1750
1994
|
return invalidGitHubResponse;
|
|
1751
1995
|
}
|
|
1752
1996
|
}
|
|
1753
|
-
function
|
|
1997
|
+
function parseMergeStateStatus(input) {
|
|
1998
|
+
switch (input) {
|
|
1999
|
+
case "BEHIND":
|
|
2000
|
+
return { ok: true, value: "behind" };
|
|
2001
|
+
case "BLOCKED":
|
|
2002
|
+
case "CLEAN":
|
|
2003
|
+
case "DIRTY":
|
|
2004
|
+
case "DRAFT":
|
|
2005
|
+
case "HAS_HOOKS":
|
|
2006
|
+
case "UNKNOWN":
|
|
2007
|
+
case "UNSTABLE":
|
|
2008
|
+
return { ok: true, value: "other" };
|
|
2009
|
+
default:
|
|
2010
|
+
return invalidGitHubResponse;
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
function parseRequiredStatusChecks(input) {
|
|
2014
|
+
if (!Array.isArray(input))
|
|
2015
|
+
return invalidGitHubResponse;
|
|
2016
|
+
for (const check of input) {
|
|
2017
|
+
if (!isRecord(check) || typeof check.context !== "string" || check.context.trim() === "") {
|
|
2018
|
+
return invalidGitHubResponse;
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
return { ok: true, value: input.length > 0 };
|
|
2022
|
+
}
|
|
2023
|
+
function parseRules(input) {
|
|
2024
|
+
if (!isRecord(input) || !Number.isInteger(input.totalCount) || Number(input.totalCount) < 0 || !isRecord(input.pageInfo) || typeof input.pageInfo.hasNextPage !== "boolean") {
|
|
2025
|
+
return invalidGitHubResponse;
|
|
2026
|
+
}
|
|
2027
|
+
const nodes = input.nodes === null && input.totalCount === 0 ? [] : input.nodes;
|
|
2028
|
+
if (!Array.isArray(nodes) || nodes.length > Number(input.totalCount))
|
|
2029
|
+
return invalidGitHubResponse;
|
|
2030
|
+
if (input.pageInfo.hasNextPage ? nodes.length >= Number(input.totalCount) : nodes.length !== input.totalCount) {
|
|
2031
|
+
return invalidGitHubResponse;
|
|
2032
|
+
}
|
|
2033
|
+
let strict = false;
|
|
2034
|
+
for (const node of nodes) {
|
|
2035
|
+
if (!isRecord(node) || !(node.parameters === null || isRecord(node.parameters))) {
|
|
2036
|
+
return invalidGitHubResponse;
|
|
2037
|
+
}
|
|
2038
|
+
if (node.parameters === null)
|
|
2039
|
+
continue;
|
|
2040
|
+
if (typeof node.parameters.__typename !== "string")
|
|
2041
|
+
return invalidGitHubResponse;
|
|
2042
|
+
if (node.parameters.__typename !== "RequiredStatusChecksParameters")
|
|
2043
|
+
continue;
|
|
2044
|
+
if (typeof node.parameters.strictRequiredStatusChecksPolicy !== "boolean")
|
|
2045
|
+
return invalidGitHubResponse;
|
|
2046
|
+
const requiredChecks = parseRequiredStatusChecks(node.parameters.requiredStatusChecks);
|
|
2047
|
+
if (!requiredChecks.ok)
|
|
2048
|
+
return requiredChecks;
|
|
2049
|
+
if (node.parameters.strictRequiredStatusChecksPolicy && requiredChecks.value)
|
|
2050
|
+
strict = true;
|
|
2051
|
+
}
|
|
2052
|
+
return { ok: true, value: { strict, incomplete: input.pageInfo.hasNextPage } };
|
|
2053
|
+
}
|
|
2054
|
+
function parseRefUpdateRule(input) {
|
|
2055
|
+
if (input === null)
|
|
2056
|
+
return { ok: true, value: false };
|
|
2057
|
+
if (!isRecord(input))
|
|
2058
|
+
return invalidGitHubResponse;
|
|
2059
|
+
if (input.requiredStatusCheckContexts === null)
|
|
2060
|
+
return { ok: true, value: false };
|
|
2061
|
+
if (!Array.isArray(input.requiredStatusCheckContexts))
|
|
2062
|
+
return invalidGitHubResponse;
|
|
2063
|
+
for (const context of input.requiredStatusCheckContexts) {
|
|
2064
|
+
if (typeof context !== "string" || context.trim() === "")
|
|
2065
|
+
return invalidGitHubResponse;
|
|
2066
|
+
}
|
|
2067
|
+
return { ok: true, value: input.requiredStatusCheckContexts.length > 0 };
|
|
2068
|
+
}
|
|
2069
|
+
function parseUpdatePolicy(input) {
|
|
2070
|
+
if (input === null)
|
|
2071
|
+
return { ok: true, value: { strict: false, incomplete: false } };
|
|
2072
|
+
if (!isRecord(input))
|
|
2073
|
+
return invalidGitHubResponse;
|
|
2074
|
+
const refUpdateHasRequiredChecks = parseRefUpdateRule(input.refUpdateRule);
|
|
2075
|
+
if (!refUpdateHasRequiredChecks.ok)
|
|
2076
|
+
return refUpdateHasRequiredChecks;
|
|
2077
|
+
let branchProtectionIsStrict = false;
|
|
2078
|
+
if (input.branchProtectionRule !== null) {
|
|
2079
|
+
if (!isRecord(input.branchProtectionRule) || typeof input.branchProtectionRule.requiresStatusChecks !== "boolean" || typeof input.branchProtectionRule.requiresStrictStatusChecks !== "boolean") {
|
|
2080
|
+
return invalidGitHubResponse;
|
|
2081
|
+
}
|
|
2082
|
+
branchProtectionIsStrict = input.branchProtectionRule.requiresStatusChecks && input.branchProtectionRule.requiresStrictStatusChecks;
|
|
2083
|
+
}
|
|
2084
|
+
const rules = parseRules(input.rules);
|
|
2085
|
+
if (!rules.ok)
|
|
2086
|
+
return rules;
|
|
2087
|
+
return {
|
|
2088
|
+
ok: true,
|
|
2089
|
+
value: {
|
|
2090
|
+
strict: branchProtectionIsStrict || rules.value.strict,
|
|
2091
|
+
incomplete: rules.value.incomplete || input.branchProtectionRule === null && refUpdateHasRequiredChecks.value
|
|
2092
|
+
}
|
|
2093
|
+
};
|
|
2094
|
+
}
|
|
2095
|
+
function parseBlocker(mergeStateStatusInput, baseRefInput) {
|
|
2096
|
+
const mergeStateStatus = parseMergeStateStatus(mergeStateStatusInput);
|
|
2097
|
+
if (!mergeStateStatus.ok)
|
|
2098
|
+
return mergeStateStatus;
|
|
2099
|
+
if (mergeStateStatus.value !== "behind")
|
|
2100
|
+
return { ok: true, value: "none" };
|
|
2101
|
+
const updatePolicy = parseUpdatePolicy(baseRefInput);
|
|
2102
|
+
if (!updatePolicy.ok)
|
|
2103
|
+
return updatePolicy;
|
|
2104
|
+
if (updatePolicy.value.strict)
|
|
2105
|
+
return { ok: true, value: "behind" };
|
|
2106
|
+
return updatePolicy.value.incomplete ? invalidGitHubResponse : { ok: true, value: "none" };
|
|
2107
|
+
}
|
|
2108
|
+
function parsePullRequestMetadata(input, pullRequest) {
|
|
1754
2109
|
if (!isRecord(input) || input.__typename !== "PullRequest" || typeof input.title !== "string" || input.title.trim() === "") {
|
|
1755
2110
|
return invalidGitHubResponse;
|
|
1756
2111
|
}
|
|
@@ -1771,17 +2126,34 @@ function parseResponse(input, pullRequest) {
|
|
|
1771
2126
|
const responseUrl = parsePullRequestUrl(input.url);
|
|
1772
2127
|
if (!responseUrl.ok || !samePullRequest(responseUrl.value, pullRequest))
|
|
1773
2128
|
return invalidGitHubResponse;
|
|
1774
|
-
const ci = parseStatusCheckRollup(input.statusCheckRollup);
|
|
1775
|
-
if (!ci.ok)
|
|
1776
|
-
return ci;
|
|
1777
2129
|
const mergeability = parseMergeability(input.mergeable);
|
|
1778
2130
|
if (!mergeability.ok)
|
|
1779
2131
|
return mergeability;
|
|
2132
|
+
return {
|
|
2133
|
+
ok: true,
|
|
2134
|
+
value: {
|
|
2135
|
+
title: input.title,
|
|
2136
|
+
state: input.state,
|
|
2137
|
+
mergeability: mergeability.value,
|
|
2138
|
+
mergeStateStatus: input.mergeStateStatus,
|
|
2139
|
+
baseRef: input.baseRef
|
|
2140
|
+
}
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
2143
|
+
function finalizeResponse(metadata, pullRequest, ci) {
|
|
1780
2144
|
let state;
|
|
1781
|
-
switch (
|
|
1782
|
-
case "OPEN":
|
|
1783
|
-
|
|
2145
|
+
switch (metadata.state) {
|
|
2146
|
+
case "OPEN": {
|
|
2147
|
+
let blocker = "none";
|
|
2148
|
+
if (metadata.mergeability !== "conflicting" && (ci === "none" || ci === "passed")) {
|
|
2149
|
+
const parsedBlocker = parseBlocker(metadata.mergeStateStatus, metadata.baseRef);
|
|
2150
|
+
if (!parsedBlocker.ok)
|
|
2151
|
+
return parsedBlocker;
|
|
2152
|
+
blocker = parsedBlocker.value;
|
|
2153
|
+
}
|
|
2154
|
+
state = { tag: "Open", ci, mergeability: metadata.mergeability, blocker };
|
|
1784
2155
|
break;
|
|
2156
|
+
}
|
|
1785
2157
|
case "MERGED":
|
|
1786
2158
|
state = { tag: "Merged" };
|
|
1787
2159
|
break;
|
|
@@ -1789,19 +2161,33 @@ function parseResponse(input, pullRequest) {
|
|
|
1789
2161
|
state = { tag: "Closed" };
|
|
1790
2162
|
break;
|
|
1791
2163
|
default:
|
|
1792
|
-
return casesHandled(
|
|
2164
|
+
return casesHandled(metadata.state);
|
|
1793
2165
|
}
|
|
1794
2166
|
return {
|
|
1795
2167
|
ok: true,
|
|
1796
2168
|
value: {
|
|
1797
2169
|
tag: "Available",
|
|
1798
2170
|
pullRequest,
|
|
1799
|
-
title:
|
|
2171
|
+
title: metadata.title,
|
|
1800
2172
|
state,
|
|
1801
2173
|
stale: false
|
|
1802
2174
|
}
|
|
1803
2175
|
};
|
|
1804
2176
|
}
|
|
2177
|
+
function parseInitialPullRequest(input, pullRequest) {
|
|
2178
|
+
if (input === null)
|
|
2179
|
+
return pullRequestNotFound;
|
|
2180
|
+
if (!isRecord(input))
|
|
2181
|
+
return invalidGitHubResponse;
|
|
2182
|
+
const contextPage = parseStatusCheckRollup(input.statusCheckRollup);
|
|
2183
|
+
if (!contextPage.ok)
|
|
2184
|
+
return contextPage;
|
|
2185
|
+
if (contextPage.value !== null && contextPage.value.nextCursor === undefined && contextPage.value.contexts.length !== contextPage.value.totalCount) {
|
|
2186
|
+
return invalidGitHubResponse;
|
|
2187
|
+
}
|
|
2188
|
+
const metadata = parsePullRequestMetadata(input, pullRequest);
|
|
2189
|
+
return metadata.ok ? { ok: true, value: { pullRequest, metadata: metadata.value, contextPage: contextPage.value } } : metadata;
|
|
2190
|
+
}
|
|
1805
2191
|
function createBatchQuery(size) {
|
|
1806
2192
|
const variables = Array.from({ length: size }, (_, index) => `$url${index}: URI!`).join(", ");
|
|
1807
2193
|
const fields = Array.from({ length: size }, (_, index) => `pr${index}: resource(url: $url${index}) { ${pullRequestSelection} }`).join(" ");
|
|
@@ -1836,9 +2222,22 @@ function parseBatchResponse(input, pullRequests) {
|
|
|
1836
2222
|
return errorAliases;
|
|
1837
2223
|
return {
|
|
1838
2224
|
ok: true,
|
|
1839
|
-
value: pullRequests.map((pullRequest, index) => errorAliases.value.has(index) ? invalidGitHubResponse :
|
|
2225
|
+
value: pullRequests.map((pullRequest, index) => errorAliases.value.has(index) ? invalidGitHubResponse : parseInitialPullRequest(data[`pr${index}`], pullRequest))
|
|
1840
2226
|
};
|
|
1841
2227
|
}
|
|
2228
|
+
function parseContinuationResponse(input, pullRequest) {
|
|
2229
|
+
if (!isRecord(input) || input.errors !== undefined && (!Array.isArray(input.errors) || input.errors.length > 0) || !isRecord(input.data) || !isRecord(input.data.resource)) {
|
|
2230
|
+
return invalidGitHubResponse;
|
|
2231
|
+
}
|
|
2232
|
+
const resource = input.data.resource;
|
|
2233
|
+
if (resource.__typename !== "PullRequest" || typeof resource.url !== "string")
|
|
2234
|
+
return invalidGitHubResponse;
|
|
2235
|
+
const responseUrl = parsePullRequestUrl(resource.url);
|
|
2236
|
+
if (!responseUrl.ok || !samePullRequest(responseUrl.value, pullRequest) || !isRecord(resource.statusCheckRollup)) {
|
|
2237
|
+
return invalidGitHubResponse;
|
|
2238
|
+
}
|
|
2239
|
+
return parseCheckContexts(resource.statusCheckRollup.contexts);
|
|
2240
|
+
}
|
|
1842
2241
|
function isCancellation(cause, signal) {
|
|
1843
2242
|
if (signal?.aborted)
|
|
1844
2243
|
return true;
|
|
@@ -1867,6 +2266,94 @@ function processFailureStdout(cause) {
|
|
|
1867
2266
|
return;
|
|
1868
2267
|
return cause.stdout;
|
|
1869
2268
|
}
|
|
2269
|
+
async function runAndDecode(runner, args, options) {
|
|
2270
|
+
let stdout;
|
|
2271
|
+
let processFailure;
|
|
2272
|
+
try {
|
|
2273
|
+
const output = await runner("gh", args, options);
|
|
2274
|
+
stdout = output.stdout;
|
|
2275
|
+
} catch (cause) {
|
|
2276
|
+
if (isCancellation(cause, options.signal)) {
|
|
2277
|
+
return {
|
|
2278
|
+
ok: false,
|
|
2279
|
+
error: {
|
|
2280
|
+
tag: "GitHubCancelled",
|
|
2281
|
+
message: "GitHub status request cancelled",
|
|
2282
|
+
cause
|
|
2283
|
+
}
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
processFailure = classifyProcessFailure(cause);
|
|
2287
|
+
const partialStdout = processFailureStdout(cause);
|
|
2288
|
+
if (partialStdout === undefined)
|
|
2289
|
+
return { ok: false, error: processFailure };
|
|
2290
|
+
stdout = partialStdout;
|
|
2291
|
+
}
|
|
2292
|
+
let decoded;
|
|
2293
|
+
try {
|
|
2294
|
+
decoded = JSON.parse(stdout);
|
|
2295
|
+
} catch {
|
|
2296
|
+
return processFailure === undefined ? invalidGitHubResponse : { ok: false, error: processFailure };
|
|
2297
|
+
}
|
|
2298
|
+
return { ok: true, value: { decoded, ...processFailure === undefined ? {} : { processFailure } } };
|
|
2299
|
+
}
|
|
2300
|
+
async function continuePullRequest(runner, initial, options) {
|
|
2301
|
+
if (initial.contextPage?.nextCursor === undefined) {
|
|
2302
|
+
const ci = initial.contextPage === null ? "none" : classifyContexts(initial.contextPage.contexts);
|
|
2303
|
+
return { tag: "Item", result: finalizeResponse(initial.metadata, initial.pullRequest, ci) };
|
|
2304
|
+
}
|
|
2305
|
+
const contexts = [...initial.contextPage.contexts];
|
|
2306
|
+
const totalCount = initial.contextPage.totalCount;
|
|
2307
|
+
const contextIds = new Set(contexts.map((context) => context.id));
|
|
2308
|
+
const cursors = new Set([initial.contextPage.nextCursor]);
|
|
2309
|
+
let cursor = initial.contextPage.nextCursor;
|
|
2310
|
+
while (cursor !== undefined) {
|
|
2311
|
+
const args = [
|
|
2312
|
+
"api",
|
|
2313
|
+
"graphql",
|
|
2314
|
+
"--method",
|
|
2315
|
+
"POST",
|
|
2316
|
+
"-f",
|
|
2317
|
+
`query=${continuationQuery}`,
|
|
2318
|
+
"-f",
|
|
2319
|
+
`url=${initial.pullRequest.url}`,
|
|
2320
|
+
"-f",
|
|
2321
|
+
`cursor=${cursor}`
|
|
2322
|
+
];
|
|
2323
|
+
const output = await runAndDecode(runner, args, options);
|
|
2324
|
+
if (!output.ok) {
|
|
2325
|
+
return output.error.tag === "GitHubCancelled" ? { tag: "Cancelled", error: output.error } : { tag: "Item", result: { ok: false, error: output.error } };
|
|
2326
|
+
}
|
|
2327
|
+
const page = parseContinuationResponse(output.value.decoded, initial.pullRequest);
|
|
2328
|
+
if (!page.ok) {
|
|
2329
|
+
return {
|
|
2330
|
+
tag: "Item",
|
|
2331
|
+
result: { ok: false, error: output.value.processFailure ?? page.error }
|
|
2332
|
+
};
|
|
2333
|
+
}
|
|
2334
|
+
if (page.value.totalCount !== totalCount)
|
|
2335
|
+
return { tag: "Item", result: invalidGitHubResponse };
|
|
2336
|
+
for (const context of page.value.contexts) {
|
|
2337
|
+
if (contextIds.has(context.id))
|
|
2338
|
+
return { tag: "Item", result: invalidGitHubResponse };
|
|
2339
|
+
contextIds.add(context.id);
|
|
2340
|
+
}
|
|
2341
|
+
if (contexts.length + page.value.contexts.length > totalCount) {
|
|
2342
|
+
return { tag: "Item", result: invalidGitHubResponse };
|
|
2343
|
+
}
|
|
2344
|
+
if (page.value.nextCursor !== undefined) {
|
|
2345
|
+
if (cursors.has(page.value.nextCursor))
|
|
2346
|
+
return { tag: "Item", result: invalidGitHubResponse };
|
|
2347
|
+
cursors.add(page.value.nextCursor);
|
|
2348
|
+
}
|
|
2349
|
+
contexts.push(...page.value.contexts);
|
|
2350
|
+
cursor = page.value.nextCursor;
|
|
2351
|
+
}
|
|
2352
|
+
if (contexts.length !== totalCount)
|
|
2353
|
+
return { tag: "Item", result: invalidGitHubResponse };
|
|
2354
|
+
const status = finalizeResponse(initial.metadata, initial.pullRequest, classifyContexts(contexts));
|
|
2355
|
+
return { tag: "Item", result: status };
|
|
2356
|
+
}
|
|
1870
2357
|
var execFileRunner = (file, args, options) => new Promise((resolve, reject) => {
|
|
1871
2358
|
execFile(file, [...args], {
|
|
1872
2359
|
encoding: "utf8",
|
|
@@ -1898,40 +2385,26 @@ function createGitHubClient(runner = execFileRunner) {
|
|
|
1898
2385
|
for (const [index, pullRequest] of pullRequests.entries()) {
|
|
1899
2386
|
args.push("-f", `url${index}=${pullRequest.url}`);
|
|
1900
2387
|
}
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
if (partialStdout !== undefined) {
|
|
1920
|
-
stdout = partialStdout;
|
|
1921
|
-
} else {
|
|
1922
|
-
return { ok: false, error: processFailure };
|
|
1923
|
-
}
|
|
1924
|
-
}
|
|
1925
|
-
let decoded;
|
|
1926
|
-
try {
|
|
1927
|
-
decoded = JSON.parse(stdout);
|
|
1928
|
-
} catch {
|
|
1929
|
-
return processFailure === undefined ? invalidGitHubResponse : { ok: false, error: processFailure };
|
|
2388
|
+
const output = await runAndDecode(runner, args, options);
|
|
2389
|
+
if (!output.ok)
|
|
2390
|
+
return output;
|
|
2391
|
+
const parsed = parseBatchResponse(output.value.decoded, pullRequests);
|
|
2392
|
+
if (!parsed.ok)
|
|
2393
|
+
return { ok: false, error: output.value.processFailure ?? parsed.error };
|
|
2394
|
+
const outcomes = await Promise.all(parsed.value.map((item) => {
|
|
2395
|
+
if (!item.ok)
|
|
2396
|
+
return Promise.resolve({ tag: "Item", result: item });
|
|
2397
|
+
return continuePullRequest(runner, item.value, options);
|
|
2398
|
+
}));
|
|
2399
|
+
const batch = [];
|
|
2400
|
+
let cancellation;
|
|
2401
|
+
for (const outcome of outcomes) {
|
|
2402
|
+
if (outcome.tag === "Cancelled")
|
|
2403
|
+
cancellation ??= outcome.error;
|
|
2404
|
+
else
|
|
2405
|
+
batch.push(outcome.result);
|
|
1930
2406
|
}
|
|
1931
|
-
|
|
1932
|
-
if (processFailure === undefined || parsed.ok)
|
|
1933
|
-
return parsed;
|
|
1934
|
-
return { ok: false, error: processFailure };
|
|
2407
|
+
return cancellation === undefined ? { ok: true, value: batch } : { ok: false, error: cancellation };
|
|
1935
2408
|
}
|
|
1936
2409
|
};
|
|
1937
2410
|
}
|
|
@@ -1945,6 +2418,7 @@ var diagnosticLabels = {
|
|
|
1945
2418
|
GitHubCliMissing: "install gh",
|
|
1946
2419
|
GitHubAuthenticationRequired: "run gh auth login",
|
|
1947
2420
|
GitHubUnavailable: "GitHub unavailable",
|
|
2421
|
+
PullRequestNotFound: "not found or inaccessible",
|
|
1948
2422
|
InvalidGitHubResponse: "invalid GitHub response"
|
|
1949
2423
|
};
|
|
1950
2424
|
function stateAppearance(state) {
|
|
@@ -1955,7 +2429,23 @@ function stateAppearance(state) {
|
|
|
1955
2429
|
return { tone: "red", label: "merge conflict", strikethrough: false };
|
|
1956
2430
|
case "mergeable":
|
|
1957
2431
|
case "unknown":
|
|
1958
|
-
|
|
2432
|
+
switch (state.ci) {
|
|
2433
|
+
case "failed":
|
|
2434
|
+
case "pending":
|
|
2435
|
+
return openAppearances[state.ci];
|
|
2436
|
+
case "none":
|
|
2437
|
+
case "passed":
|
|
2438
|
+
switch (state.blocker) {
|
|
2439
|
+
case "behind":
|
|
2440
|
+
return { tone: "yellow", label: "branch behind", strikethrough: false };
|
|
2441
|
+
case "none":
|
|
2442
|
+
return openAppearances[state.ci];
|
|
2443
|
+
default:
|
|
2444
|
+
return casesHandled(state.blocker);
|
|
2445
|
+
}
|
|
2446
|
+
default:
|
|
2447
|
+
return casesHandled(state.ci);
|
|
2448
|
+
}
|
|
1959
2449
|
default:
|
|
1960
2450
|
return casesHandled(state.mergeability);
|
|
1961
2451
|
}
|
|
@@ -1980,866 +2470,2005 @@ function statusAppearance(status) {
|
|
|
1980
2470
|
return status.stale ? { ...appearance, label: `${appearance.label} (stale; ${diagnosticLabels[status.diagnostic]})` } : appearance;
|
|
1981
2471
|
}
|
|
1982
2472
|
|
|
1983
|
-
// src/
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
2473
|
+
// src/external-url.ts
|
|
2474
|
+
async function openExternalUrl(url, subject, options = {}) {
|
|
2475
|
+
const platform = options.platform ?? process.platform;
|
|
2476
|
+
const executable = platform === "darwin" ? "open" : platform === "linux" ? "xdg-open" : undefined;
|
|
2477
|
+
if (executable === undefined) {
|
|
2478
|
+
return {
|
|
2479
|
+
ok: false,
|
|
2480
|
+
error: {
|
|
2481
|
+
tag: "UnsupportedPlatform",
|
|
2482
|
+
message: `Opening ${subject} is unsupported on ${platform}`,
|
|
2483
|
+
platform
|
|
2484
|
+
}
|
|
2485
|
+
};
|
|
1989
2486
|
}
|
|
1990
|
-
};
|
|
1991
|
-
var repositoryResolutionFailed = {
|
|
1992
|
-
tag: "RepositoryResolutionFailed",
|
|
1993
|
-
message: "Unable to resolve the current GitHub repository with gh; attach with a full URL instead"
|
|
1994
|
-
};
|
|
1995
|
-
var repositoryResolutionCancelled = {
|
|
1996
|
-
ok: false,
|
|
1997
|
-
error: { tag: "RepositoryResolutionCancelled" }
|
|
1998
|
-
};
|
|
1999
|
-
function isCancellation2(cause, signal) {
|
|
2000
|
-
if (signal?.aborted)
|
|
2001
|
-
return true;
|
|
2002
|
-
return cause instanceof Error && cause.name === "AbortError";
|
|
2003
|
-
}
|
|
2004
|
-
function parseRepositoryPullRequest(stdout, number) {
|
|
2005
|
-
let decoded;
|
|
2006
2487
|
try {
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2488
|
+
await (options.runner ?? execFileRunner)(executable, [url], options.signal ? { signal: options.signal } : {});
|
|
2489
|
+
return { ok: true, value: undefined };
|
|
2490
|
+
} catch (cause) {
|
|
2491
|
+
return {
|
|
2492
|
+
ok: false,
|
|
2493
|
+
error: { tag: "OpenExternalUrlFailed", message: `Unable to open ${subject}`, cause }
|
|
2494
|
+
};
|
|
2013
2495
|
}
|
|
2014
|
-
const repositoryUrl = decoded.url.endsWith("/") ? decoded.url.slice(0, -1) : decoded.url;
|
|
2015
|
-
const pullRequest = parsePullRequestUrl(`${repositoryUrl}/pull/${number}`);
|
|
2016
|
-
return pullRequest.ok ? pullRequest : { ok: false, error: repositoryResolutionFailed };
|
|
2017
2496
|
}
|
|
2018
|
-
async function
|
|
2019
|
-
const
|
|
2020
|
-
if (
|
|
2021
|
-
return
|
|
2022
|
-
if (
|
|
2023
|
-
return
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
let stdout;
|
|
2028
|
-
try {
|
|
2029
|
-
const result = await (options.runner ?? execFileRunner)("gh", ["repo", "view", "--json", "url"], {
|
|
2030
|
-
cwd: options.directory,
|
|
2031
|
-
...options.signal ? { signal: options.signal } : {}
|
|
2032
|
-
});
|
|
2033
|
-
stdout = result.stdout;
|
|
2034
|
-
} catch (cause) {
|
|
2035
|
-
if (isCancellation2(cause, options.signal))
|
|
2036
|
-
return repositoryResolutionCancelled;
|
|
2037
|
-
return { ok: false, error: { ...repositoryResolutionFailed, cause } };
|
|
2497
|
+
async function openPullRequest(pullRequest, options = {}) {
|
|
2498
|
+
const result = await openExternalUrl(pullRequest.url, "pull requests", options);
|
|
2499
|
+
if (result.ok)
|
|
2500
|
+
return result;
|
|
2501
|
+
if (result.error.tag === "UnsupportedPlatform") {
|
|
2502
|
+
return {
|
|
2503
|
+
ok: false,
|
|
2504
|
+
error: result.error
|
|
2505
|
+
};
|
|
2038
2506
|
}
|
|
2039
|
-
return
|
|
2507
|
+
return {
|
|
2508
|
+
ok: false,
|
|
2509
|
+
error: {
|
|
2510
|
+
tag: "OpenPullRequestFailed",
|
|
2511
|
+
message: "Unable to open the pull request",
|
|
2512
|
+
cause: result.error.cause
|
|
2513
|
+
}
|
|
2514
|
+
};
|
|
2040
2515
|
}
|
|
2041
2516
|
|
|
2042
|
-
// src/
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
import { homedir } from "os";
|
|
2047
|
-
import { join } from "path";
|
|
2048
|
-
var maximumPullRequestsPerSession = 20;
|
|
2049
|
-
var invalidStateFile = {
|
|
2050
|
-
ok: false,
|
|
2051
|
-
error: {
|
|
2052
|
-
tag: "InvalidStateFile",
|
|
2053
|
-
message: "The session pull request state file is invalid"
|
|
2517
|
+
// src/feedback.ts
|
|
2518
|
+
function parseProcessExecutionFailed2(value) {
|
|
2519
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) || !("tag" in value) || value.tag !== "ProcessExecutionFailed" || !("code" in value) || value.code !== null && typeof value.code !== "string" && typeof value.code !== "number" || !("stderr" in value) || typeof value.stderr !== "string" || !("stdout" in value) || typeof value.stdout !== "string" || !("cause" in value)) {
|
|
2520
|
+
return;
|
|
2054
2521
|
}
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2522
|
+
return {
|
|
2523
|
+
tag: "ProcessExecutionFailed",
|
|
2524
|
+
code: value.code,
|
|
2525
|
+
stderr: value.stderr,
|
|
2526
|
+
stdout: value.stdout,
|
|
2527
|
+
cause: value.cause
|
|
2528
|
+
};
|
|
2060
2529
|
}
|
|
2061
|
-
function
|
|
2062
|
-
|
|
2530
|
+
function isProcessCancellation(cause, signal) {
|
|
2531
|
+
if (signal?.aborted)
|
|
2532
|
+
return true;
|
|
2533
|
+
const originalCause = parseProcessExecutionFailed2(cause)?.cause ?? cause;
|
|
2534
|
+
return originalCause instanceof Error && originalCause.name === "AbortError";
|
|
2063
2535
|
}
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2536
|
+
var authenticationFailureMarkers2 = ["http 401", "bad credentials", "not logged into", "gh auth login"];
|
|
2537
|
+
function isAuthenticationFailure2(failure) {
|
|
2538
|
+
if (failure.code === 4)
|
|
2539
|
+
return true;
|
|
2540
|
+
const stderr = failure.stderr.toLowerCase();
|
|
2541
|
+
return authenticationFailureMarkers2.some((marker) => stderr.includes(marker));
|
|
2067
2542
|
}
|
|
2068
|
-
function
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2543
|
+
function trimRequired(value, field) {
|
|
2544
|
+
const trimmed = value.trim();
|
|
2545
|
+
if (trimmed !== "")
|
|
2546
|
+
return { ok: true, value: trimmed };
|
|
2547
|
+
return {
|
|
2548
|
+
ok: false,
|
|
2549
|
+
error: { tag: "InvalidFeedback", message: `${field} must not be empty` }
|
|
2550
|
+
};
|
|
2551
|
+
}
|
|
2552
|
+
function appendDiagnostics(draft, diagnostics) {
|
|
2553
|
+
if (diagnostics === undefined)
|
|
2554
|
+
return { ok: true, value: draft };
|
|
2555
|
+
const entries = [
|
|
2556
|
+
["pluginVersion", "Plugin version", diagnostics.pluginVersion],
|
|
2557
|
+
["opencodeVersion", "OpenCode version", diagnostics.opencodeVersion],
|
|
2558
|
+
["operatingSystem", "Operating system", diagnostics.operatingSystem],
|
|
2559
|
+
["installationSource", "Installation source", diagnostics.installationSource]
|
|
2560
|
+
];
|
|
2561
|
+
const lines = [];
|
|
2562
|
+
for (const [field, label, value] of entries) {
|
|
2563
|
+
const trimmed = trimRequired(value, field);
|
|
2564
|
+
if (!trimmed.ok)
|
|
2565
|
+
return trimmed;
|
|
2566
|
+
lines.push(`- ${label}: ${trimmed.value}`);
|
|
2090
2567
|
}
|
|
2091
|
-
return {
|
|
2568
|
+
return {
|
|
2569
|
+
ok: true,
|
|
2570
|
+
value: {
|
|
2571
|
+
...draft,
|
|
2572
|
+
body: [draft.body, "", "## Diagnostics", "", ...lines].join(`
|
|
2573
|
+
`)
|
|
2574
|
+
}
|
|
2575
|
+
};
|
|
2092
2576
|
}
|
|
2093
|
-
function
|
|
2094
|
-
|
|
2577
|
+
function createFeedbackDraft(input, diagnostics) {
|
|
2578
|
+
const title = trimRequired(input.title, "title");
|
|
2579
|
+
if (!title.ok)
|
|
2580
|
+
return title;
|
|
2581
|
+
switch (input.kind) {
|
|
2582
|
+
case "bug": {
|
|
2583
|
+
const problem = trimRequired(input.problem, "problem");
|
|
2584
|
+
if (!problem.ok)
|
|
2585
|
+
return problem;
|
|
2586
|
+
const reproduction = trimRequired(input.reproduction, "reproduction");
|
|
2587
|
+
if (!reproduction.ok)
|
|
2588
|
+
return reproduction;
|
|
2589
|
+
const expectedBehavior = trimRequired(input.expectedBehavior, "expectedBehavior");
|
|
2590
|
+
if (!expectedBehavior.ok)
|
|
2591
|
+
return expectedBehavior;
|
|
2592
|
+
return appendDiagnostics({
|
|
2593
|
+
title: title.value,
|
|
2594
|
+
body: [
|
|
2595
|
+
"## Problem",
|
|
2596
|
+
"",
|
|
2597
|
+
problem.value,
|
|
2598
|
+
"",
|
|
2599
|
+
"## Reproduction",
|
|
2600
|
+
"",
|
|
2601
|
+
reproduction.value,
|
|
2602
|
+
"",
|
|
2603
|
+
"## Expected Behavior",
|
|
2604
|
+
"",
|
|
2605
|
+
expectedBehavior.value
|
|
2606
|
+
].join(`
|
|
2607
|
+
`),
|
|
2608
|
+
label: "bug",
|
|
2609
|
+
template: "bug_report.md"
|
|
2610
|
+
}, diagnostics);
|
|
2611
|
+
}
|
|
2612
|
+
case "feature": {
|
|
2613
|
+
const problem = trimRequired(input.problem, "problem");
|
|
2614
|
+
if (!problem.ok)
|
|
2615
|
+
return problem;
|
|
2616
|
+
const desiredOutcome = trimRequired(input.desiredOutcome, "desiredOutcome");
|
|
2617
|
+
if (!desiredOutcome.ok)
|
|
2618
|
+
return desiredOutcome;
|
|
2619
|
+
const body = ["## Problem", "", problem.value, "", "## Desired Outcome", "", desiredOutcome.value];
|
|
2620
|
+
const constraints = input.constraints?.trim();
|
|
2621
|
+
if (constraints !== undefined && constraints !== "") {
|
|
2622
|
+
body.push("", "## Constraints", "", constraints);
|
|
2623
|
+
}
|
|
2624
|
+
return appendDiagnostics({
|
|
2625
|
+
title: title.value,
|
|
2626
|
+
body: body.join(`
|
|
2627
|
+
`),
|
|
2628
|
+
label: "enhancement",
|
|
2629
|
+
template: "feature_request.md"
|
|
2630
|
+
}, diagnostics);
|
|
2631
|
+
}
|
|
2632
|
+
case "other": {
|
|
2633
|
+
const details = trimRequired(input.details, "details");
|
|
2634
|
+
if (!details.ok)
|
|
2635
|
+
return details;
|
|
2636
|
+
return appendDiagnostics({
|
|
2637
|
+
title: title.value,
|
|
2638
|
+
body: ["## Details", "", details.value].join(`
|
|
2639
|
+
`)
|
|
2640
|
+
}, diagnostics);
|
|
2641
|
+
}
|
|
2642
|
+
default:
|
|
2643
|
+
return casesHandled(input);
|
|
2644
|
+
}
|
|
2095
2645
|
}
|
|
2096
|
-
function
|
|
2097
|
-
|
|
2646
|
+
function createFeedbackIssueUrl(draft) {
|
|
2647
|
+
const url = new URL("https://github.com/hcrosse/opencode-pr-tracker/issues/new");
|
|
2648
|
+
url.searchParams.set("title", draft.title);
|
|
2649
|
+
url.searchParams.set("body", draft.body);
|
|
2650
|
+
if (draft.template !== undefined)
|
|
2651
|
+
url.searchParams.set("template", draft.template);
|
|
2652
|
+
return url.toString();
|
|
2098
2653
|
}
|
|
2099
|
-
function
|
|
2100
|
-
const
|
|
2101
|
-
|
|
2654
|
+
async function openFeedbackDraft(draft, options = {}) {
|
|
2655
|
+
const url = createFeedbackIssueUrl(draft);
|
|
2656
|
+
if (url.length > 8000) {
|
|
2657
|
+
return {
|
|
2658
|
+
ok: false,
|
|
2659
|
+
error: {
|
|
2660
|
+
tag: "FeedbackUrlTooLong",
|
|
2661
|
+
message: "Feedback is too long for browser delivery; choose GitHub CLI delivery"
|
|
2662
|
+
}
|
|
2663
|
+
};
|
|
2664
|
+
}
|
|
2665
|
+
const result = await openExternalUrl(url, "feedback", options);
|
|
2666
|
+
if (result.ok)
|
|
2667
|
+
return result;
|
|
2668
|
+
if (result.error.tag === "UnsupportedPlatform") {
|
|
2669
|
+
return {
|
|
2670
|
+
ok: false,
|
|
2671
|
+
error: result.error
|
|
2672
|
+
};
|
|
2673
|
+
}
|
|
2674
|
+
return {
|
|
2675
|
+
ok: false,
|
|
2676
|
+
error: {
|
|
2677
|
+
tag: "OpenFeedbackFailed",
|
|
2678
|
+
message: "Unable to open feedback; choose GitHub CLI delivery or retry",
|
|
2679
|
+
cause: result.error.cause
|
|
2680
|
+
}
|
|
2681
|
+
};
|
|
2102
2682
|
}
|
|
2103
|
-
function
|
|
2104
|
-
const
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2683
|
+
async function submitFeedbackDraft(draft, options = {}) {
|
|
2684
|
+
const args = [
|
|
2685
|
+
"issue",
|
|
2686
|
+
"create",
|
|
2687
|
+
"--repo",
|
|
2688
|
+
"hcrosse/opencode-pr-tracker",
|
|
2689
|
+
"--title",
|
|
2690
|
+
draft.title,
|
|
2691
|
+
"--body",
|
|
2692
|
+
draft.body
|
|
2693
|
+
];
|
|
2694
|
+
let stdout;
|
|
2695
|
+
try {
|
|
2696
|
+
const result = await (options.runner ?? execFileRunner)("gh", args, options.signal ? { signal: options.signal } : {});
|
|
2697
|
+
stdout = result.stdout;
|
|
2698
|
+
} catch (cause) {
|
|
2699
|
+
if (isProcessCancellation(cause, options.signal)) {
|
|
2700
|
+
return { ok: false, error: { tag: "SubmitFeedbackCancelled" } };
|
|
2701
|
+
}
|
|
2702
|
+
const failure = parseProcessExecutionFailed2(cause);
|
|
2703
|
+
if (failure?.code === "ENOENT") {
|
|
2704
|
+
return {
|
|
2705
|
+
ok: false,
|
|
2706
|
+
error: {
|
|
2707
|
+
tag: "GitHubCliMissing",
|
|
2708
|
+
message: "GitHub CLI is not installed; install gh and retry",
|
|
2709
|
+
cause
|
|
2118
2710
|
}
|
|
2119
|
-
}
|
|
2120
|
-
|
|
2121
|
-
|
|
2711
|
+
};
|
|
2712
|
+
}
|
|
2713
|
+
if (failure !== undefined && isAuthenticationFailure2(failure)) {
|
|
2122
2714
|
return {
|
|
2123
2715
|
ok: false,
|
|
2124
|
-
error:
|
|
2716
|
+
error: {
|
|
2717
|
+
tag: "GitHubAuthenticationRequired",
|
|
2718
|
+
message: "GitHub CLI authentication required; run gh auth login",
|
|
2719
|
+
cause
|
|
2720
|
+
}
|
|
2125
2721
|
};
|
|
2126
2722
|
}
|
|
2723
|
+
return {
|
|
2724
|
+
ok: false,
|
|
2725
|
+
error: {
|
|
2726
|
+
tag: "SubmitFeedbackFailed",
|
|
2727
|
+
message: "Unable to submit feedback with GitHub CLI; choose browser delivery or retry",
|
|
2728
|
+
cause
|
|
2729
|
+
}
|
|
2730
|
+
};
|
|
2127
2731
|
}
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2732
|
+
const issueUrl = stdout.trim();
|
|
2733
|
+
if (issueUrl === "") {
|
|
2734
|
+
return {
|
|
2735
|
+
ok: false,
|
|
2736
|
+
error: {
|
|
2737
|
+
tag: "InvalidGitHubResponse",
|
|
2738
|
+
message: "GitHub CLI did not return the created issue URL"
|
|
2739
|
+
}
|
|
2740
|
+
};
|
|
2741
|
+
}
|
|
2742
|
+
return { ok: true, value: issueUrl };
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
// src/feedback-tui.tsx
|
|
2746
|
+
function FeedbackConfirmation(props) {
|
|
2747
|
+
useKeyboard((key) => {
|
|
2748
|
+
if (key.name === "return") {
|
|
2749
|
+
key.preventDefault();
|
|
2750
|
+
key.stopPropagation();
|
|
2751
|
+
props.onConfirm();
|
|
2752
|
+
return;
|
|
2753
|
+
}
|
|
2754
|
+
if (key.name === "escape" || key.ctrl && key.name === "c") {
|
|
2755
|
+
key.preventDefault();
|
|
2756
|
+
key.stopPropagation();
|
|
2757
|
+
props.onCancel();
|
|
2758
|
+
}
|
|
2759
|
+
});
|
|
2760
|
+
return (() => {
|
|
2761
|
+
var _el$ = _$createElement("box"), _el$2 = _$createElement("text"), _el$3 = _$createElement("b"), _el$4 = _$createElement("scrollbox"), _el$5 = _$createElement("text"), _el$6 = _$createElement("box"), _el$7 = _$createElement("text"), _el$9 = _$createElement("text");
|
|
2762
|
+
_$insertNode(_el$, _el$2);
|
|
2763
|
+
_$insertNode(_el$, _el$4);
|
|
2764
|
+
_$insertNode(_el$, _el$6);
|
|
2765
|
+
_$setProp(_el$, "flexDirection", "column");
|
|
2766
|
+
_$setProp(_el$, "gap", 1);
|
|
2767
|
+
_$insertNode(_el$2, _el$3);
|
|
2768
|
+
_$insert(_el$3, () => props.title);
|
|
2769
|
+
_$insertNode(_el$4, _el$5);
|
|
2770
|
+
_$setProp(_el$4, "focused", true);
|
|
2771
|
+
_$setProp(_el$4, "scrollY", true);
|
|
2772
|
+
_$setProp(_el$4, "height", 10);
|
|
2773
|
+
_$insert(_el$5, () => props.preview);
|
|
2774
|
+
_$insertNode(_el$6, _el$7);
|
|
2775
|
+
_$insertNode(_el$6, _el$9);
|
|
2776
|
+
_$setProp(_el$6, "flexDirection", "row");
|
|
2777
|
+
_$setProp(_el$6, "gap", 2);
|
|
2778
|
+
_$insertNode(_el$7, _$createTextNode(`[Esc] Cancel`));
|
|
2779
|
+
_$insert(_el$9, () => props.confirmLabel);
|
|
2780
|
+
_$effect((_p$) => {
|
|
2781
|
+
var { onCancel: _v$, onConfirm: _v$2 } = props;
|
|
2782
|
+
_v$ !== _p$.e && (_p$.e = _$setProp(_el$7, "onMouseUp", _v$, _p$.e));
|
|
2783
|
+
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$9, "onMouseUp", _v$2, _p$.t));
|
|
2784
|
+
return _p$;
|
|
2785
|
+
}, {
|
|
2786
|
+
e: undefined,
|
|
2787
|
+
t: undefined
|
|
2788
|
+
});
|
|
2789
|
+
return _el$;
|
|
2790
|
+
})();
|
|
2791
|
+
}
|
|
2792
|
+
function showDialog(api, signal, render) {
|
|
2793
|
+
return new Promise((resolve) => {
|
|
2794
|
+
let finished = false;
|
|
2795
|
+
const onAbort = () => finish(undefined);
|
|
2796
|
+
const finish = (value, clearDialog = true) => {
|
|
2797
|
+
if (finished)
|
|
2137
2798
|
return;
|
|
2138
|
-
|
|
2139
|
-
|
|
2799
|
+
finished = true;
|
|
2800
|
+
signal.removeEventListener("abort", onAbort);
|
|
2801
|
+
if (clearDialog)
|
|
2802
|
+
api.ui.dialog.clear();
|
|
2803
|
+
resolve(value);
|
|
2804
|
+
};
|
|
2805
|
+
if (signal.aborted) {
|
|
2806
|
+
finish(undefined, false);
|
|
2807
|
+
return;
|
|
2140
2808
|
}
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
}
|
|
2809
|
+
signal.addEventListener("abort", onAbort, {
|
|
2810
|
+
once: true
|
|
2811
|
+
});
|
|
2812
|
+
api.ui.dialog.setSize("medium");
|
|
2813
|
+
api.ui.dialog.replace(() => render((value) => finish(value)), () => finish(undefined, false));
|
|
2814
|
+
});
|
|
2815
|
+
}
|
|
2816
|
+
function selectFeedbackKind(api, signal) {
|
|
2817
|
+
return showDialog(api, signal, (finish) => {
|
|
2818
|
+
const DialogSelect = api.ui.DialogSelect;
|
|
2819
|
+
return _$createComponent(DialogSelect, {
|
|
2820
|
+
title: "Feedback type",
|
|
2821
|
+
options: [{
|
|
2822
|
+
title: "Bug report",
|
|
2823
|
+
value: "bug"
|
|
2824
|
+
}, {
|
|
2825
|
+
title: "Feature request",
|
|
2826
|
+
value: "feature"
|
|
2827
|
+
}, {
|
|
2828
|
+
title: "Other feedback",
|
|
2829
|
+
value: "other"
|
|
2830
|
+
}],
|
|
2831
|
+
current: "bug",
|
|
2832
|
+
onSelect: (option) => finish(option.value)
|
|
2833
|
+
});
|
|
2834
|
+
});
|
|
2835
|
+
}
|
|
2836
|
+
function promptForValue(api, signal, title, required) {
|
|
2837
|
+
return showDialog(api, signal, (finish) => {
|
|
2838
|
+
const [error, setError] = createSignal();
|
|
2839
|
+
const DialogPrompt = api.ui.DialogPrompt;
|
|
2840
|
+
return _$createComponent(DialogPrompt, {
|
|
2841
|
+
title,
|
|
2842
|
+
description: () => error() ? (() => {
|
|
2843
|
+
var _el$0 = _$createElement("text");
|
|
2844
|
+
_$insert(_el$0, error);
|
|
2845
|
+
_$effect((_$p) => _$setProp(_el$0, "fg", api.theme.current.error, _$p));
|
|
2846
|
+
return _el$0;
|
|
2847
|
+
})() : null,
|
|
2848
|
+
onConfirm: (value) => {
|
|
2849
|
+
if (required && value.trim() === "") {
|
|
2850
|
+
setError(`${title} is required`);
|
|
2851
|
+
return;
|
|
2852
|
+
}
|
|
2853
|
+
finish(value);
|
|
2854
|
+
},
|
|
2855
|
+
onCancel: () => finish(undefined)
|
|
2856
|
+
});
|
|
2857
|
+
});
|
|
2858
|
+
}
|
|
2859
|
+
function confirmDiagnostics(api, signal, diagnostics) {
|
|
2860
|
+
const message = [`Plugin version: ${diagnostics.pluginVersion}`, `OpenCode version: ${diagnostics.opencodeVersion}`, `Operating system: ${diagnostics.operatingSystem}`, `Installation source: ${diagnostics.installationSource}`].join(`
|
|
2861
|
+
`);
|
|
2862
|
+
return showDialog(api, signal, (finish) => _$createComponent(api.ui.DialogConfirm, {
|
|
2863
|
+
title: "Include diagnostics?",
|
|
2864
|
+
message,
|
|
2865
|
+
onConfirm: () => finish(true),
|
|
2866
|
+
onCancel: () => finish(false)
|
|
2867
|
+
}));
|
|
2868
|
+
}
|
|
2869
|
+
function selectDelivery(api, signal) {
|
|
2870
|
+
return showDialog(api, signal, (finish) => {
|
|
2871
|
+
const DialogSelect = api.ui.DialogSelect;
|
|
2872
|
+
return _$createComponent(DialogSelect, {
|
|
2873
|
+
title: "Send feedback",
|
|
2874
|
+
options: [{
|
|
2875
|
+
title: "Open in browser",
|
|
2876
|
+
value: "browser"
|
|
2877
|
+
}, {
|
|
2878
|
+
title: "Submit with GitHub CLI",
|
|
2879
|
+
value: "gh"
|
|
2880
|
+
}],
|
|
2881
|
+
current: "browser",
|
|
2882
|
+
onSelect: (option) => finish(option.value)
|
|
2883
|
+
});
|
|
2884
|
+
});
|
|
2885
|
+
}
|
|
2886
|
+
function deliveryAction(delivery) {
|
|
2887
|
+
return delivery === "browser" ? "Open a prefilled issue in your browser" : "Create the issue with GitHub CLI";
|
|
2888
|
+
}
|
|
2889
|
+
function feedbackPreview(draft, delivery) {
|
|
2890
|
+
const label = delivery === "gh" ? "none" : draft.template === "bug_report.md" ? "bug" : draft.template === "feature_request.md" ? "enhancement" : "none";
|
|
2891
|
+
return ["Repository: hcrosse/opencode-pr-tracker", `Action: ${deliveryAction(delivery)}`, `Title: ${draft.title}`, `Label: ${label}`, "", "Body:", draft.body].join(`
|
|
2892
|
+
`);
|
|
2893
|
+
}
|
|
2894
|
+
function confirmFeedback(api, signal, draft, delivery, confirmationRenderer) {
|
|
2895
|
+
const title = delivery === "browser" ? "Open PR tracker feedback?" : "Send PR tracker feedback?";
|
|
2896
|
+
const confirmLabel = delivery === "browser" ? "[Enter] Open issue" : "[Enter] Send";
|
|
2897
|
+
return showDialog(api, signal, (finish) => {
|
|
2898
|
+
const Confirmation = confirmationRenderer;
|
|
2899
|
+
return _$createComponent(Confirmation, {
|
|
2900
|
+
title,
|
|
2901
|
+
confirmLabel,
|
|
2902
|
+
get preview() {
|
|
2903
|
+
return feedbackPreview(draft, delivery);
|
|
2904
|
+
},
|
|
2905
|
+
onConfirm: () => finish(true),
|
|
2906
|
+
onCancel: () => finish(false)
|
|
2907
|
+
});
|
|
2908
|
+
});
|
|
2909
|
+
}
|
|
2910
|
+
async function collectFeedbackInput(api, signal, kind) {
|
|
2911
|
+
const title = await promptForValue(api, signal, "Feedback title", true);
|
|
2912
|
+
if (title === undefined)
|
|
2913
|
+
return;
|
|
2914
|
+
switch (kind) {
|
|
2915
|
+
case "bug": {
|
|
2916
|
+
const problem = await promptForValue(api, signal, "Problem", true);
|
|
2917
|
+
if (problem === undefined)
|
|
2918
|
+
return;
|
|
2919
|
+
const reproduction = await promptForValue(api, signal, "Reproduction steps", true);
|
|
2920
|
+
if (reproduction === undefined)
|
|
2921
|
+
return;
|
|
2922
|
+
const expectedBehavior = await promptForValue(api, signal, "Expected behavior", true);
|
|
2923
|
+
if (expectedBehavior === undefined)
|
|
2924
|
+
return;
|
|
2144
2925
|
return {
|
|
2145
|
-
|
|
2146
|
-
|
|
2926
|
+
kind,
|
|
2927
|
+
title,
|
|
2928
|
+
problem,
|
|
2929
|
+
reproduction,
|
|
2930
|
+
expectedBehavior
|
|
2147
2931
|
};
|
|
2148
2932
|
}
|
|
2149
|
-
|
|
2150
|
-
|
|
2933
|
+
case "feature": {
|
|
2934
|
+
const problem = await promptForValue(api, signal, "Problem", true);
|
|
2935
|
+
if (problem === undefined)
|
|
2936
|
+
return;
|
|
2937
|
+
const desiredOutcome = await promptForValue(api, signal, "Desired outcome", true);
|
|
2938
|
+
if (desiredOutcome === undefined)
|
|
2939
|
+
return;
|
|
2940
|
+
const constraints = await promptForValue(api, signal, "Constraints (optional)", false);
|
|
2941
|
+
if (constraints === undefined)
|
|
2942
|
+
return;
|
|
2151
2943
|
return {
|
|
2152
|
-
|
|
2153
|
-
|
|
2944
|
+
kind,
|
|
2945
|
+
title,
|
|
2946
|
+
problem,
|
|
2947
|
+
desiredOutcome,
|
|
2948
|
+
constraints
|
|
2154
2949
|
};
|
|
2155
2950
|
}
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
let content;
|
|
2161
|
-
try {
|
|
2162
|
-
content = await readFile(path, "utf8");
|
|
2163
|
-
} catch (cause) {
|
|
2164
|
-
if (isMissingFile(cause))
|
|
2165
|
-
return { ok: true, value: undefined };
|
|
2951
|
+
case "other": {
|
|
2952
|
+
const details = await promptForValue(api, signal, "Details", true);
|
|
2953
|
+
if (details === undefined)
|
|
2954
|
+
return;
|
|
2166
2955
|
return {
|
|
2167
|
-
|
|
2168
|
-
|
|
2956
|
+
kind,
|
|
2957
|
+
title,
|
|
2958
|
+
details
|
|
2169
2959
|
};
|
|
2170
2960
|
}
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
decoded = JSON.parse(content);
|
|
2174
|
-
} catch {
|
|
2175
|
-
return invalidStateFile;
|
|
2176
|
-
}
|
|
2177
|
-
return parseState(decoded);
|
|
2178
|
-
}
|
|
2179
|
-
async function read(sessionID) {
|
|
2180
|
-
const result = await readExisting(sessionID);
|
|
2181
|
-
if (!result.ok)
|
|
2182
|
-
return result;
|
|
2183
|
-
return { ok: true, value: result.value ?? [] };
|
|
2961
|
+
default:
|
|
2962
|
+
return casesHandled(kind);
|
|
2184
2963
|
}
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
await writeFile(temporary, `${JSON.stringify(state, null, 2)}
|
|
2198
|
-
`, { mode: 384 });
|
|
2199
|
-
await rename(temporary, destination);
|
|
2200
|
-
return { ok: true, value: undefined };
|
|
2201
|
-
} catch (cause) {
|
|
2202
|
-
await rm(temporary, { force: true }).catch(() => {
|
|
2964
|
+
}
|
|
2965
|
+
function createFeedbackCommand(api, dependencies, release) {
|
|
2966
|
+
return {
|
|
2967
|
+
name: "pr.tracker.feedback",
|
|
2968
|
+
title: "Send PR tracker feedback",
|
|
2969
|
+
category: "Plugin",
|
|
2970
|
+
namespace: "palette",
|
|
2971
|
+
slashName: "pr-tracker-feedback",
|
|
2972
|
+
async run() {
|
|
2973
|
+
const signal = api.lifecycle.signal;
|
|
2974
|
+
const kind = await selectFeedbackKind(api, signal);
|
|
2975
|
+
if (kind === undefined)
|
|
2203
2976
|
return;
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2977
|
+
const input = await collectFeedbackInput(api, signal, kind);
|
|
2978
|
+
if (input === undefined)
|
|
2979
|
+
return;
|
|
2980
|
+
const diagnostics = {
|
|
2981
|
+
pluginVersion: release?.version ?? "unavailable",
|
|
2982
|
+
opencodeVersion: api.app.version,
|
|
2983
|
+
operatingSystem: `${process.platform}/${process.arch}`,
|
|
2984
|
+
installationSource: release?.source ?? "unavailable"
|
|
2208
2985
|
};
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
}
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
if (next.length === current.value.length)
|
|
2244
|
-
return { ok: true, value: "absent" };
|
|
2245
|
-
const written = await write(sessionID, next);
|
|
2246
|
-
if (!written.ok)
|
|
2247
|
-
return written;
|
|
2248
|
-
return { ok: true, value: "removed" };
|
|
2249
|
-
});
|
|
2250
|
-
},
|
|
2251
|
-
async detachByNumber(sessionID, number) {
|
|
2252
|
-
return withLock(sessionID, async () => {
|
|
2253
|
-
const current = await read(sessionID);
|
|
2254
|
-
if (!current.ok)
|
|
2255
|
-
return current;
|
|
2256
|
-
const matches = current.value.filter((attachment) => attachment.pullRequest.number === number);
|
|
2257
|
-
if (matches.length === 0)
|
|
2258
|
-
return { ok: true, value: { tag: "absent" } };
|
|
2259
|
-
if (matches.length > 1) {
|
|
2260
|
-
return {
|
|
2261
|
-
ok: true,
|
|
2262
|
-
value: { tag: "ambiguous", pullRequests: matches.map((attachment) => attachment.pullRequest) }
|
|
2263
|
-
};
|
|
2986
|
+
const includeDiagnostics = await confirmDiagnostics(api, signal, diagnostics);
|
|
2987
|
+
if (includeDiagnostics === undefined)
|
|
2988
|
+
return;
|
|
2989
|
+
const draft = createFeedbackDraft(input, includeDiagnostics ? diagnostics : undefined);
|
|
2990
|
+
if (!draft.ok) {
|
|
2991
|
+
api.ui.toast({
|
|
2992
|
+
variant: "error",
|
|
2993
|
+
title: "Pull request tracker",
|
|
2994
|
+
message: draft.error.message
|
|
2995
|
+
});
|
|
2996
|
+
return;
|
|
2997
|
+
}
|
|
2998
|
+
const delivery = await selectDelivery(api, signal);
|
|
2999
|
+
if (delivery === undefined)
|
|
3000
|
+
return;
|
|
3001
|
+
const confirmed = await confirmFeedback(api, signal, draft.value, delivery, dependencies.confirmationRenderer ?? FeedbackConfirmation);
|
|
3002
|
+
if (!confirmed || signal.aborted)
|
|
3003
|
+
return;
|
|
3004
|
+
if (delivery === "browser") {
|
|
3005
|
+
const result2 = await openFeedbackDraft(draft.value, {
|
|
3006
|
+
...dependencies.platform === undefined ? {} : {
|
|
3007
|
+
platform: dependencies.platform
|
|
3008
|
+
},
|
|
3009
|
+
...dependencies.runner === undefined ? {} : {
|
|
3010
|
+
runner: dependencies.runner
|
|
3011
|
+
},
|
|
3012
|
+
signal
|
|
3013
|
+
});
|
|
3014
|
+
if (!result2.ok && !signal.aborted) {
|
|
3015
|
+
api.ui.toast({
|
|
3016
|
+
variant: "error",
|
|
3017
|
+
title: "Pull request tracker",
|
|
3018
|
+
message: result2.error.message
|
|
3019
|
+
});
|
|
2264
3020
|
}
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
return { ok: true, value: { tag: "removed", pullRequest: match.pullRequest } };
|
|
3021
|
+
return;
|
|
3022
|
+
}
|
|
3023
|
+
const result = await submitFeedbackDraft(draft.value, {
|
|
3024
|
+
...dependencies.runner === undefined ? {} : {
|
|
3025
|
+
runner: dependencies.runner
|
|
3026
|
+
},
|
|
3027
|
+
signal
|
|
2273
3028
|
});
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
return { ok: true, value: "absent" };
|
|
2282
|
-
try {
|
|
2283
|
-
await rm(join(directory, fileName(sessionID)), { force: true });
|
|
2284
|
-
return { ok: true, value: "removed" };
|
|
2285
|
-
} catch (cause) {
|
|
2286
|
-
return {
|
|
2287
|
-
ok: false,
|
|
2288
|
-
error: stateUnavailable("write", "Unable to remove the session pull request state", cause)
|
|
2289
|
-
};
|
|
3029
|
+
if (result.ok) {
|
|
3030
|
+
if (!signal.aborted) {
|
|
3031
|
+
api.ui.toast({
|
|
3032
|
+
variant: "success",
|
|
3033
|
+
title: "Pull request tracker",
|
|
3034
|
+
message: result.value
|
|
3035
|
+
});
|
|
2290
3036
|
}
|
|
2291
|
-
})
|
|
3037
|
+
} else if (result.error.tag !== "SubmitFeedbackCancelled" && !signal.aborted) {
|
|
3038
|
+
api.ui.toast({
|
|
3039
|
+
variant: "error",
|
|
3040
|
+
title: "Pull request tracker",
|
|
3041
|
+
message: result.error.message
|
|
3042
|
+
});
|
|
3043
|
+
}
|
|
2292
3044
|
}
|
|
2293
3045
|
};
|
|
2294
3046
|
}
|
|
2295
3047
|
|
|
2296
|
-
// src/tui.tsx
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
};
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
3048
|
+
// src/plugin-update-tui.tsx
|
|
3049
|
+
import { createComponent as _$createComponent2 } from "@opentui/solid";
|
|
3050
|
+
import { join as join2 } from "path";
|
|
3051
|
+
|
|
3052
|
+
// src/update.ts
|
|
3053
|
+
import { readFile } from "fs/promises";
|
|
3054
|
+
import { join } from "path";
|
|
3055
|
+
var updateCacheKey = "plugin-update-check-v1";
|
|
3056
|
+
var packageName = "@hcrosse/opencode-pr-tracker";
|
|
3057
|
+
var registryUrl = `https://registry.npmjs.org/${encodeURIComponent(packageName)}`;
|
|
3058
|
+
var updateCacheMilliseconds = 24 * 60 * 60 * 1000;
|
|
3059
|
+
function isRecord2(value) {
|
|
3060
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2307
3061
|
}
|
|
2308
|
-
function
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
const
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
let refreshQueued = false;
|
|
2317
|
-
function project(attachments) {
|
|
2318
|
-
return attachments.map((attachment) => ({
|
|
2319
|
-
attachment,
|
|
2320
|
-
status: statuses.get(attachment.pullRequest.url) ?? {
|
|
2321
|
-
tag: "Unavailable"
|
|
2322
|
-
}
|
|
2323
|
-
}));
|
|
3062
|
+
function parseRegistryReleases(input) {
|
|
3063
|
+
if (!isRecord2(input) || !isRecord2(input.versions))
|
|
3064
|
+
return;
|
|
3065
|
+
const releases = [];
|
|
3066
|
+
for (const [version, metadata] of Object.entries(input.versions)) {
|
|
3067
|
+
if (!isRecord2(metadata) || !isRecord2(metadata.engines) || typeof metadata.engines.opencode !== "string")
|
|
3068
|
+
continue;
|
|
3069
|
+
releases.push({ version, opencodeRange: metadata.engines.opencode });
|
|
2324
3070
|
}
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
});
|
|
2347
|
-
if (stopped)
|
|
2348
|
-
return;
|
|
2349
|
-
let batchDiagnostic;
|
|
2350
|
-
if (!batch.ok) {
|
|
2351
|
-
if (batch.error.tag === "GitHubCancelled")
|
|
2352
|
-
return;
|
|
2353
|
-
batchDiagnostic = batch.error.tag === "GitHubBatchLimitExceeded" ? "GitHubUnavailable" : batch.error.tag;
|
|
2354
|
-
}
|
|
2355
|
-
for (const [index, attachment] of refreshable.entries()) {
|
|
2356
|
-
const previous = statuses.get(attachment.pullRequest.url);
|
|
2357
|
-
const result = batch.ok ? batch.value[index] : undefined;
|
|
2358
|
-
if (result?.ok) {
|
|
2359
|
-
statuses.set(attachment.pullRequest.url, result.value);
|
|
3071
|
+
return releases;
|
|
3072
|
+
}
|
|
3073
|
+
function isStableVersion(version) {
|
|
3074
|
+
return /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.test(version);
|
|
3075
|
+
}
|
|
3076
|
+
function isNewerStableVersion(version, currentVersion) {
|
|
3077
|
+
if (!isStableVersion(version))
|
|
3078
|
+
return false;
|
|
3079
|
+
try {
|
|
3080
|
+
return Bun.semver.order(version, currentVersion) > 0;
|
|
3081
|
+
} catch {
|
|
3082
|
+
return false;
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
function newestCompatibleUpdate(releases, versions) {
|
|
3086
|
+
let latest;
|
|
3087
|
+
for (const release of releases) {
|
|
3088
|
+
if (!isNewerStableVersion(release.version, versions.currentVersion))
|
|
3089
|
+
continue;
|
|
3090
|
+
try {
|
|
3091
|
+
if (!Bun.semver.satisfies(versions.opencodeVersion, release.opencodeRange))
|
|
2360
3092
|
continue;
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
stale: true,
|
|
2366
|
-
diagnostic
|
|
2367
|
-
} : {
|
|
2368
|
-
tag: "Unavailable",
|
|
2369
|
-
diagnostic
|
|
2370
|
-
});
|
|
3093
|
+
if (latest === undefined || Bun.semver.order(release.version, latest) > 0)
|
|
3094
|
+
latest = release.version;
|
|
3095
|
+
} catch {
|
|
3096
|
+
continue;
|
|
2371
3097
|
}
|
|
2372
|
-
if (!stopped)
|
|
2373
|
-
input.publish(project(attachments.value));
|
|
2374
3098
|
}
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
3099
|
+
return latest === undefined ? undefined : { currentVersion: versions.currentVersion, version: latest };
|
|
3100
|
+
}
|
|
3101
|
+
function isCancellation2(cause, signal) {
|
|
3102
|
+
return signal?.aborted === true || cause instanceof DOMException && cause.name === "AbortError";
|
|
3103
|
+
}
|
|
3104
|
+
async function checkForUpdate(versions, options = {}) {
|
|
3105
|
+
const fetcher = options.fetch ?? ((url, input) => globalThis.fetch(url, input));
|
|
3106
|
+
let response;
|
|
3107
|
+
try {
|
|
3108
|
+
response = await fetcher(registryUrl, options.signal ? { signal: options.signal } : {});
|
|
3109
|
+
} catch (cause) {
|
|
3110
|
+
if (isCancellation2(cause, options.signal)) {
|
|
3111
|
+
return { ok: false, error: { tag: "UpdateCheckCancelled", message: "Plugin update check cancelled" } };
|
|
2381
3112
|
}
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
return refresh();
|
|
2387
|
-
}
|
|
2388
|
-
return;
|
|
2389
|
-
});
|
|
2390
|
-
inFlight = wrapped;
|
|
2391
|
-
return wrapped;
|
|
3113
|
+
return {
|
|
3114
|
+
ok: false,
|
|
3115
|
+
error: { tag: "UpdateCheckUnavailable", message: "Unable to check for plugin updates", cause }
|
|
3116
|
+
};
|
|
2392
3117
|
}
|
|
2393
|
-
|
|
2394
|
-
start() {
|
|
2395
|
-
if (stopped)
|
|
2396
|
-
return Promise.resolve();
|
|
2397
|
-
if (!timerRegistered) {
|
|
2398
|
-
timer = scheduler.setInterval(() => {
|
|
2399
|
-
refresh().catch(input.onError);
|
|
2400
|
-
}, pollIntervalMilliseconds);
|
|
2401
|
-
timerRegistered = true;
|
|
2402
|
-
}
|
|
2403
|
-
return refresh();
|
|
2404
|
-
},
|
|
2405
|
-
refresh,
|
|
2406
|
-
stop() {
|
|
2407
|
-
if (stopped)
|
|
2408
|
-
return;
|
|
2409
|
-
stopped = true;
|
|
2410
|
-
controller.abort();
|
|
2411
|
-
if (timerRegistered)
|
|
2412
|
-
scheduler.clearInterval(timer);
|
|
2413
|
-
}
|
|
2414
|
-
};
|
|
2415
|
-
}
|
|
2416
|
-
async function openPullRequest(pullRequest, options = {}) {
|
|
2417
|
-
const platform = options.platform ?? process.platform;
|
|
2418
|
-
const executable = platform === "darwin" ? "open" : platform === "linux" ? "xdg-open" : undefined;
|
|
2419
|
-
if (executable === undefined) {
|
|
3118
|
+
if (!response.ok) {
|
|
2420
3119
|
return {
|
|
2421
3120
|
ok: false,
|
|
2422
3121
|
error: {
|
|
2423
|
-
tag: "
|
|
2424
|
-
message:
|
|
2425
|
-
|
|
3122
|
+
tag: "UpdateCheckUnavailable",
|
|
3123
|
+
message: "Unable to check for plugin updates",
|
|
3124
|
+
cause: new Error(`npm registry returned HTTP ${response.status}`)
|
|
2426
3125
|
}
|
|
2427
3126
|
};
|
|
2428
3127
|
}
|
|
3128
|
+
let decoded;
|
|
2429
3129
|
try {
|
|
2430
|
-
await (
|
|
2431
|
-
|
|
2432
|
-
} : {});
|
|
3130
|
+
decoded = await response.json();
|
|
3131
|
+
} catch {
|
|
2433
3132
|
return {
|
|
2434
|
-
ok:
|
|
2435
|
-
|
|
3133
|
+
ok: false,
|
|
3134
|
+
error: {
|
|
3135
|
+
tag: "InvalidUpdateResponse",
|
|
3136
|
+
message: "The npm registry returned invalid plugin release metadata"
|
|
3137
|
+
}
|
|
2436
3138
|
};
|
|
2437
|
-
}
|
|
3139
|
+
}
|
|
3140
|
+
const releases = parseRegistryReleases(decoded);
|
|
3141
|
+
if (releases === undefined) {
|
|
2438
3142
|
return {
|
|
2439
3143
|
ok: false,
|
|
2440
3144
|
error: {
|
|
2441
|
-
tag: "
|
|
2442
|
-
message: "
|
|
2443
|
-
cause
|
|
3145
|
+
tag: "InvalidUpdateResponse",
|
|
3146
|
+
message: "The npm registry returned invalid plugin release metadata"
|
|
2444
3147
|
}
|
|
2445
3148
|
};
|
|
2446
3149
|
}
|
|
3150
|
+
return { ok: true, value: newestCompatibleUpdate(releases, versions) };
|
|
2447
3151
|
}
|
|
2448
|
-
function
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
3152
|
+
function parseFreshUpdateCache(input, versions, now = Date.now()) {
|
|
3153
|
+
if (!isRecord2(input))
|
|
3154
|
+
return;
|
|
3155
|
+
if (typeof input.checkedAt !== "number" || !Number.isFinite(input.checkedAt) || input.checkedAt > now || now - input.checkedAt >= updateCacheMilliseconds || input.currentVersion !== versions.currentVersion || input.opencodeVersion !== versions.opencodeVersion || input.availableVersion !== null && (typeof input.availableVersion !== "string" || !isNewerStableVersion(input.availableVersion, versions.currentVersion))) {
|
|
3156
|
+
return;
|
|
3157
|
+
}
|
|
3158
|
+
return input.availableVersion;
|
|
3159
|
+
}
|
|
3160
|
+
function isMissingFile(cause) {
|
|
3161
|
+
return cause instanceof Error && "code" in cause && cause.code === "ENOENT";
|
|
3162
|
+
}
|
|
3163
|
+
async function readConfig(path) {
|
|
3164
|
+
try {
|
|
3165
|
+
return Bun.JSONC.parse(await readFile(path, "utf8"));
|
|
3166
|
+
} catch (cause) {
|
|
3167
|
+
if (isMissingFile(cause))
|
|
3168
|
+
return;
|
|
3169
|
+
return;
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
function configuredPluginSpecifier(input) {
|
|
3173
|
+
if (typeof input === "string")
|
|
3174
|
+
return input;
|
|
3175
|
+
if (Array.isArray(input) && input.length === 2 && typeof input[0] === "string" && isRecord2(input[1])) {
|
|
3176
|
+
return input[0];
|
|
3177
|
+
}
|
|
3178
|
+
return;
|
|
3179
|
+
}
|
|
3180
|
+
function containsPlugin(input) {
|
|
3181
|
+
if (!isRecord2(input) || !Array.isArray(input.plugin))
|
|
3182
|
+
return false;
|
|
3183
|
+
return input.plugin.some((entry) => {
|
|
3184
|
+
const spec = configuredPluginSpecifier(entry);
|
|
3185
|
+
return spec !== undefined && (spec === packageName || spec.startsWith(`${packageName}/`) || spec.startsWith(`${packageName}@`));
|
|
3186
|
+
});
|
|
3187
|
+
}
|
|
3188
|
+
async function containsConfiguredPlugin(directory) {
|
|
3189
|
+
const configs = await Promise.all(["opencode.json", "opencode.jsonc", "tui.json", "tui.jsonc"].map((name) => readConfig(join(directory, name))));
|
|
3190
|
+
return configs.some(containsPlugin);
|
|
3191
|
+
}
|
|
3192
|
+
async function detectInstallationScopes(input) {
|
|
3193
|
+
const [project, global2] = await Promise.all([
|
|
3194
|
+
containsConfiguredPlugin(input.projectConfigDirectory),
|
|
3195
|
+
containsConfiguredPlugin(input.globalConfigDirectory)
|
|
3196
|
+
]);
|
|
3197
|
+
return [...project ? ["project"] : [], ...global2 ? ["global"] : []];
|
|
3198
|
+
}
|
|
3199
|
+
function updateCommand(version, scope) {
|
|
3200
|
+
const globalFlag = scope === "global" ? " --global" : "";
|
|
3201
|
+
return `opencode plugin ${packageName}@${version}${globalFlag} --force`;
|
|
3202
|
+
}
|
|
3203
|
+
function formatUpdateInstructions(version, scopes) {
|
|
3204
|
+
const commands = scopes.length === 0 ? ["project", "global"] : scopes;
|
|
3205
|
+
const body = commands.length === 1 ? updateCommand(version, commands[0]) : commands.map((scope) => `${scope === "project" ? "Project" : "Global"} installation:
|
|
3206
|
+
${updateCommand(version, scope)}`).join(`
|
|
3207
|
+
|
|
3208
|
+
`);
|
|
3209
|
+
return `${body}
|
|
3210
|
+
|
|
3211
|
+
Restart OpenCode after updating.`;
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3214
|
+
// src/plugin-update-tui.tsx
|
|
3215
|
+
function createUpdateBus() {
|
|
3216
|
+
const listeners = new Set;
|
|
3217
|
+
let version;
|
|
3218
|
+
return {
|
|
3219
|
+
current: () => version,
|
|
3220
|
+
publish(value) {
|
|
3221
|
+
version = value;
|
|
3222
|
+
for (const listener of listeners)
|
|
3223
|
+
listener(value);
|
|
3224
|
+
},
|
|
3225
|
+
subscribe(listener) {
|
|
3226
|
+
listeners.add(listener);
|
|
3227
|
+
return () => listeners.delete(listener);
|
|
2464
3228
|
}
|
|
2465
3229
|
};
|
|
2466
3230
|
}
|
|
2467
|
-
function
|
|
2468
|
-
const
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
3231
|
+
function writeUpdateCache(api, versions, availableVersion, now) {
|
|
3232
|
+
const cache = {
|
|
3233
|
+
checkedAt: now(),
|
|
3234
|
+
...versions,
|
|
3235
|
+
availableVersion
|
|
3236
|
+
};
|
|
3237
|
+
api.kv.set(updateCacheKey, cache);
|
|
2472
3238
|
}
|
|
2473
|
-
function
|
|
3239
|
+
function waitForKvReady(api) {
|
|
2474
3240
|
return new Promise((resolve) => {
|
|
2475
|
-
|
|
2476
|
-
const
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
finished = true;
|
|
2482
|
-
controller.abort();
|
|
2483
|
-
api.ui.dialog.clear();
|
|
2484
|
-
resolve(value);
|
|
3241
|
+
let timer;
|
|
3242
|
+
const finish = (ready) => {
|
|
3243
|
+
if (timer !== undefined)
|
|
3244
|
+
clearTimeout(timer);
|
|
3245
|
+
api.lifecycle.signal.removeEventListener("abort", onAbort);
|
|
3246
|
+
resolve(ready);
|
|
2485
3247
|
};
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
return _el$;
|
|
2499
|
-
})() : null,
|
|
2500
|
-
get busy() {
|
|
2501
|
-
return busy();
|
|
2502
|
-
},
|
|
2503
|
-
busyText: "Resolving repository",
|
|
2504
|
-
onConfirm: (value) => {
|
|
2505
|
-
if (busy())
|
|
2506
|
-
return;
|
|
2507
|
-
setBusy(true);
|
|
2508
|
-
resolvePullRequestInput(value, {
|
|
2509
|
-
directory: options.directory,
|
|
2510
|
-
...options.runner ? {
|
|
2511
|
-
runner: options.runner
|
|
2512
|
-
} : {},
|
|
2513
|
-
signal
|
|
2514
|
-
}).then((result) => {
|
|
2515
|
-
if (finished)
|
|
2516
|
-
return;
|
|
2517
|
-
setBusy(false);
|
|
2518
|
-
if (result.ok) {
|
|
2519
|
-
finish(result.value);
|
|
2520
|
-
return;
|
|
2521
|
-
}
|
|
2522
|
-
if (result.error.tag === "RepositoryResolutionCancelled") {
|
|
2523
|
-
finish(undefined);
|
|
2524
|
-
return;
|
|
2525
|
-
}
|
|
2526
|
-
setError(result.error.message);
|
|
2527
|
-
});
|
|
2528
|
-
},
|
|
2529
|
-
onCancel: () => finish(undefined)
|
|
2530
|
-
});
|
|
2531
|
-
}, () => {
|
|
2532
|
-
if (finished)
|
|
2533
|
-
return;
|
|
2534
|
-
finished = true;
|
|
2535
|
-
controller.abort();
|
|
2536
|
-
resolve(undefined);
|
|
3248
|
+
const onAbort = () => finish(false);
|
|
3249
|
+
const check = () => {
|
|
3250
|
+
if (api.lifecycle.signal.aborted) {
|
|
3251
|
+
finish(false);
|
|
3252
|
+
} else if (api.kv.ready) {
|
|
3253
|
+
finish(true);
|
|
3254
|
+
} else {
|
|
3255
|
+
timer = setTimeout(check, 10);
|
|
3256
|
+
}
|
|
3257
|
+
};
|
|
3258
|
+
api.lifecycle.signal.addEventListener("abort", onAbort, {
|
|
3259
|
+
once: true
|
|
2537
3260
|
});
|
|
3261
|
+
check();
|
|
2538
3262
|
});
|
|
2539
3263
|
}
|
|
2540
|
-
function
|
|
2541
|
-
return
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
3264
|
+
function updateStatusLabel(version) {
|
|
3265
|
+
return `${version} available`;
|
|
3266
|
+
}
|
|
3267
|
+
function createPluginUpdateController(api, dependencies, release) {
|
|
3268
|
+
const updateBus = createUpdateBus();
|
|
3269
|
+
const updateChecker = dependencies.updateChecker ?? checkForUpdate;
|
|
3270
|
+
const installationScopes = dependencies.installationScopes ?? detectInstallationScopes;
|
|
3271
|
+
const now = dependencies.now ?? Date.now;
|
|
3272
|
+
const versions = release?.source === "npm" && release.version !== undefined ? {
|
|
3273
|
+
currentVersion: release.version,
|
|
3274
|
+
opencodeVersion: api.app.version
|
|
3275
|
+
} : undefined;
|
|
3276
|
+
let updateOperations = Promise.resolve();
|
|
3277
|
+
function serializeUpdateOperation(operation) {
|
|
3278
|
+
const result = updateOperations.then(operation);
|
|
3279
|
+
updateOperations = result.then(() => {
|
|
3280
|
+
return;
|
|
3281
|
+
}, () => {
|
|
3282
|
+
return;
|
|
3283
|
+
});
|
|
3284
|
+
return result;
|
|
3285
|
+
}
|
|
3286
|
+
let startup;
|
|
3287
|
+
if (versions !== undefined) {
|
|
3288
|
+
startup = serializeUpdateOperation(async () => {
|
|
3289
|
+
try {
|
|
3290
|
+
if (!await waitForKvReady(api))
|
|
3291
|
+
return;
|
|
3292
|
+
const cached = parseFreshUpdateCache(api.kv.get(updateCacheKey), versions, now());
|
|
3293
|
+
if (cached !== undefined) {
|
|
3294
|
+
updateBus.publish(cached ?? undefined);
|
|
3295
|
+
return;
|
|
3296
|
+
}
|
|
3297
|
+
const result = await updateChecker(versions, {
|
|
3298
|
+
signal: api.lifecycle.signal
|
|
3299
|
+
});
|
|
3300
|
+
if (api.lifecycle.signal.aborted)
|
|
3301
|
+
return;
|
|
3302
|
+
const availableVersion = result.ok ? result.value?.version ?? null : null;
|
|
3303
|
+
writeUpdateCache(api, versions, availableVersion, now);
|
|
3304
|
+
if (result.ok)
|
|
3305
|
+
updateBus.publish(result.value?.version);
|
|
3306
|
+
} catch {
|
|
3307
|
+
if (!api.lifecycle.signal.aborted && api.kv.ready)
|
|
3308
|
+
writeUpdateCache(api, versions, null, now);
|
|
3309
|
+
}
|
|
3310
|
+
});
|
|
3311
|
+
}
|
|
3312
|
+
const command = {
|
|
3313
|
+
name: "pr.tracker.plugin.update",
|
|
3314
|
+
title: "Update PR tracker plugin",
|
|
3315
|
+
category: "Plugin",
|
|
3316
|
+
namespace: "palette",
|
|
3317
|
+
slashName: "pr-tracker-plugin-update",
|
|
3318
|
+
async run() {
|
|
3319
|
+
if (versions === undefined) {
|
|
3320
|
+
api.ui.toast({
|
|
3321
|
+
variant: "info",
|
|
3322
|
+
title: "Pull request tracker",
|
|
3323
|
+
message: "Update checks are unavailable for this plugin installation"
|
|
3324
|
+
});
|
|
2545
3325
|
return;
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
3326
|
+
}
|
|
3327
|
+
const result = await serializeUpdateOperation(async () => {
|
|
3328
|
+
if (!await waitForKvReady(api))
|
|
3329
|
+
return;
|
|
3330
|
+
const checked = await updateChecker(versions, {
|
|
3331
|
+
signal: api.lifecycle.signal
|
|
3332
|
+
});
|
|
3333
|
+
if (api.lifecycle.signal.aborted || !checked.ok && checked.error.tag === "UpdateCheckCancelled") {
|
|
3334
|
+
return;
|
|
3335
|
+
}
|
|
3336
|
+
if (checked.ok) {
|
|
3337
|
+
writeUpdateCache(api, versions, checked.value?.version ?? null, now);
|
|
3338
|
+
updateBus.publish(checked.value?.version);
|
|
3339
|
+
}
|
|
3340
|
+
return checked;
|
|
3341
|
+
});
|
|
3342
|
+
if (result === undefined)
|
|
3343
|
+
return;
|
|
3344
|
+
if (!result.ok) {
|
|
3345
|
+
api.ui.toast({
|
|
3346
|
+
variant: "error",
|
|
3347
|
+
title: "Pull request tracker",
|
|
3348
|
+
message: result.error.message
|
|
3349
|
+
});
|
|
3350
|
+
return;
|
|
3351
|
+
}
|
|
3352
|
+
if (result.value === undefined) {
|
|
3353
|
+
api.ui.toast({
|
|
3354
|
+
variant: "info",
|
|
3355
|
+
title: "Pull request tracker",
|
|
3356
|
+
message: "PR tracker is up to date"
|
|
3357
|
+
});
|
|
3358
|
+
return;
|
|
3359
|
+
}
|
|
3360
|
+
const update = result.value;
|
|
3361
|
+
let scopes;
|
|
3362
|
+
try {
|
|
3363
|
+
const projectRoot = api.state.path.worktree === "/" ? api.state.path.directory : api.state.path.worktree;
|
|
3364
|
+
scopes = await installationScopes({
|
|
3365
|
+
projectConfigDirectory: join2(projectRoot, ".opencode"),
|
|
3366
|
+
globalConfigDirectory: api.state.path.config
|
|
3367
|
+
});
|
|
3368
|
+
} catch {
|
|
3369
|
+
scopes = [];
|
|
3370
|
+
}
|
|
3371
|
+
if (api.lifecycle.signal.aborted)
|
|
3372
|
+
return;
|
|
3373
|
+
api.ui.dialog.setSize("medium");
|
|
3374
|
+
api.ui.dialog.replace(() => _$createComponent2(api.ui.DialogAlert, {
|
|
3375
|
+
get title() {
|
|
3376
|
+
return `Update PR tracker to ${update.version}`;
|
|
3377
|
+
},
|
|
3378
|
+
get message() {
|
|
3379
|
+
return formatUpdateInstructions(update.version, scopes);
|
|
3380
|
+
}
|
|
3381
|
+
}));
|
|
3382
|
+
}
|
|
3383
|
+
};
|
|
3384
|
+
return {
|
|
3385
|
+
command,
|
|
3386
|
+
current: updateBus.current,
|
|
3387
|
+
subscribe: updateBus.subscribe,
|
|
3388
|
+
startup
|
|
3389
|
+
};
|
|
3390
|
+
}
|
|
3391
|
+
|
|
3392
|
+
// src/pull-request-tui.tsx
|
|
3393
|
+
import { createTextNode as _$createTextNode2 } from "@opentui/solid";
|
|
3394
|
+
import { insertNode as _$insertNode2 } from "@opentui/solid";
|
|
3395
|
+
import { memo as _$memo } from "@opentui/solid";
|
|
3396
|
+
import { setProp as _$setProp2 } from "@opentui/solid";
|
|
3397
|
+
import { effect as _$effect2 } from "@opentui/solid";
|
|
3398
|
+
import { insert as _$insert2 } from "@opentui/solid";
|
|
3399
|
+
import { createElement as _$createElement2 } from "@opentui/solid";
|
|
3400
|
+
import { createComponent as _$createComponent3 } from "@opentui/solid";
|
|
3401
|
+
import { TextAttributes } from "@opentui/core";
|
|
3402
|
+
import { createSignal as createSignal2, onCleanup } from "solid-js";
|
|
3403
|
+
|
|
3404
|
+
// src/attach.ts
|
|
3405
|
+
async function attachPullRequest(dependencies, sessionID, pullRequest, options = {}) {
|
|
3406
|
+
return dependencies.store.attach(sessionID, pullRequest, {
|
|
3407
|
+
async validate() {
|
|
3408
|
+
const batch = await dependencies.github.get([pullRequest], options);
|
|
3409
|
+
if (!batch.ok)
|
|
3410
|
+
return batch;
|
|
3411
|
+
const item = batch.value[0];
|
|
3412
|
+
if (item === undefined)
|
|
3413
|
+
throw new Error("GitHub client omitted the requested pull request");
|
|
3414
|
+
if (!item.ok)
|
|
3415
|
+
return item;
|
|
3416
|
+
return { ok: true, value: undefined };
|
|
3417
|
+
}
|
|
3418
|
+
});
|
|
3419
|
+
}
|
|
3420
|
+
var invalidPullRequestInput = {
|
|
3421
|
+
ok: false,
|
|
3422
|
+
error: {
|
|
3423
|
+
tag: "InvalidPullRequestInput",
|
|
3424
|
+
message: "Expected https://github.com/<owner>/<repository>/pull/<positive-integer>, github.com/<owner>/<repository>/pull/<positive-integer>, or a positive pull request number"
|
|
3425
|
+
}
|
|
3426
|
+
};
|
|
3427
|
+
var repositoryResolutionFailed = {
|
|
3428
|
+
tag: "RepositoryResolutionFailed",
|
|
3429
|
+
message: "Unable to resolve the current GitHub repository with gh; attach with a full URL instead"
|
|
3430
|
+
};
|
|
3431
|
+
var repositoryResolutionCancelled = {
|
|
3432
|
+
ok: false,
|
|
3433
|
+
error: { tag: "RepositoryResolutionCancelled" }
|
|
3434
|
+
};
|
|
3435
|
+
function isCancellation3(cause, signal) {
|
|
3436
|
+
if (signal?.aborted)
|
|
3437
|
+
return true;
|
|
3438
|
+
return cause instanceof Error && cause.name === "AbortError";
|
|
3439
|
+
}
|
|
3440
|
+
function parseRepositoryPullRequest(stdout, number) {
|
|
3441
|
+
let decoded;
|
|
3442
|
+
try {
|
|
3443
|
+
decoded = JSON.parse(stdout);
|
|
3444
|
+
} catch {
|
|
3445
|
+
return { ok: false, error: repositoryResolutionFailed };
|
|
3446
|
+
}
|
|
3447
|
+
if (decoded === null || typeof decoded !== "object" || !("url" in decoded) || typeof decoded.url !== "string" || decoded.url === "") {
|
|
3448
|
+
return { ok: false, error: repositoryResolutionFailed };
|
|
3449
|
+
}
|
|
3450
|
+
const repositoryUrl = decoded.url.endsWith("/") ? decoded.url.slice(0, -1) : decoded.url;
|
|
3451
|
+
const pullRequest = parsePullRequestUrl(`${repositoryUrl}/pull/${number}`);
|
|
3452
|
+
return pullRequest.ok ? pullRequest : { ok: false, error: repositoryResolutionFailed };
|
|
3453
|
+
}
|
|
3454
|
+
async function resolvePullRequestInput(input, options) {
|
|
3455
|
+
const direct = parsePullRequestUrl(input);
|
|
3456
|
+
if (direct.ok)
|
|
3457
|
+
return direct;
|
|
3458
|
+
if (input.trim() !== input || !/^\d+$/.test(input))
|
|
3459
|
+
return invalidPullRequestInput;
|
|
3460
|
+
const number = Number(input);
|
|
3461
|
+
if (!Number.isSafeInteger(number) || number <= 0)
|
|
3462
|
+
return invalidPullRequestInput;
|
|
3463
|
+
let stdout;
|
|
3464
|
+
try {
|
|
3465
|
+
const result = await (options.runner ?? execFileRunner)("gh", ["repo", "view", "--json", "url"], {
|
|
3466
|
+
cwd: options.directory,
|
|
3467
|
+
...options.signal ? { signal: options.signal } : {}
|
|
3468
|
+
});
|
|
3469
|
+
stdout = result.stdout;
|
|
3470
|
+
} catch (cause) {
|
|
3471
|
+
if (isCancellation3(cause, options.signal))
|
|
3472
|
+
return repositoryResolutionCancelled;
|
|
3473
|
+
return { ok: false, error: { ...repositoryResolutionFailed, cause } };
|
|
3474
|
+
}
|
|
3475
|
+
return parseRepositoryPullRequest(stdout, number);
|
|
3476
|
+
}
|
|
3477
|
+
|
|
3478
|
+
// src/polling.ts
|
|
3479
|
+
var pollIntervalMilliseconds = 60000;
|
|
3480
|
+
var defaultScheduler = {
|
|
3481
|
+
setInterval: (task, delay) => globalThis.setInterval(task, delay),
|
|
3482
|
+
clearInterval: (handle) => globalThis.clearInterval(handle)
|
|
3483
|
+
};
|
|
3484
|
+
function startSessionPolling(input) {
|
|
3485
|
+
const scheduler = input.scheduler ?? defaultScheduler;
|
|
3486
|
+
const statuses = new Map;
|
|
3487
|
+
const controller = new AbortController;
|
|
3488
|
+
let timer;
|
|
3489
|
+
let timerRegistered = false;
|
|
3490
|
+
let stopped = false;
|
|
3491
|
+
let inFlight;
|
|
3492
|
+
let queued;
|
|
3493
|
+
function project(attachments) {
|
|
3494
|
+
return attachments.map((attachment) => ({
|
|
3495
|
+
attachment,
|
|
3496
|
+
status: statuses.get(attachment.pullRequest.url) ?? { tag: "Unavailable" }
|
|
3497
|
+
}));
|
|
3498
|
+
}
|
|
3499
|
+
async function poll() {
|
|
3500
|
+
const attachments = await input.store.list(input.sessionID);
|
|
3501
|
+
if (stopped)
|
|
3502
|
+
return { ok: true, value: "stopped" };
|
|
3503
|
+
if (!attachments.ok) {
|
|
3504
|
+
input.publish([]);
|
|
3505
|
+
input.onStateFailure(attachments.error);
|
|
3506
|
+
return attachments;
|
|
3507
|
+
}
|
|
3508
|
+
const attachedUrls = new Set(attachments.value.map((attachment) => attachment.pullRequest.url));
|
|
3509
|
+
for (const url of statuses.keys()) {
|
|
3510
|
+
if (!attachedUrls.has(url))
|
|
3511
|
+
statuses.delete(url);
|
|
3512
|
+
}
|
|
3513
|
+
input.publish(project(attachments.value));
|
|
3514
|
+
if (attachments.value.length === 0)
|
|
3515
|
+
return { ok: true, value: "no_attachments" };
|
|
3516
|
+
const refreshable = attachments.value.filter((attachment) => {
|
|
3517
|
+
const previous = statuses.get(attachment.pullRequest.url);
|
|
3518
|
+
return previous?.tag !== "Available" || previous.state.tag !== "Merged";
|
|
3519
|
+
});
|
|
3520
|
+
const batch = await input.github.get(refreshable.map((attachment) => attachment.pullRequest), { signal: controller.signal });
|
|
3521
|
+
if (stopped)
|
|
3522
|
+
return { ok: true, value: "stopped" };
|
|
3523
|
+
let batchDiagnostic;
|
|
3524
|
+
let failure;
|
|
3525
|
+
if (!batch.ok) {
|
|
3526
|
+
if (batch.error.tag === "GitHubCancelled")
|
|
3527
|
+
return batch;
|
|
3528
|
+
batchDiagnostic = batch.error.tag === "GitHubBatchLimitExceeded" ? "GitHubUnavailable" : batch.error.tag;
|
|
3529
|
+
failure = batch.error;
|
|
3530
|
+
}
|
|
3531
|
+
for (const [index, attachment] of refreshable.entries()) {
|
|
3532
|
+
const previous = statuses.get(attachment.pullRequest.url);
|
|
3533
|
+
const result = batch.ok ? batch.value[index] : undefined;
|
|
3534
|
+
if (result?.ok) {
|
|
3535
|
+
statuses.set(attachment.pullRequest.url, result.value);
|
|
3536
|
+
continue;
|
|
3537
|
+
}
|
|
3538
|
+
const diagnostic = result === undefined ? batchDiagnostic ?? "GitHubUnavailable" : result.error.tag;
|
|
3539
|
+
if (result !== undefined && !result.ok)
|
|
3540
|
+
failure ??= result.error;
|
|
3541
|
+
statuses.set(attachment.pullRequest.url, previous?.tag === "Available" ? { ...previous, stale: true, diagnostic } : { tag: "Unavailable", diagnostic });
|
|
3542
|
+
}
|
|
3543
|
+
if (!stopped)
|
|
3544
|
+
input.publish(project(attachments.value));
|
|
3545
|
+
return failure === undefined ? { ok: true, value: "refreshed" } : { ok: false, error: failure };
|
|
3546
|
+
}
|
|
3547
|
+
function startQueuedRefresh() {
|
|
3548
|
+
inFlight = undefined;
|
|
3549
|
+
const next = queued;
|
|
3550
|
+
queued = undefined;
|
|
3551
|
+
if (next === undefined)
|
|
3552
|
+
return;
|
|
3553
|
+
if (stopped) {
|
|
3554
|
+
next.resolve({ ok: true, value: "stopped" });
|
|
3555
|
+
return;
|
|
3556
|
+
}
|
|
3557
|
+
requestRefresh().then((result) => next.resolve(result), (error) => next.reject(error));
|
|
3558
|
+
}
|
|
3559
|
+
function requestRefresh() {
|
|
3560
|
+
if (stopped)
|
|
3561
|
+
return Promise.resolve({ ok: true, value: "stopped" });
|
|
3562
|
+
if (inFlight) {
|
|
3563
|
+
if (queued !== undefined)
|
|
3564
|
+
return queued.promise;
|
|
3565
|
+
let resolve;
|
|
3566
|
+
let reject;
|
|
3567
|
+
const promise = new Promise((resolvePromise, rejectPromise) => {
|
|
3568
|
+
resolve = resolvePromise;
|
|
3569
|
+
reject = rejectPromise;
|
|
3570
|
+
});
|
|
3571
|
+
queued = { promise, resolve, reject };
|
|
3572
|
+
return promise;
|
|
3573
|
+
}
|
|
3574
|
+
const current = poll();
|
|
3575
|
+
inFlight = current;
|
|
3576
|
+
current.then(startQueuedRefresh, startQueuedRefresh);
|
|
3577
|
+
return current;
|
|
3578
|
+
}
|
|
3579
|
+
function scheduledRefresh() {
|
|
3580
|
+
return requestRefresh().then(() => {
|
|
3581
|
+
return;
|
|
3582
|
+
});
|
|
3583
|
+
}
|
|
3584
|
+
return {
|
|
3585
|
+
start() {
|
|
3586
|
+
if (stopped)
|
|
3587
|
+
return Promise.resolve();
|
|
3588
|
+
if (!timerRegistered) {
|
|
3589
|
+
timer = scheduler.setInterval(() => {
|
|
3590
|
+
scheduledRefresh().catch(input.onError);
|
|
3591
|
+
}, pollIntervalMilliseconds);
|
|
3592
|
+
timerRegistered = true;
|
|
3593
|
+
}
|
|
3594
|
+
return scheduledRefresh();
|
|
3595
|
+
},
|
|
3596
|
+
refresh: scheduledRefresh,
|
|
3597
|
+
forceRefresh: requestRefresh,
|
|
3598
|
+
stop() {
|
|
3599
|
+
if (stopped)
|
|
3600
|
+
return;
|
|
3601
|
+
stopped = true;
|
|
3602
|
+
controller.abort();
|
|
3603
|
+
if (timerRegistered)
|
|
3604
|
+
scheduler.clearInterval(timer);
|
|
3605
|
+
}
|
|
3606
|
+
};
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3609
|
+
// src/pull-request-tui.tsx
|
|
3610
|
+
function attachPullRequest2(store, sessionID, input, options = {}) {
|
|
3611
|
+
const pullRequest = parsePullRequestUrl(input);
|
|
3612
|
+
if (!pullRequest.ok)
|
|
3613
|
+
return Promise.resolve(pullRequest);
|
|
3614
|
+
return attachPullRequest({
|
|
3615
|
+
store,
|
|
3616
|
+
github: options.github ?? createGitHubClient()
|
|
3617
|
+
}, sessionID, pullRequest.value, options.signal ? {
|
|
3618
|
+
signal: options.signal
|
|
3619
|
+
} : {});
|
|
3620
|
+
}
|
|
3621
|
+
function createRefreshBus() {
|
|
3622
|
+
const listeners = new Map;
|
|
3623
|
+
return {
|
|
3624
|
+
emit(sessionID) {
|
|
3625
|
+
for (const listener of listeners.get(sessionID) ?? [])
|
|
3626
|
+
listener.refresh();
|
|
3627
|
+
},
|
|
3628
|
+
async forceRefresh(sessionID) {
|
|
3629
|
+
const sessionListeners = listeners.get(sessionID);
|
|
3630
|
+
if (sessionListeners === undefined || sessionListeners.size === 0)
|
|
3631
|
+
return;
|
|
3632
|
+
const settled = await Promise.allSettled([...sessionListeners].map((listener) => listener.forceRefresh()));
|
|
3633
|
+
const rejected = settled.find((result) => result.status === "rejected");
|
|
3634
|
+
if (rejected !== undefined)
|
|
3635
|
+
throw rejected.reason;
|
|
3636
|
+
const results = settled.flatMap((result) => result.status === "fulfilled" ? [result.value] : []);
|
|
3637
|
+
return results.find((result) => !result.ok) ?? results.find((result) => result.ok && result.value === "refreshed") ?? results[0];
|
|
3638
|
+
},
|
|
3639
|
+
subscribe(sessionID, listener) {
|
|
3640
|
+
const sessionListeners = listeners.get(sessionID) ?? new Set;
|
|
3641
|
+
sessionListeners.add(listener);
|
|
3642
|
+
listeners.set(sessionID, sessionListeners);
|
|
3643
|
+
return () => {
|
|
3644
|
+
sessionListeners.delete(listener);
|
|
3645
|
+
if (sessionListeners.size === 0)
|
|
3646
|
+
listeners.delete(sessionID);
|
|
3647
|
+
};
|
|
3648
|
+
}
|
|
3649
|
+
};
|
|
3650
|
+
}
|
|
3651
|
+
function currentSessionID(api) {
|
|
3652
|
+
const route = api.route.current;
|
|
3653
|
+
if (route.name !== "session" || !("params" in route))
|
|
3654
|
+
return;
|
|
3655
|
+
return typeof route.params?.sessionID === "string" ? route.params.sessionID : undefined;
|
|
3656
|
+
}
|
|
3657
|
+
function promptForPullRequest(api, options) {
|
|
3658
|
+
return new Promise((resolve) => {
|
|
3659
|
+
const controller = new AbortController;
|
|
3660
|
+
const signal = AbortSignal.any([options.signal, controller.signal]);
|
|
3661
|
+
let finished = false;
|
|
3662
|
+
const onAbort = () => finish(undefined);
|
|
3663
|
+
const finish = (value, clearDialog = true) => {
|
|
3664
|
+
if (finished)
|
|
3665
|
+
return;
|
|
3666
|
+
finished = true;
|
|
3667
|
+
options.signal.removeEventListener("abort", onAbort);
|
|
3668
|
+
controller.abort();
|
|
3669
|
+
if (clearDialog)
|
|
3670
|
+
api.ui.dialog.clear();
|
|
3671
|
+
resolve(value);
|
|
3672
|
+
};
|
|
3673
|
+
if (options.signal.aborted) {
|
|
3674
|
+
finish(undefined, false);
|
|
3675
|
+
return;
|
|
3676
|
+
}
|
|
3677
|
+
options.signal.addEventListener("abort", onAbort, {
|
|
3678
|
+
once: true
|
|
3679
|
+
});
|
|
3680
|
+
api.ui.dialog.setSize("medium");
|
|
3681
|
+
api.ui.dialog.replace(() => {
|
|
3682
|
+
const [error, setError] = createSignal2();
|
|
3683
|
+
const [busy, setBusy] = createSignal2(false);
|
|
3684
|
+
const DialogPrompt = api.ui.DialogPrompt;
|
|
3685
|
+
return _$createComponent3(DialogPrompt, {
|
|
3686
|
+
title: "Attach pull request",
|
|
3687
|
+
placeholder: "https://github.com/owner/repository/pull/123, github.com/owner/repository/pull/123, or 123",
|
|
3688
|
+
description: () => error() ? (() => {
|
|
3689
|
+
var _el$ = _$createElement2("text");
|
|
3690
|
+
_$insert2(_el$, error);
|
|
3691
|
+
_$effect2((_$p) => _$setProp2(_el$, "fg", api.theme.current.error, _$p));
|
|
3692
|
+
return _el$;
|
|
3693
|
+
})() : null,
|
|
3694
|
+
get busy() {
|
|
3695
|
+
return busy();
|
|
3696
|
+
},
|
|
3697
|
+
busyText: "Resolving repository",
|
|
3698
|
+
onConfirm: (value) => {
|
|
3699
|
+
if (finished || busy())
|
|
3700
|
+
return;
|
|
3701
|
+
setBusy(true);
|
|
3702
|
+
resolvePullRequestInput(value, {
|
|
3703
|
+
directory: options.directory,
|
|
3704
|
+
...options.runner ? {
|
|
3705
|
+
runner: options.runner
|
|
3706
|
+
} : {},
|
|
3707
|
+
signal
|
|
3708
|
+
}).then((result) => {
|
|
3709
|
+
if (finished)
|
|
3710
|
+
return;
|
|
3711
|
+
setBusy(false);
|
|
3712
|
+
if (result.ok) {
|
|
3713
|
+
finish(result.value);
|
|
3714
|
+
return;
|
|
3715
|
+
}
|
|
3716
|
+
if (result.error.tag === "RepositoryResolutionCancelled") {
|
|
3717
|
+
finish(undefined);
|
|
3718
|
+
return;
|
|
3719
|
+
}
|
|
3720
|
+
setError(result.error.message);
|
|
3721
|
+
});
|
|
3722
|
+
},
|
|
3723
|
+
onCancel: () => finish(undefined)
|
|
3724
|
+
});
|
|
3725
|
+
}, () => finish(undefined, false));
|
|
3726
|
+
});
|
|
3727
|
+
}
|
|
3728
|
+
function selectPullRequest(api, title, attachments, signal) {
|
|
3729
|
+
return new Promise((resolve) => {
|
|
3730
|
+
let finished = false;
|
|
3731
|
+
const onAbort = () => finish(undefined);
|
|
3732
|
+
const finish = (value, clearDialog = true) => {
|
|
3733
|
+
if (finished)
|
|
3734
|
+
return;
|
|
3735
|
+
finished = true;
|
|
3736
|
+
signal.removeEventListener("abort", onAbort);
|
|
3737
|
+
if (clearDialog)
|
|
3738
|
+
api.ui.dialog.clear();
|
|
3739
|
+
resolve(value);
|
|
3740
|
+
};
|
|
3741
|
+
if (signal.aborted) {
|
|
3742
|
+
finish(undefined, false);
|
|
3743
|
+
return;
|
|
3744
|
+
}
|
|
3745
|
+
signal.addEventListener("abort", onAbort, {
|
|
3746
|
+
once: true
|
|
3747
|
+
});
|
|
3748
|
+
api.ui.dialog.setSize("medium");
|
|
3749
|
+
api.ui.dialog.replace(() => {
|
|
3750
|
+
const DialogSelect = api.ui.DialogSelect;
|
|
3751
|
+
return _$createComponent3(DialogSelect, {
|
|
3752
|
+
title,
|
|
3753
|
+
get options() {
|
|
3754
|
+
return attachments.map((attachment) => ({
|
|
3755
|
+
title: formatPullRequestRef(attachment.pullRequest),
|
|
3756
|
+
value: attachment.pullRequest,
|
|
3757
|
+
description: attachment.pullRequest.url
|
|
3758
|
+
}));
|
|
3759
|
+
},
|
|
3760
|
+
onSelect: (option) => finish(option.value)
|
|
3761
|
+
});
|
|
3762
|
+
}, () => finish(undefined, false));
|
|
3763
|
+
});
|
|
3764
|
+
}
|
|
3765
|
+
function showStateFailure(api, failure) {
|
|
3766
|
+
api.ui.toast({
|
|
3767
|
+
variant: "error",
|
|
3768
|
+
title: "Pull request tracker",
|
|
3769
|
+
message: failure.message
|
|
3770
|
+
});
|
|
3771
|
+
}
|
|
3772
|
+
function createPullRequestCommands(api, dependencies, refreshBus) {
|
|
3773
|
+
return [{
|
|
3774
|
+
name: "pr.attach",
|
|
3775
|
+
title: "Attach pull request",
|
|
3776
|
+
category: "Plugin",
|
|
3777
|
+
namespace: "palette",
|
|
3778
|
+
slashName: "pr-attach",
|
|
3779
|
+
async run() {
|
|
3780
|
+
const sessionID = currentSessionID(api);
|
|
3781
|
+
if (sessionID === undefined) {
|
|
3782
|
+
api.ui.toast({
|
|
3783
|
+
variant: "warning",
|
|
3784
|
+
title: "Pull request tracker",
|
|
3785
|
+
message: "Open a session first"
|
|
3786
|
+
});
|
|
3787
|
+
return;
|
|
3788
|
+
}
|
|
3789
|
+
const pullRequest = await promptForPullRequest(api, {
|
|
3790
|
+
directory: api.state.path.directory,
|
|
3791
|
+
...dependencies.runner ? {
|
|
3792
|
+
runner: dependencies.runner
|
|
3793
|
+
} : {},
|
|
3794
|
+
signal: api.lifecycle.signal
|
|
3795
|
+
});
|
|
3796
|
+
if (pullRequest === undefined)
|
|
3797
|
+
return;
|
|
3798
|
+
const result = await attachPullRequest(dependencies, sessionID, pullRequest, {
|
|
3799
|
+
signal: api.lifecycle.signal
|
|
3800
|
+
});
|
|
3801
|
+
if (!result.ok) {
|
|
3802
|
+
showStateFailure(api, result.error);
|
|
3803
|
+
return;
|
|
3804
|
+
}
|
|
3805
|
+
const message = result.value === "added" ? `Attached ${formatPullRequestRef(pullRequest)}` : `${formatPullRequestRef(pullRequest)} is already attached`;
|
|
3806
|
+
api.ui.toast({
|
|
3807
|
+
variant: "success",
|
|
3808
|
+
title: "Pull request tracker",
|
|
3809
|
+
message
|
|
3810
|
+
});
|
|
3811
|
+
refreshBus.emit(sessionID);
|
|
3812
|
+
}
|
|
3813
|
+
}, {
|
|
3814
|
+
name: "pr.open",
|
|
3815
|
+
title: "Open pull request",
|
|
3816
|
+
category: "Plugin",
|
|
3817
|
+
namespace: "palette",
|
|
3818
|
+
slashName: "pr-open",
|
|
3819
|
+
async run() {
|
|
3820
|
+
const sessionID = currentSessionID(api);
|
|
3821
|
+
if (sessionID === undefined) {
|
|
3822
|
+
api.ui.toast({
|
|
3823
|
+
variant: "warning",
|
|
3824
|
+
title: "Pull request tracker",
|
|
3825
|
+
message: "Open a session first"
|
|
3826
|
+
});
|
|
3827
|
+
return;
|
|
3828
|
+
}
|
|
3829
|
+
const attachments = await dependencies.store.list(sessionID);
|
|
3830
|
+
if (!attachments.ok) {
|
|
3831
|
+
showStateFailure(api, attachments.error);
|
|
3832
|
+
return;
|
|
3833
|
+
}
|
|
3834
|
+
if (attachments.value.length === 0) {
|
|
3835
|
+
api.ui.toast({
|
|
3836
|
+
variant: "info",
|
|
3837
|
+
title: "Pull request tracker",
|
|
3838
|
+
message: "No pull requests are attached"
|
|
3839
|
+
});
|
|
3840
|
+
return;
|
|
3841
|
+
}
|
|
3842
|
+
const pullRequest = await selectPullRequest(api, "Open pull request", attachments.value, api.lifecycle.signal);
|
|
3843
|
+
if (pullRequest === undefined)
|
|
3844
|
+
return;
|
|
3845
|
+
const result = await openPullRequest(pullRequest, {
|
|
3846
|
+
...dependencies.runner ? {
|
|
3847
|
+
runner: dependencies.runner
|
|
3848
|
+
} : {},
|
|
3849
|
+
signal: api.lifecycle.signal
|
|
3850
|
+
});
|
|
3851
|
+
if (!result.ok) {
|
|
3852
|
+
api.ui.toast({
|
|
3853
|
+
variant: "error",
|
|
3854
|
+
title: "Pull request tracker",
|
|
3855
|
+
message: result.error.message
|
|
3856
|
+
});
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
}, {
|
|
3860
|
+
name: "pr.detach",
|
|
3861
|
+
title: "Detach pull request",
|
|
3862
|
+
category: "Plugin",
|
|
3863
|
+
namespace: "palette",
|
|
3864
|
+
slashName: "pr-detach",
|
|
3865
|
+
async run() {
|
|
3866
|
+
const sessionID = currentSessionID(api);
|
|
3867
|
+
if (sessionID === undefined) {
|
|
3868
|
+
api.ui.toast({
|
|
3869
|
+
variant: "warning",
|
|
3870
|
+
title: "Pull request tracker",
|
|
3871
|
+
message: "Open a session first"
|
|
3872
|
+
});
|
|
3873
|
+
return;
|
|
3874
|
+
}
|
|
3875
|
+
const attachments = await dependencies.store.list(sessionID);
|
|
3876
|
+
if (!attachments.ok) {
|
|
3877
|
+
showStateFailure(api, attachments.error);
|
|
3878
|
+
return;
|
|
3879
|
+
}
|
|
3880
|
+
if (attachments.value.length === 0) {
|
|
3881
|
+
api.ui.toast({
|
|
3882
|
+
variant: "info",
|
|
3883
|
+
title: "Pull request tracker",
|
|
3884
|
+
message: "No pull requests are attached"
|
|
3885
|
+
});
|
|
3886
|
+
return;
|
|
3887
|
+
}
|
|
3888
|
+
const pullRequest = await selectPullRequest(api, "Detach pull request", attachments.value, api.lifecycle.signal);
|
|
3889
|
+
if (pullRequest === undefined)
|
|
3890
|
+
return;
|
|
3891
|
+
const result = await dependencies.store.detach(sessionID, pullRequest);
|
|
3892
|
+
if (!result.ok) {
|
|
3893
|
+
showStateFailure(api, result.error);
|
|
3894
|
+
return;
|
|
3895
|
+
}
|
|
3896
|
+
const message = result.value === "removed" ? `Detached ${formatPullRequestRef(pullRequest)}` : `${formatPullRequestRef(pullRequest)} was not attached`;
|
|
3897
|
+
api.ui.toast({
|
|
3898
|
+
variant: "success",
|
|
3899
|
+
title: "Pull request tracker",
|
|
3900
|
+
message
|
|
3901
|
+
});
|
|
3902
|
+
refreshBus.emit(sessionID);
|
|
3903
|
+
}
|
|
3904
|
+
}, {
|
|
3905
|
+
name: "pr.sync",
|
|
3906
|
+
title: "Sync pull request status",
|
|
3907
|
+
category: "Plugin",
|
|
3908
|
+
namespace: "palette",
|
|
3909
|
+
slashName: "pr-sync",
|
|
3910
|
+
async run() {
|
|
3911
|
+
const sessionID = currentSessionID(api);
|
|
3912
|
+
if (sessionID === undefined) {
|
|
3913
|
+
api.ui.toast({
|
|
3914
|
+
variant: "warning",
|
|
3915
|
+
title: "Pull request tracker",
|
|
3916
|
+
message: "Open a session first"
|
|
3917
|
+
});
|
|
3918
|
+
return;
|
|
3919
|
+
}
|
|
3920
|
+
try {
|
|
3921
|
+
const result = await refreshBus.forceRefresh(sessionID);
|
|
3922
|
+
if (result === undefined) {
|
|
3923
|
+
api.ui.toast({
|
|
3924
|
+
variant: "warning",
|
|
3925
|
+
title: "Pull request tracker",
|
|
3926
|
+
message: "Pull request sidebar is not available"
|
|
3927
|
+
});
|
|
3928
|
+
return;
|
|
3929
|
+
}
|
|
3930
|
+
if (!result.ok) {
|
|
3931
|
+
api.ui.toast({
|
|
3932
|
+
variant: "error",
|
|
3933
|
+
title: "Pull request tracker",
|
|
3934
|
+
message: result.error.message
|
|
3935
|
+
});
|
|
3936
|
+
return;
|
|
3937
|
+
}
|
|
3938
|
+
switch (result.value) {
|
|
3939
|
+
case "refreshed":
|
|
3940
|
+
api.ui.toast({
|
|
3941
|
+
variant: "success",
|
|
3942
|
+
title: "Pull request tracker",
|
|
3943
|
+
message: "Pull request status synced"
|
|
3944
|
+
});
|
|
3945
|
+
return;
|
|
3946
|
+
case "no_attachments":
|
|
3947
|
+
api.ui.toast({
|
|
3948
|
+
variant: "info",
|
|
3949
|
+
title: "Pull request tracker",
|
|
3950
|
+
message: "No pull requests are attached"
|
|
3951
|
+
});
|
|
3952
|
+
return;
|
|
3953
|
+
case "stopped":
|
|
3954
|
+
api.ui.toast({
|
|
3955
|
+
variant: "error",
|
|
3956
|
+
title: "Pull request tracker",
|
|
3957
|
+
message: "Unable to refresh pull request status"
|
|
3958
|
+
});
|
|
3959
|
+
return;
|
|
3960
|
+
}
|
|
3961
|
+
} catch {
|
|
3962
|
+
api.ui.toast({
|
|
3963
|
+
variant: "error",
|
|
3964
|
+
title: "Pull request tracker",
|
|
3965
|
+
message: "Unable to refresh pull request status"
|
|
3966
|
+
});
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
}];
|
|
3970
|
+
}
|
|
3971
|
+
function toneColor(theme, tone) {
|
|
3972
|
+
const colors = {
|
|
3973
|
+
green: theme.success,
|
|
3974
|
+
yellow: theme.warning,
|
|
3975
|
+
red: theme.error,
|
|
3976
|
+
purple: theme.secondary,
|
|
3977
|
+
gray: theme.textMuted
|
|
3978
|
+
};
|
|
3979
|
+
return colors[tone];
|
|
3980
|
+
}
|
|
3981
|
+
function PullRequestSidebar(props) {
|
|
3982
|
+
const [items, setItems] = createSignal2([]);
|
|
3983
|
+
const [failure, setFailure] = createSignal2();
|
|
3984
|
+
const [update, setUpdate] = createSignal2(props.updates.current());
|
|
3985
|
+
const [open, setOpen] = createSignal2(true);
|
|
3986
|
+
const collapsible = () => items().length > 2;
|
|
3987
|
+
const polling = startSessionPolling({
|
|
3988
|
+
sessionID: props.sessionID,
|
|
3989
|
+
store: props.dependencies.store,
|
|
3990
|
+
github: props.dependencies.github,
|
|
3991
|
+
publish: (value) => {
|
|
3992
|
+
setFailure(undefined);
|
|
3993
|
+
setItems(value);
|
|
3994
|
+
},
|
|
3995
|
+
onStateFailure: (error) => setFailure(error.message),
|
|
3996
|
+
onError: () => setFailure("Unable to refresh pull request status")
|
|
3997
|
+
});
|
|
3998
|
+
polling.start().catch(() => setFailure("Unable to refresh pull request status"));
|
|
3999
|
+
const unsubscribe = props.refreshBus.subscribe(props.sessionID, {
|
|
4000
|
+
refresh() {
|
|
4001
|
+
polling.refresh().catch(() => setFailure("Unable to refresh pull request status"));
|
|
4002
|
+
},
|
|
4003
|
+
async forceRefresh() {
|
|
4004
|
+
try {
|
|
4005
|
+
return await polling.forceRefresh();
|
|
4006
|
+
} catch (error) {
|
|
4007
|
+
setFailure("Unable to refresh pull request status");
|
|
4008
|
+
throw error;
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
4011
|
+
});
|
|
4012
|
+
const unsubscribeUpdate = props.updates.subscribe(setUpdate);
|
|
4013
|
+
const onAbort = () => polling.stop();
|
|
4014
|
+
props.api.lifecycle.signal.addEventListener("abort", onAbort, {
|
|
4015
|
+
once: true
|
|
4016
|
+
});
|
|
4017
|
+
onCleanup(() => {
|
|
4018
|
+
unsubscribe();
|
|
4019
|
+
unsubscribeUpdate();
|
|
4020
|
+
polling.stop();
|
|
4021
|
+
props.api.lifecycle.signal.removeEventListener("abort", onAbort);
|
|
4022
|
+
});
|
|
4023
|
+
return (() => {
|
|
4024
|
+
var _el$2 = _$createElement2("box"), _el$3 = _$createElement2("box"), _el$4 = _$createElement2("text"), _el$5 = _$createElement2("b");
|
|
4025
|
+
_$insertNode2(_el$2, _el$3);
|
|
4026
|
+
_$setProp2(_el$2, "flexDirection", "column");
|
|
4027
|
+
_$setProp2(_el$2, "gap", 1);
|
|
4028
|
+
_$insertNode2(_el$3, _el$4);
|
|
4029
|
+
_$setProp2(_el$3, "flexDirection", "row");
|
|
4030
|
+
_$setProp2(_el$3, "gap", 1);
|
|
4031
|
+
_$setProp2(_el$3, "onMouseDown", () => collapsible() && setOpen((value) => !value));
|
|
4032
|
+
_$insert2(_el$3, (() => {
|
|
4033
|
+
var _c$ = _$memo(() => !!collapsible());
|
|
4034
|
+
return () => _c$() ? (() => {
|
|
4035
|
+
var _el$7 = _$createElement2("text");
|
|
4036
|
+
_$insert2(_el$7, () => open() ? "\u25BC" : "\u25B6");
|
|
4037
|
+
_$effect2((_$p) => _$setProp2(_el$7, "fg", props.api.theme.current.text, _$p));
|
|
4038
|
+
return _el$7;
|
|
4039
|
+
})() : null;
|
|
4040
|
+
})(), _el$4);
|
|
4041
|
+
_$insertNode2(_el$4, _el$5);
|
|
4042
|
+
_$insertNode2(_el$5, _$createTextNode2(`Pull requests`));
|
|
4043
|
+
_$insert2(_el$2, (() => {
|
|
4044
|
+
var _c$2 = _$memo(() => !!(!collapsible() || open()));
|
|
4045
|
+
return () => _c$2() ? (() => {
|
|
4046
|
+
var _el$8 = _$createElement2("box");
|
|
4047
|
+
_$setProp2(_el$8, "flexDirection", "column");
|
|
4048
|
+
_$setProp2(_el$8, "gap", 1);
|
|
4049
|
+
_$insert2(_el$8, (() => {
|
|
4050
|
+
var _c$3 = _$memo(() => !!update());
|
|
4051
|
+
return () => _c$3() ? (() => {
|
|
4052
|
+
var _el$9 = _$createElement2("box"), _el$0 = _$createElement2("text"), _el$10 = _$createElement2("text");
|
|
4053
|
+
_$insertNode2(_el$9, _el$0);
|
|
4054
|
+
_$insertNode2(_el$9, _el$10);
|
|
4055
|
+
_$setProp2(_el$9, "flexDirection", "row");
|
|
4056
|
+
_$setProp2(_el$9, "onMouseUp", () => props.api.keymap.dispatchCommand("pr.tracker.plugin.update"));
|
|
4057
|
+
_$insertNode2(_el$0, _$createTextNode2(`\u2022 `));
|
|
4058
|
+
_$insert2(_el$10, () => updateStatusLabel(update()));
|
|
4059
|
+
_$effect2((_p$) => {
|
|
4060
|
+
var _v$ = props.api.theme.current.warning, _v$2 = props.api.theme.current.textMuted, _v$3 = TextAttributes.ITALIC;
|
|
4061
|
+
_v$ !== _p$.e && (_p$.e = _$setProp2(_el$0, "fg", _v$, _p$.e));
|
|
4062
|
+
_v$2 !== _p$.t && (_p$.t = _$setProp2(_el$10, "fg", _v$2, _p$.t));
|
|
4063
|
+
_v$3 !== _p$.a && (_p$.a = _$setProp2(_el$10, "attributes", _v$3, _p$.a));
|
|
4064
|
+
return _p$;
|
|
4065
|
+
}, {
|
|
4066
|
+
e: undefined,
|
|
4067
|
+
t: undefined,
|
|
4068
|
+
a: undefined
|
|
4069
|
+
});
|
|
4070
|
+
return _el$9;
|
|
4071
|
+
})() : null;
|
|
4072
|
+
})(), null);
|
|
4073
|
+
_$insert2(_el$8, (() => {
|
|
4074
|
+
var _c$4 = _$memo(() => !!failure());
|
|
4075
|
+
return () => _c$4() ? (() => {
|
|
4076
|
+
var _el$11 = _$createElement2("text");
|
|
4077
|
+
_$insert2(_el$11, failure);
|
|
4078
|
+
_$effect2((_$p) => _$setProp2(_el$11, "fg", props.api.theme.current.error, _$p));
|
|
4079
|
+
return _el$11;
|
|
4080
|
+
})() : null;
|
|
4081
|
+
})(), null);
|
|
4082
|
+
_$insert2(_el$8, (() => {
|
|
4083
|
+
var _c$5 = _$memo(() => !!(!failure() && items().length === 0));
|
|
4084
|
+
return () => _c$5() ? (() => {
|
|
4085
|
+
var _el$12 = _$createElement2("text");
|
|
4086
|
+
_$insertNode2(_el$12, _$createTextNode2(`No pull requests attached`));
|
|
4087
|
+
_$effect2((_$p) => _$setProp2(_el$12, "fg", props.api.theme.current.textMuted, _$p));
|
|
4088
|
+
return _el$12;
|
|
4089
|
+
})() : null;
|
|
4090
|
+
})(), null);
|
|
4091
|
+
_$insert2(_el$8, () => items().map((item) => {
|
|
4092
|
+
const appearance = statusAppearance(item.status);
|
|
4093
|
+
const attributes = appearance.strikethrough ? TextAttributes.STRIKETHROUGH : TextAttributes.NONE;
|
|
4094
|
+
const title = item.status.tag === "Available" ? item.status.title : "Title unavailable";
|
|
4095
|
+
return (() => {
|
|
4096
|
+
var _el$14 = _$createElement2("box"), _el$15 = _$createElement2("text"), _el$16 = _$createElement2("b"), _el$17 = _$createTextNode2(` `), _el$18 = _$createElement2("text");
|
|
4097
|
+
_$insertNode2(_el$14, _el$15);
|
|
4098
|
+
_$insertNode2(_el$14, _el$18);
|
|
4099
|
+
_$setProp2(_el$14, "flexDirection", "column");
|
|
4100
|
+
_$setProp2(_el$14, "onMouseUp", () => {
|
|
4101
|
+
openPullRequest(item.attachment.pullRequest, {
|
|
4102
|
+
...props.dependencies.runner ? {
|
|
4103
|
+
runner: props.dependencies.runner
|
|
4104
|
+
} : {},
|
|
4105
|
+
signal: props.api.lifecycle.signal
|
|
4106
|
+
}).then((result) => {
|
|
4107
|
+
if (!result.ok) {
|
|
4108
|
+
props.api.ui.toast({
|
|
4109
|
+
variant: "error",
|
|
4110
|
+
title: "Pull request tracker",
|
|
4111
|
+
message: result.error.message
|
|
4112
|
+
});
|
|
4113
|
+
}
|
|
4114
|
+
}).catch(() => {
|
|
4115
|
+
props.api.ui.toast({
|
|
4116
|
+
variant: "error",
|
|
4117
|
+
title: "Pull request tracker",
|
|
4118
|
+
message: "Unable to open the pull request"
|
|
4119
|
+
});
|
|
4120
|
+
});
|
|
4121
|
+
});
|
|
4122
|
+
_$insertNode2(_el$15, _el$16);
|
|
4123
|
+
_$insertNode2(_el$15, _el$17);
|
|
4124
|
+
_$setProp2(_el$15, "attributes", attributes);
|
|
4125
|
+
_$insert2(_el$16, () => formatPullRequestRef(item.attachment.pullRequest));
|
|
4126
|
+
_$insert2(_el$15, () => appearance.label, null);
|
|
4127
|
+
_$setProp2(_el$18, "attributes", attributes);
|
|
4128
|
+
_$insert2(_el$18, title);
|
|
4129
|
+
_$effect2((_p$) => {
|
|
4130
|
+
var _v$4 = toneColor(props.api.theme.current, appearance.tone), _v$5 = props.api.theme.current.textMuted;
|
|
4131
|
+
_v$4 !== _p$.e && (_p$.e = _$setProp2(_el$15, "fg", _v$4, _p$.e));
|
|
4132
|
+
_v$5 !== _p$.t && (_p$.t = _$setProp2(_el$18, "fg", _v$5, _p$.t));
|
|
4133
|
+
return _p$;
|
|
4134
|
+
}, {
|
|
4135
|
+
e: undefined,
|
|
4136
|
+
t: undefined
|
|
4137
|
+
});
|
|
4138
|
+
return _el$14;
|
|
4139
|
+
})();
|
|
4140
|
+
}), null);
|
|
4141
|
+
return _el$8;
|
|
4142
|
+
})() : null;
|
|
4143
|
+
})(), null);
|
|
4144
|
+
_$effect2((_$p) => _$setProp2(_el$4, "fg", props.api.theme.current.text, _$p));
|
|
4145
|
+
return _el$2;
|
|
4146
|
+
})();
|
|
4147
|
+
}
|
|
4148
|
+
|
|
4149
|
+
// src/state.ts
|
|
4150
|
+
var import_proper_lockfile = __toESM(require_proper_lockfile(), 1);
|
|
4151
|
+
import { createHash, randomUUID } from "crypto";
|
|
4152
|
+
import { mkdir, readFile as readFile2, rename, rm, writeFile } from "fs/promises";
|
|
4153
|
+
import { homedir } from "os";
|
|
4154
|
+
import { join as join3 } from "path";
|
|
4155
|
+
var maximumPullRequestsPerSession = 20;
|
|
4156
|
+
var invalidStateFile = {
|
|
4157
|
+
ok: false,
|
|
4158
|
+
error: {
|
|
4159
|
+
tag: "InvalidStateFile",
|
|
4160
|
+
message: "The session pull request state file is invalid"
|
|
4161
|
+
}
|
|
4162
|
+
};
|
|
4163
|
+
var lockStaleMilliseconds = 1e4;
|
|
4164
|
+
var lockUpdateMilliseconds = 2000;
|
|
4165
|
+
function stateUnavailable(operation, message, cause) {
|
|
4166
|
+
return { tag: "StateUnavailable", operation, message, cause };
|
|
4167
|
+
}
|
|
4168
|
+
function isRecord3(value) {
|
|
4169
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4170
|
+
}
|
|
4171
|
+
function hasExactKeys(value, keys) {
|
|
4172
|
+
const actual = Object.keys(value);
|
|
4173
|
+
return actual.length === keys.length && keys.every((key) => Object.hasOwn(value, key));
|
|
4174
|
+
}
|
|
4175
|
+
function parseState(input) {
|
|
4176
|
+
if (!isRecord3(input) || !hasExactKeys(input, ["version", "pullRequests"]))
|
|
4177
|
+
return invalidStateFile;
|
|
4178
|
+
if (input.version !== 1 || !Array.isArray(input.pullRequests))
|
|
4179
|
+
return invalidStateFile;
|
|
4180
|
+
if (input.pullRequests.length > maximumPullRequestsPerSession)
|
|
4181
|
+
return invalidStateFile;
|
|
4182
|
+
const attachments = [];
|
|
4183
|
+
const seen = new Set;
|
|
4184
|
+
for (const item of input.pullRequests) {
|
|
4185
|
+
if (!isRecord3(item) || !hasExactKeys(item, ["url", "attachedAt"]))
|
|
4186
|
+
return invalidStateFile;
|
|
4187
|
+
if (typeof item.url !== "string" || typeof item.attachedAt !== "string")
|
|
4188
|
+
return invalidStateFile;
|
|
4189
|
+
const parsed = parsePullRequestUrl(item.url);
|
|
4190
|
+
if (!parsed.ok || parsed.value.url !== item.url || seen.has(item.url))
|
|
4191
|
+
return invalidStateFile;
|
|
4192
|
+
const attachedAt = new Date(item.attachedAt);
|
|
4193
|
+
if (Number.isNaN(attachedAt.valueOf()) || attachedAt.toISOString() !== item.attachedAt)
|
|
4194
|
+
return invalidStateFile;
|
|
4195
|
+
seen.add(item.url);
|
|
4196
|
+
attachments.push({ pullRequest: parsed.value, attachedAt: item.attachedAt });
|
|
4197
|
+
}
|
|
4198
|
+
return { ok: true, value: attachments };
|
|
4199
|
+
}
|
|
4200
|
+
function isMissingFile2(cause) {
|
|
4201
|
+
return cause instanceof Error && "code" in cause && cause.code === "ENOENT";
|
|
4202
|
+
}
|
|
4203
|
+
function fileName(sessionID) {
|
|
4204
|
+
return `${createHash("sha256").update(sessionID).digest("hex")}.json`;
|
|
4205
|
+
}
|
|
4206
|
+
function defaultStateDirectory(environment = process.env, home = homedir()) {
|
|
4207
|
+
const dataHome = environment.XDG_DATA_HOME || join3(home, ".local", "share");
|
|
4208
|
+
return join3(dataHome, "opencode", "opencode-pr-tracker");
|
|
4209
|
+
}
|
|
4210
|
+
function createStateStore(options = {}) {
|
|
4211
|
+
const directory = options.directory ?? defaultStateDirectory();
|
|
4212
|
+
const now = options.now ?? (() => new Date);
|
|
4213
|
+
const lockStateFile = options.lock ?? import_proper_lockfile.lock;
|
|
4214
|
+
const attachTails = new Map;
|
|
4215
|
+
async function enqueueAttach(sessionID, operation) {
|
|
4216
|
+
const previous = attachTails.get(sessionID) ?? Promise.resolve();
|
|
4217
|
+
let release;
|
|
4218
|
+
const current = new Promise((resolve) => {
|
|
4219
|
+
release = resolve;
|
|
4220
|
+
});
|
|
4221
|
+
attachTails.set(sessionID, current);
|
|
4222
|
+
await previous;
|
|
4223
|
+
try {
|
|
4224
|
+
return await operation();
|
|
4225
|
+
} finally {
|
|
4226
|
+
release();
|
|
4227
|
+
if (attachTails.get(sessionID) === current)
|
|
4228
|
+
attachTails.delete(sessionID);
|
|
4229
|
+
}
|
|
4230
|
+
}
|
|
4231
|
+
async function acquireLock(sessionID) {
|
|
4232
|
+
const stateFile = join3(directory, fileName(sessionID));
|
|
4233
|
+
let compromised;
|
|
4234
|
+
try {
|
|
4235
|
+
await mkdir(directory, { recursive: true });
|
|
4236
|
+
const release = await lockStateFile(stateFile, {
|
|
4237
|
+
realpath: false,
|
|
4238
|
+
stale: lockStaleMilliseconds,
|
|
4239
|
+
update: lockUpdateMilliseconds,
|
|
4240
|
+
retries: { retries: 50, factor: 1, minTimeout: 10, maxTimeout: 100 },
|
|
4241
|
+
onCompromised: (error) => {
|
|
4242
|
+
compromised = error;
|
|
4243
|
+
}
|
|
4244
|
+
});
|
|
4245
|
+
return { ok: true, value: { release, compromised: () => compromised } };
|
|
4246
|
+
} catch (cause) {
|
|
4247
|
+
return {
|
|
4248
|
+
ok: false,
|
|
4249
|
+
error: stateUnavailable("write", "Unable to lock the session pull request state", cause)
|
|
4250
|
+
};
|
|
4251
|
+
}
|
|
4252
|
+
}
|
|
4253
|
+
async function withLock(sessionID, operation) {
|
|
4254
|
+
const lock = await acquireLock(sessionID);
|
|
4255
|
+
if (!lock.ok)
|
|
4256
|
+
return lock;
|
|
4257
|
+
let result;
|
|
4258
|
+
try {
|
|
4259
|
+
result = await operation();
|
|
4260
|
+
} catch (cause) {
|
|
4261
|
+
await lock.value.release().catch(() => {
|
|
4262
|
+
return;
|
|
4263
|
+
});
|
|
4264
|
+
throw cause;
|
|
4265
|
+
}
|
|
4266
|
+
try {
|
|
4267
|
+
await lock.value.release();
|
|
4268
|
+
} catch (cause) {
|
|
4269
|
+
return {
|
|
4270
|
+
ok: false,
|
|
4271
|
+
error: stateUnavailable("write", "Unable to unlock the session pull request state", cause)
|
|
4272
|
+
};
|
|
4273
|
+
}
|
|
4274
|
+
const compromise = lock.value.compromised();
|
|
4275
|
+
if (compromise !== undefined) {
|
|
4276
|
+
return {
|
|
4277
|
+
ok: false,
|
|
4278
|
+
error: stateUnavailable("write", "The session pull request state lock was compromised", compromise)
|
|
4279
|
+
};
|
|
4280
|
+
}
|
|
4281
|
+
return result;
|
|
4282
|
+
}
|
|
4283
|
+
async function readExisting(sessionID) {
|
|
4284
|
+
const path = join3(directory, fileName(sessionID));
|
|
4285
|
+
let content;
|
|
4286
|
+
try {
|
|
4287
|
+
content = await readFile2(path, "utf8");
|
|
4288
|
+
} catch (cause) {
|
|
4289
|
+
if (isMissingFile2(cause))
|
|
4290
|
+
return { ok: true, value: undefined };
|
|
4291
|
+
return {
|
|
4292
|
+
ok: false,
|
|
4293
|
+
error: stateUnavailable("read", "Unable to read the session pull request state", cause)
|
|
4294
|
+
};
|
|
4295
|
+
}
|
|
4296
|
+
let decoded;
|
|
4297
|
+
try {
|
|
4298
|
+
decoded = JSON.parse(content);
|
|
4299
|
+
} catch {
|
|
4300
|
+
return invalidStateFile;
|
|
4301
|
+
}
|
|
4302
|
+
return parseState(decoded);
|
|
4303
|
+
}
|
|
4304
|
+
async function read(sessionID) {
|
|
4305
|
+
const result = await readExisting(sessionID);
|
|
4306
|
+
if (!result.ok)
|
|
4307
|
+
return result;
|
|
4308
|
+
return { ok: true, value: result.value ?? [] };
|
|
4309
|
+
}
|
|
4310
|
+
async function write(sessionID, attachments) {
|
|
4311
|
+
const destination = join3(directory, fileName(sessionID));
|
|
4312
|
+
const temporary = `${destination}.${process.pid}.${randomUUID()}.tmp`;
|
|
4313
|
+
const state = {
|
|
4314
|
+
version: 1,
|
|
4315
|
+
pullRequests: attachments.map((attachment) => ({
|
|
4316
|
+
url: attachment.pullRequest.url,
|
|
4317
|
+
attachedAt: attachment.attachedAt
|
|
4318
|
+
}))
|
|
2549
4319
|
};
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
description: attachment.pullRequest.url
|
|
2560
|
-
}));
|
|
2561
|
-
},
|
|
2562
|
-
onSelect: (option) => finish(option.value)
|
|
4320
|
+
try {
|
|
4321
|
+
await mkdir(directory, { recursive: true });
|
|
4322
|
+
await writeFile(temporary, `${JSON.stringify(state, null, 2)}
|
|
4323
|
+
`, { mode: 384 });
|
|
4324
|
+
await rename(temporary, destination);
|
|
4325
|
+
return { ok: true, value: undefined };
|
|
4326
|
+
} catch (cause) {
|
|
4327
|
+
await rm(temporary, { force: true }).catch(() => {
|
|
4328
|
+
return;
|
|
2563
4329
|
});
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
}
|
|
2570
|
-
function
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
const colors = {
|
|
2579
|
-
green: theme.success,
|
|
2580
|
-
yellow: theme.warning,
|
|
2581
|
-
red: theme.error,
|
|
2582
|
-
purple: theme.secondary,
|
|
2583
|
-
gray: theme.textMuted
|
|
2584
|
-
};
|
|
2585
|
-
return colors[tone];
|
|
2586
|
-
}
|
|
2587
|
-
function PullRequestSidebar(props) {
|
|
2588
|
-
const [items, setItems] = createSignal([]);
|
|
2589
|
-
const [failure, setFailure] = createSignal();
|
|
2590
|
-
const polling = startSessionPolling({
|
|
2591
|
-
sessionID: props.sessionID,
|
|
2592
|
-
store: props.dependencies.store,
|
|
2593
|
-
github: props.dependencies.github,
|
|
2594
|
-
publish: (value) => {
|
|
2595
|
-
setFailure(undefined);
|
|
2596
|
-
setItems(value);
|
|
2597
|
-
},
|
|
2598
|
-
onStateFailure: (error) => setFailure(error.message),
|
|
2599
|
-
onError: () => setFailure("Unable to refresh pull request status")
|
|
2600
|
-
});
|
|
2601
|
-
polling.start().catch(() => setFailure("Unable to refresh pull request status"));
|
|
2602
|
-
const unsubscribe = props.refreshBus.subscribe(props.sessionID, () => {
|
|
2603
|
-
polling.refresh().catch(() => setFailure("Unable to refresh pull request status"));
|
|
2604
|
-
});
|
|
2605
|
-
const onAbort = () => polling.stop();
|
|
2606
|
-
props.api.lifecycle.signal.addEventListener("abort", onAbort, {
|
|
2607
|
-
once: true
|
|
2608
|
-
});
|
|
2609
|
-
onCleanup(() => {
|
|
2610
|
-
unsubscribe();
|
|
2611
|
-
polling.stop();
|
|
2612
|
-
props.api.lifecycle.signal.removeEventListener("abort", onAbort);
|
|
2613
|
-
});
|
|
2614
|
-
return (() => {
|
|
2615
|
-
var _el$2 = _$createElement("box"), _el$3 = _$createElement("text"), _el$4 = _$createElement("b");
|
|
2616
|
-
_$insertNode(_el$2, _el$3);
|
|
2617
|
-
_$setProp(_el$2, "flexDirection", "column");
|
|
2618
|
-
_$setProp(_el$2, "gap", 1);
|
|
2619
|
-
_$insertNode(_el$3, _el$4);
|
|
2620
|
-
_$insertNode(_el$4, _$createTextNode(`Pull requests`));
|
|
2621
|
-
_$insert(_el$2, (() => {
|
|
2622
|
-
var _c$ = _$memo(() => !!failure());
|
|
2623
|
-
return () => _c$() ? (() => {
|
|
2624
|
-
var _el$6 = _$createElement("text");
|
|
2625
|
-
_$insert(_el$6, failure);
|
|
2626
|
-
_$effect((_$p) => _$setProp(_el$6, "fg", props.api.theme.current.error, _$p));
|
|
2627
|
-
return _el$6;
|
|
2628
|
-
})() : null;
|
|
2629
|
-
})(), null);
|
|
2630
|
-
_$insert(_el$2, (() => {
|
|
2631
|
-
var _c$2 = _$memo(() => !!(!failure() && items().length === 0));
|
|
2632
|
-
return () => _c$2() ? (() => {
|
|
2633
|
-
var _el$7 = _$createElement("text");
|
|
2634
|
-
_$insertNode(_el$7, _$createTextNode(`No pull requests attached`));
|
|
2635
|
-
_$effect((_$p) => _$setProp(_el$7, "fg", props.api.theme.current.textMuted, _$p));
|
|
2636
|
-
return _el$7;
|
|
2637
|
-
})() : null;
|
|
2638
|
-
})(), null);
|
|
2639
|
-
_$insert(_el$2, () => items().map((item) => {
|
|
2640
|
-
const appearance = statusAppearance(item.status);
|
|
2641
|
-
const attributes = appearance.strikethrough ? TextAttributes.STRIKETHROUGH : TextAttributes.NONE;
|
|
2642
|
-
const title = item.status.tag === "Available" ? item.status.title : "Title unavailable";
|
|
2643
|
-
return (() => {
|
|
2644
|
-
var _el$9 = _$createElement("box"), _el$0 = _$createElement("text"), _el$1 = _$createElement("b"), _el$10 = _$createTextNode(` `), _el$11 = _$createElement("text");
|
|
2645
|
-
_$insertNode(_el$9, _el$0);
|
|
2646
|
-
_$insertNode(_el$9, _el$11);
|
|
2647
|
-
_$setProp(_el$9, "flexDirection", "column");
|
|
2648
|
-
_$setProp(_el$9, "onMouseUp", () => {
|
|
2649
|
-
openPullRequest(item.attachment.pullRequest, {
|
|
2650
|
-
...props.dependencies.runner ? {
|
|
2651
|
-
runner: props.dependencies.runner
|
|
2652
|
-
} : {},
|
|
2653
|
-
signal: props.api.lifecycle.signal
|
|
2654
|
-
}).then((result) => {
|
|
2655
|
-
if (!result.ok) {
|
|
2656
|
-
props.api.ui.toast({
|
|
2657
|
-
variant: "error",
|
|
2658
|
-
title: "Pull request tracker",
|
|
2659
|
-
message: result.error.message
|
|
2660
|
-
});
|
|
2661
|
-
}
|
|
2662
|
-
}).catch(() => {
|
|
2663
|
-
props.api.ui.toast({
|
|
2664
|
-
variant: "error",
|
|
2665
|
-
title: "Pull request tracker",
|
|
2666
|
-
message: "Unable to open the pull request"
|
|
2667
|
-
});
|
|
2668
|
-
});
|
|
2669
|
-
});
|
|
2670
|
-
_$insertNode(_el$0, _el$1);
|
|
2671
|
-
_$insertNode(_el$0, _el$10);
|
|
2672
|
-
_$setProp(_el$0, "attributes", attributes);
|
|
2673
|
-
_$insert(_el$1, () => formatPullRequestRef(item.attachment.pullRequest));
|
|
2674
|
-
_$insert(_el$0, () => appearance.label, null);
|
|
2675
|
-
_$setProp(_el$11, "attributes", attributes);
|
|
2676
|
-
_$insert(_el$11, title);
|
|
2677
|
-
_$effect((_p$) => {
|
|
2678
|
-
var _v$ = toneColor(props.api.theme.current, appearance.tone), _v$2 = props.api.theme.current.textMuted;
|
|
2679
|
-
_v$ !== _p$.e && (_p$.e = _$setProp(_el$0, "fg", _v$, _p$.e));
|
|
2680
|
-
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$11, "fg", _v$2, _p$.t));
|
|
2681
|
-
return _p$;
|
|
2682
|
-
}, {
|
|
2683
|
-
e: undefined,
|
|
2684
|
-
t: undefined
|
|
2685
|
-
});
|
|
2686
|
-
return _el$9;
|
|
2687
|
-
})();
|
|
2688
|
-
}), null);
|
|
2689
|
-
_$effect((_$p) => _$setProp(_el$3, "fg", props.api.theme.current.text, _$p));
|
|
2690
|
-
return _el$2;
|
|
2691
|
-
})();
|
|
2692
|
-
}
|
|
2693
|
-
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));
|
|
2698
|
-
const disposeCommands = api.keymap.registerLayer({
|
|
2699
|
-
commands: [{
|
|
2700
|
-
name: "pr.attach",
|
|
2701
|
-
title: "Attach pull request",
|
|
2702
|
-
category: "Plugin",
|
|
2703
|
-
namespace: "palette",
|
|
2704
|
-
slashName: "pr-attach",
|
|
2705
|
-
async run() {
|
|
2706
|
-
const sessionID = currentSessionID(api);
|
|
2707
|
-
if (sessionID === undefined) {
|
|
2708
|
-
api.ui.toast({
|
|
2709
|
-
variant: "warning",
|
|
2710
|
-
title: "Pull request tracker",
|
|
2711
|
-
message: "Open a session first"
|
|
2712
|
-
});
|
|
2713
|
-
return;
|
|
2714
|
-
}
|
|
2715
|
-
const pullRequest = await promptForPullRequest(api, {
|
|
2716
|
-
directory: api.state.path.directory,
|
|
2717
|
-
...dependencies.runner ? {
|
|
2718
|
-
runner: dependencies.runner
|
|
2719
|
-
} : {},
|
|
2720
|
-
signal: api.lifecycle.signal
|
|
2721
|
-
});
|
|
2722
|
-
if (pullRequest === undefined)
|
|
2723
|
-
return;
|
|
2724
|
-
const result = await dependencies.store.attach(sessionID, pullRequest);
|
|
2725
|
-
if (!result.ok) {
|
|
2726
|
-
showStateFailure(api, result.error);
|
|
2727
|
-
return;
|
|
2728
|
-
}
|
|
2729
|
-
const message = result.value === "added" ? `Attached ${formatPullRequestRef(pullRequest)}` : `${formatPullRequestRef(pullRequest)} is already attached`;
|
|
2730
|
-
api.ui.toast({
|
|
2731
|
-
variant: "success",
|
|
2732
|
-
title: "Pull request tracker",
|
|
2733
|
-
message
|
|
2734
|
-
});
|
|
2735
|
-
refreshBus.emit(sessionID);
|
|
2736
|
-
}
|
|
2737
|
-
}, {
|
|
2738
|
-
name: "pr.open",
|
|
2739
|
-
title: "Open pull request",
|
|
2740
|
-
category: "Plugin",
|
|
2741
|
-
namespace: "palette",
|
|
2742
|
-
slashName: "pr-open",
|
|
2743
|
-
async run() {
|
|
2744
|
-
const sessionID = currentSessionID(api);
|
|
2745
|
-
if (sessionID === undefined) {
|
|
2746
|
-
api.ui.toast({
|
|
2747
|
-
variant: "warning",
|
|
2748
|
-
title: "Pull request tracker",
|
|
2749
|
-
message: "Open a session first"
|
|
2750
|
-
});
|
|
2751
|
-
return;
|
|
2752
|
-
}
|
|
2753
|
-
const attachments = await dependencies.store.list(sessionID);
|
|
2754
|
-
if (!attachments.ok) {
|
|
2755
|
-
showStateFailure(api, attachments.error);
|
|
2756
|
-
return;
|
|
2757
|
-
}
|
|
2758
|
-
if (attachments.value.length === 0) {
|
|
2759
|
-
api.ui.toast({
|
|
2760
|
-
variant: "info",
|
|
2761
|
-
title: "Pull request tracker",
|
|
2762
|
-
message: "No pull requests are attached"
|
|
2763
|
-
});
|
|
2764
|
-
return;
|
|
4330
|
+
return {
|
|
4331
|
+
ok: false,
|
|
4332
|
+
error: stateUnavailable("write", "Unable to write the session pull request state", cause)
|
|
4333
|
+
};
|
|
4334
|
+
}
|
|
4335
|
+
}
|
|
4336
|
+
async function attach(sessionID, pullRequest, attachOptions = {}) {
|
|
4337
|
+
return enqueueAttach(sessionID, async () => {
|
|
4338
|
+
if (attachOptions.validate !== undefined) {
|
|
4339
|
+
const current = await read(sessionID);
|
|
4340
|
+
if (!current.ok)
|
|
4341
|
+
return current;
|
|
4342
|
+
if (current.value.some((attachment) => attachment.pullRequest.url === pullRequest.url)) {
|
|
4343
|
+
return { ok: true, value: "already_attached" };
|
|
2765
4344
|
}
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
if (!result.ok) {
|
|
2776
|
-
api.ui.toast({
|
|
2777
|
-
variant: "error",
|
|
2778
|
-
title: "Pull request tracker",
|
|
2779
|
-
message: result.error.message
|
|
2780
|
-
});
|
|
4345
|
+
if (current.value.length >= maximumPullRequestsPerSession) {
|
|
4346
|
+
return {
|
|
4347
|
+
ok: false,
|
|
4348
|
+
error: {
|
|
4349
|
+
tag: "AttachmentLimitReached",
|
|
4350
|
+
limit: maximumPullRequestsPerSession,
|
|
4351
|
+
message: "A session can track at most 20 pull requests"
|
|
4352
|
+
}
|
|
4353
|
+
};
|
|
2781
4354
|
}
|
|
4355
|
+
const validation = await attachOptions.validate();
|
|
4356
|
+
if (!validation.ok)
|
|
4357
|
+
return validation;
|
|
2782
4358
|
}
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
async run() {
|
|
2790
|
-
const sessionID = currentSessionID(api);
|
|
2791
|
-
if (sessionID === undefined) {
|
|
2792
|
-
api.ui.toast({
|
|
2793
|
-
variant: "warning",
|
|
2794
|
-
title: "Pull request tracker",
|
|
2795
|
-
message: "Open a session first"
|
|
2796
|
-
});
|
|
2797
|
-
return;
|
|
4359
|
+
return withLock(sessionID, async () => {
|
|
4360
|
+
const current = await read(sessionID);
|
|
4361
|
+
if (!current.ok)
|
|
4362
|
+
return current;
|
|
4363
|
+
if (current.value.some((attachment) => attachment.pullRequest.url === pullRequest.url)) {
|
|
4364
|
+
return { ok: true, value: "already_attached" };
|
|
2798
4365
|
}
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
4366
|
+
if (current.value.length >= maximumPullRequestsPerSession) {
|
|
4367
|
+
return {
|
|
4368
|
+
ok: false,
|
|
4369
|
+
error: {
|
|
4370
|
+
tag: "AttachmentLimitReached",
|
|
4371
|
+
limit: maximumPullRequestsPerSession,
|
|
4372
|
+
message: "A session can track at most 20 pull requests"
|
|
4373
|
+
}
|
|
4374
|
+
};
|
|
2803
4375
|
}
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
4376
|
+
const written = await write(sessionID, [...current.value, { pullRequest, attachedAt: now().toISOString() }]);
|
|
4377
|
+
if (!written.ok)
|
|
4378
|
+
return written;
|
|
4379
|
+
return { ok: true, value: "added" };
|
|
4380
|
+
});
|
|
4381
|
+
});
|
|
4382
|
+
}
|
|
4383
|
+
return {
|
|
4384
|
+
list: read,
|
|
4385
|
+
attach,
|
|
4386
|
+
async detach(sessionID, pullRequest) {
|
|
4387
|
+
return withLock(sessionID, async () => {
|
|
4388
|
+
const current = await read(sessionID);
|
|
4389
|
+
if (!current.ok)
|
|
4390
|
+
return current;
|
|
4391
|
+
const next = current.value.filter((attachment) => attachment.pullRequest.url !== pullRequest.url);
|
|
4392
|
+
if (next.length === current.value.length)
|
|
4393
|
+
return { ok: true, value: "absent" };
|
|
4394
|
+
const written = await write(sessionID, next);
|
|
4395
|
+
if (!written.ok)
|
|
4396
|
+
return written;
|
|
4397
|
+
return { ok: true, value: "removed" };
|
|
4398
|
+
});
|
|
4399
|
+
},
|
|
4400
|
+
async detachByNumber(sessionID, number) {
|
|
4401
|
+
return withLock(sessionID, async () => {
|
|
4402
|
+
const current = await read(sessionID);
|
|
4403
|
+
if (!current.ok)
|
|
4404
|
+
return current;
|
|
4405
|
+
const matches = current.value.filter((attachment) => attachment.pullRequest.number === number);
|
|
4406
|
+
if (matches.length === 0)
|
|
4407
|
+
return { ok: true, value: { tag: "absent" } };
|
|
4408
|
+
if (matches.length > 1) {
|
|
4409
|
+
return {
|
|
4410
|
+
ok: true,
|
|
4411
|
+
value: { tag: "ambiguous", pullRequests: matches.map((attachment) => attachment.pullRequest) }
|
|
4412
|
+
};
|
|
2811
4413
|
}
|
|
2812
|
-
const
|
|
2813
|
-
if (
|
|
2814
|
-
return;
|
|
2815
|
-
const
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
return;
|
|
4414
|
+
const match = matches[0];
|
|
4415
|
+
if (match === undefined)
|
|
4416
|
+
return { ok: true, value: { tag: "absent" } };
|
|
4417
|
+
const next = current.value.filter((attachment) => attachment.pullRequest.url !== match.pullRequest.url);
|
|
4418
|
+
const written = await write(sessionID, next);
|
|
4419
|
+
if (!written.ok)
|
|
4420
|
+
return written;
|
|
4421
|
+
return { ok: true, value: { tag: "removed", pullRequest: match.pullRequest } };
|
|
4422
|
+
});
|
|
4423
|
+
},
|
|
4424
|
+
async removeSession(sessionID) {
|
|
4425
|
+
return withLock(sessionID, async () => {
|
|
4426
|
+
const current = await readExisting(sessionID);
|
|
4427
|
+
if (!current.ok)
|
|
4428
|
+
return current;
|
|
4429
|
+
if (current.value === undefined)
|
|
4430
|
+
return { ok: true, value: "absent" };
|
|
4431
|
+
try {
|
|
4432
|
+
await rm(join3(directory, fileName(sessionID)), { force: true });
|
|
4433
|
+
return { ok: true, value: "removed" };
|
|
4434
|
+
} catch (cause) {
|
|
4435
|
+
return {
|
|
4436
|
+
ok: false,
|
|
4437
|
+
error: stateUnavailable("write", "Unable to remove the session pull request state", cause)
|
|
4438
|
+
};
|
|
2819
4439
|
}
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
4440
|
+
});
|
|
4441
|
+
}
|
|
4442
|
+
};
|
|
4443
|
+
}
|
|
4444
|
+
|
|
4445
|
+
// src/tui.tsx
|
|
4446
|
+
function registerTui(api, dependencies, release) {
|
|
4447
|
+
const refreshBus = dependencies.refreshBus ?? createRefreshBus();
|
|
4448
|
+
const updates = createPluginUpdateController(api, dependencies, release);
|
|
4449
|
+
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))];
|
|
4450
|
+
const disposeCommands = api.keymap.registerLayer({
|
|
4451
|
+
commands: [...createPullRequestCommands(api, dependencies, refreshBus), updates.command, createFeedbackCommand(api, dependencies, release)],
|
|
2829
4452
|
bindings: []
|
|
2830
4453
|
});
|
|
2831
|
-
api.lifecycle.onDispose(
|
|
4454
|
+
api.lifecycle.onDispose(async () => {
|
|
4455
|
+
disposeCommands();
|
|
4456
|
+
for (const disposeEvent of disposeEvents)
|
|
4457
|
+
disposeEvent();
|
|
4458
|
+
await updates.startup;
|
|
4459
|
+
});
|
|
2832
4460
|
api.slots.register({
|
|
2833
4461
|
order: 250,
|
|
2834
4462
|
slots: {
|
|
2835
4463
|
sidebar_content(_context, value) {
|
|
2836
|
-
return _$
|
|
4464
|
+
return _$createComponent4(PullRequestSidebar, {
|
|
2837
4465
|
api,
|
|
2838
4466
|
get sessionID() {
|
|
2839
4467
|
return value.session_id;
|
|
2840
4468
|
},
|
|
2841
4469
|
dependencies,
|
|
2842
|
-
refreshBus
|
|
4470
|
+
refreshBus,
|
|
4471
|
+
updates
|
|
2843
4472
|
});
|
|
2844
4473
|
}
|
|
2845
4474
|
}
|
|
@@ -2847,22 +4476,23 @@ function registerTui(api, dependencies) {
|
|
|
2847
4476
|
}
|
|
2848
4477
|
var plugin = {
|
|
2849
4478
|
id: "opencode-pr-tracker",
|
|
2850
|
-
async tui(api, options) {
|
|
4479
|
+
async tui(api, options, meta) {
|
|
2851
4480
|
if (options?.enabled === false)
|
|
2852
4481
|
return;
|
|
2853
4482
|
registerTui(api, {
|
|
2854
4483
|
store: createStateStore(),
|
|
2855
4484
|
github: createGitHubClient()
|
|
2856
|
-
});
|
|
4485
|
+
}, meta);
|
|
2857
4486
|
}
|
|
2858
4487
|
};
|
|
2859
4488
|
var tui_default = plugin;
|
|
2860
4489
|
export {
|
|
4490
|
+
updateStatusLabel,
|
|
2861
4491
|
startSessionPolling,
|
|
2862
4492
|
registerTui,
|
|
2863
4493
|
openPullRequest,
|
|
2864
4494
|
tui_default as default,
|
|
2865
|
-
attachPullRequest
|
|
4495
|
+
attachPullRequest2 as attachPullRequest
|
|
2866
4496
|
};
|
|
2867
4497
|
|
|
2868
|
-
//# debugId=
|
|
4498
|
+
//# debugId=9AE11FCB9BB4D09764756E2164756E21
|