@hcrosse/opencode-pr-tracker 0.2.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 +16 -6
- package/dist/server.js +999 -30
- package/dist/server.js.map +9 -6
- package/dist/tui.js +2400 -914
- 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,48 +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))
|
|
1586
1590
|
return invalidPullRequestUrl;
|
|
1587
1591
|
if (input.includes("\\"))
|
|
1588
1592
|
return invalidPullRequestUrl;
|
|
1589
|
-
|
|
1593
|
+
const candidate = input.slice(0, schemeLessPrefix.length).toLowerCase() === schemeLessPrefix ? `https://${input}` : input;
|
|
1594
|
+
if (!candidate.startsWith("https://"))
|
|
1590
1595
|
return invalidPullRequestUrl;
|
|
1591
|
-
const authorityEnd =
|
|
1596
|
+
const authorityEnd = candidate.indexOf("/", "https://".length);
|
|
1592
1597
|
if (authorityEnd === -1)
|
|
1593
1598
|
return invalidPullRequestUrl;
|
|
1594
|
-
if (
|
|
1599
|
+
if (candidate.slice("https://".length, authorityEnd).toLowerCase() !== "github.com") {
|
|
1595
1600
|
return invalidPullRequestUrl;
|
|
1596
1601
|
}
|
|
1597
|
-
const rawPath =
|
|
1602
|
+
const rawPath = candidate.slice(authorityEnd).split(/[?#]/, 1).join("");
|
|
1598
1603
|
for (const segment of rawPath.split("/")) {
|
|
1599
1604
|
let decoded;
|
|
1600
1605
|
try {
|
|
@@ -1607,7 +1612,7 @@ function parsePullRequestUrl(input) {
|
|
|
1607
1612
|
}
|
|
1608
1613
|
let parsed;
|
|
1609
1614
|
try {
|
|
1610
|
-
parsed = new URL(
|
|
1615
|
+
parsed = new URL(candidate);
|
|
1611
1616
|
} catch {
|
|
1612
1617
|
return invalidPullRequestUrl;
|
|
1613
1618
|
}
|
|
@@ -1645,6 +1650,13 @@ var invalidGitHubResponse = {
|
|
|
1645
1650
|
message: "GitHub returned an invalid pull request response"
|
|
1646
1651
|
}
|
|
1647
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
|
+
};
|
|
1648
1660
|
var githubBatchLimitExceeded = {
|
|
1649
1661
|
ok: false,
|
|
1650
1662
|
error: {
|
|
@@ -1653,15 +1665,13 @@ var githubBatchLimitExceeded = {
|
|
|
1653
1665
|
message: "GitHub batch cannot contain more than 20 pull requests"
|
|
1654
1666
|
}
|
|
1655
1667
|
};
|
|
1656
|
-
var checkRunPending = new Set(["QUEUED", "IN_PROGRESS", "WAITING", "PENDING"]);
|
|
1657
|
-
var checkRunPassed = new Set(["SUCCESS"]);
|
|
1658
|
-
var checkRunFailed = new Set(["FAILURE", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED", "STARTUP_FAILURE", "STALE"]);
|
|
1659
|
-
var checkRunIgnored = new Set(["NEUTRAL", "SKIPPED"]);
|
|
1660
|
-
var statusContextPending = new Set(["EXPECTED", "PENDING"]);
|
|
1661
|
-
var statusContextPassed = new Set(["SUCCESS"]);
|
|
1662
|
-
var statusContextFailed = new Set(["ERROR", "FAILURE"]);
|
|
1663
1668
|
var maximumPullRequestsPerBatch = 20;
|
|
1664
|
-
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} } } } } }`;
|
|
1665
1675
|
function isRecord(value) {
|
|
1666
1676
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1667
1677
|
}
|
|
@@ -1677,67 +1687,297 @@ function parseProcessExecutionFailed(value) {
|
|
|
1677
1687
|
cause: value.cause
|
|
1678
1688
|
};
|
|
1679
1689
|
}
|
|
1680
|
-
function
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
if (
|
|
1688
|
-
return
|
|
1689
|
-
|
|
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
|
+
};
|
|
1690
1721
|
}
|
|
1691
|
-
function
|
|
1692
|
-
if (
|
|
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
|
+
}
|
|
1777
|
+
}
|
|
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) {
|
|
1693
1784
|
return invalidGitHubResponse;
|
|
1694
1785
|
}
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
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)) {
|
|
1700
1816
|
return invalidGitHubResponse;
|
|
1701
1817
|
}
|
|
1702
|
-
const
|
|
1703
|
-
|
|
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) {
|
|
1704
1823
|
return invalidGitHubResponse;
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
if (bucket !== undefined && Number(item.count) > 0)
|
|
1708
|
-
buckets.add(bucket);
|
|
1824
|
+
}
|
|
1825
|
+
workflowRun = { event, workflowId, runNumber: Number(runNumber), runAttempt: Number(runAttempt) };
|
|
1709
1826
|
}
|
|
1710
|
-
return
|
|
1827
|
+
return {
|
|
1828
|
+
ok: true,
|
|
1829
|
+
value: { tag: "CheckRun", id, name, status, conclusion, suiteId, suiteCreatedAt, sourceIdentity, workflowRun }
|
|
1830
|
+
};
|
|
1711
1831
|
}
|
|
1712
|
-
function
|
|
1713
|
-
if (input
|
|
1714
|
-
return { ok: true, value: "none" };
|
|
1715
|
-
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") {
|
|
1716
1834
|
return invalidGitHubResponse;
|
|
1717
|
-
|
|
1718
|
-
const
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
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
|
+
}
|
|
1734
1967
|
if (buckets.has("failed"))
|
|
1735
|
-
return
|
|
1968
|
+
return "failed";
|
|
1736
1969
|
if (buckets.has("pending"))
|
|
1737
|
-
return
|
|
1970
|
+
return "pending";
|
|
1738
1971
|
if (buckets.has("passed"))
|
|
1739
|
-
return
|
|
1740
|
-
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);
|
|
1741
1981
|
}
|
|
1742
1982
|
function samePullRequest(left, right) {
|
|
1743
1983
|
return left.number === right.number && left.owner.toLowerCase() === right.owner.toLowerCase() && left.repository.toLowerCase() === right.repository.toLowerCase();
|
|
@@ -1754,7 +1994,118 @@ function parseMergeability(input) {
|
|
|
1754
1994
|
return invalidGitHubResponse;
|
|
1755
1995
|
}
|
|
1756
1996
|
}
|
|
1757
|
-
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) {
|
|
1758
2109
|
if (!isRecord(input) || input.__typename !== "PullRequest" || typeof input.title !== "string" || input.title.trim() === "") {
|
|
1759
2110
|
return invalidGitHubResponse;
|
|
1760
2111
|
}
|
|
@@ -1775,17 +2126,34 @@ function parseResponse(input, pullRequest) {
|
|
|
1775
2126
|
const responseUrl = parsePullRequestUrl(input.url);
|
|
1776
2127
|
if (!responseUrl.ok || !samePullRequest(responseUrl.value, pullRequest))
|
|
1777
2128
|
return invalidGitHubResponse;
|
|
1778
|
-
const ci = parseStatusCheckRollup(input.statusCheckRollup);
|
|
1779
|
-
if (!ci.ok)
|
|
1780
|
-
return ci;
|
|
1781
2129
|
const mergeability = parseMergeability(input.mergeable);
|
|
1782
2130
|
if (!mergeability.ok)
|
|
1783
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) {
|
|
1784
2144
|
let state;
|
|
1785
|
-
switch (
|
|
1786
|
-
case "OPEN":
|
|
1787
|
-
|
|
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 };
|
|
1788
2155
|
break;
|
|
2156
|
+
}
|
|
1789
2157
|
case "MERGED":
|
|
1790
2158
|
state = { tag: "Merged" };
|
|
1791
2159
|
break;
|
|
@@ -1793,19 +2161,33 @@ function parseResponse(input, pullRequest) {
|
|
|
1793
2161
|
state = { tag: "Closed" };
|
|
1794
2162
|
break;
|
|
1795
2163
|
default:
|
|
1796
|
-
return casesHandled(
|
|
2164
|
+
return casesHandled(metadata.state);
|
|
1797
2165
|
}
|
|
1798
2166
|
return {
|
|
1799
2167
|
ok: true,
|
|
1800
2168
|
value: {
|
|
1801
2169
|
tag: "Available",
|
|
1802
2170
|
pullRequest,
|
|
1803
|
-
title:
|
|
2171
|
+
title: metadata.title,
|
|
1804
2172
|
state,
|
|
1805
2173
|
stale: false
|
|
1806
2174
|
}
|
|
1807
2175
|
};
|
|
1808
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
|
+
}
|
|
1809
2191
|
function createBatchQuery(size) {
|
|
1810
2192
|
const variables = Array.from({ length: size }, (_, index) => `$url${index}: URI!`).join(", ");
|
|
1811
2193
|
const fields = Array.from({ length: size }, (_, index) => `pr${index}: resource(url: $url${index}) { ${pullRequestSelection} }`).join(" ");
|
|
@@ -1840,9 +2222,22 @@ function parseBatchResponse(input, pullRequests) {
|
|
|
1840
2222
|
return errorAliases;
|
|
1841
2223
|
return {
|
|
1842
2224
|
ok: true,
|
|
1843
|
-
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))
|
|
1844
2226
|
};
|
|
1845
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
|
+
}
|
|
1846
2241
|
function isCancellation(cause, signal) {
|
|
1847
2242
|
if (signal?.aborted)
|
|
1848
2243
|
return true;
|
|
@@ -1871,6 +2266,94 @@ function processFailureStdout(cause) {
|
|
|
1871
2266
|
return;
|
|
1872
2267
|
return cause.stdout;
|
|
1873
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
|
+
}
|
|
1874
2357
|
var execFileRunner = (file, args, options) => new Promise((resolve, reject) => {
|
|
1875
2358
|
execFile(file, [...args], {
|
|
1876
2359
|
encoding: "utf8",
|
|
@@ -1902,40 +2385,26 @@ function createGitHubClient(runner = execFileRunner) {
|
|
|
1902
2385
|
for (const [index, pullRequest] of pullRequests.entries()) {
|
|
1903
2386
|
args.push("-f", `url${index}=${pullRequest.url}`);
|
|
1904
2387
|
}
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
if (partialStdout !== undefined) {
|
|
1924
|
-
stdout = partialStdout;
|
|
1925
|
-
} else {
|
|
1926
|
-
return { ok: false, error: processFailure };
|
|
1927
|
-
}
|
|
1928
|
-
}
|
|
1929
|
-
let decoded;
|
|
1930
|
-
try {
|
|
1931
|
-
decoded = JSON.parse(stdout);
|
|
1932
|
-
} catch {
|
|
1933
|
-
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);
|
|
1934
2406
|
}
|
|
1935
|
-
|
|
1936
|
-
if (processFailure === undefined || parsed.ok)
|
|
1937
|
-
return parsed;
|
|
1938
|
-
return { ok: false, error: processFailure };
|
|
2407
|
+
return cancellation === undefined ? { ok: true, value: batch } : { ok: false, error: cancellation };
|
|
1939
2408
|
}
|
|
1940
2409
|
};
|
|
1941
2410
|
}
|
|
@@ -1949,6 +2418,7 @@ var diagnosticLabels = {
|
|
|
1949
2418
|
GitHubCliMissing: "install gh",
|
|
1950
2419
|
GitHubAuthenticationRequired: "run gh auth login",
|
|
1951
2420
|
GitHubUnavailable: "GitHub unavailable",
|
|
2421
|
+
PullRequestNotFound: "not found or inaccessible",
|
|
1952
2422
|
InvalidGitHubResponse: "invalid GitHub response"
|
|
1953
2423
|
};
|
|
1954
2424
|
function stateAppearance(state) {
|
|
@@ -1959,7 +2429,23 @@ function stateAppearance(state) {
|
|
|
1959
2429
|
return { tone: "red", label: "merge conflict", strikethrough: false };
|
|
1960
2430
|
case "mergeable":
|
|
1961
2431
|
case "unknown":
|
|
1962
|
-
|
|
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
|
+
}
|
|
1963
2449
|
default:
|
|
1964
2450
|
return casesHandled(state.mergeability);
|
|
1965
2451
|
}
|
|
@@ -1984,441 +2470,1095 @@ function statusAppearance(status) {
|
|
|
1984
2470
|
return status.stale ? { ...appearance, label: `${appearance.label} (stale; ${diagnosticLabels[status.diagnostic]})` } : appearance;
|
|
1985
2471
|
}
|
|
1986
2472
|
|
|
1987
|
-
// src/
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
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
|
+
};
|
|
1993
2486
|
}
|
|
1994
|
-
};
|
|
1995
|
-
var repositoryResolutionFailed = {
|
|
1996
|
-
tag: "RepositoryResolutionFailed",
|
|
1997
|
-
message: "Unable to resolve the current GitHub repository with gh; attach with a full URL instead"
|
|
1998
|
-
};
|
|
1999
|
-
var repositoryResolutionCancelled = {
|
|
2000
|
-
ok: false,
|
|
2001
|
-
error: { tag: "RepositoryResolutionCancelled" }
|
|
2002
|
-
};
|
|
2003
|
-
function isCancellation2(cause, signal) {
|
|
2004
|
-
if (signal?.aborted)
|
|
2005
|
-
return true;
|
|
2006
|
-
return cause instanceof Error && cause.name === "AbortError";
|
|
2007
|
-
}
|
|
2008
|
-
function parseRepositoryPullRequest(stdout, number) {
|
|
2009
|
-
let decoded;
|
|
2010
2487
|
try {
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
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
|
+
};
|
|
2017
2495
|
}
|
|
2018
|
-
const repositoryUrl = decoded.url.endsWith("/") ? decoded.url.slice(0, -1) : decoded.url;
|
|
2019
|
-
const pullRequest = parsePullRequestUrl(`${repositoryUrl}/pull/${number}`);
|
|
2020
|
-
return pullRequest.ok ? pullRequest : { ok: false, error: repositoryResolutionFailed };
|
|
2021
2496
|
}
|
|
2022
|
-
async function
|
|
2023
|
-
const
|
|
2024
|
-
if (
|
|
2025
|
-
return
|
|
2026
|
-
if (
|
|
2027
|
-
return
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
let stdout;
|
|
2032
|
-
try {
|
|
2033
|
-
const result = await (options.runner ?? execFileRunner)("gh", ["repo", "view", "--json", "url"], {
|
|
2034
|
-
cwd: options.directory,
|
|
2035
|
-
...options.signal ? { signal: options.signal } : {}
|
|
2036
|
-
});
|
|
2037
|
-
stdout = result.stdout;
|
|
2038
|
-
} catch (cause) {
|
|
2039
|
-
if (isCancellation2(cause, options.signal))
|
|
2040
|
-
return repositoryResolutionCancelled;
|
|
2041
|
-
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
|
+
};
|
|
2042
2506
|
}
|
|
2043
|
-
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
|
+
};
|
|
2044
2515
|
}
|
|
2045
2516
|
|
|
2046
|
-
// src/
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
import { homedir } from "os";
|
|
2051
|
-
import { join } from "path";
|
|
2052
|
-
var maximumPullRequestsPerSession = 20;
|
|
2053
|
-
var invalidStateFile = {
|
|
2054
|
-
ok: false,
|
|
2055
|
-
error: {
|
|
2056
|
-
tag: "InvalidStateFile",
|
|
2057
|
-
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;
|
|
2058
2521
|
}
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2522
|
+
return {
|
|
2523
|
+
tag: "ProcessExecutionFailed",
|
|
2524
|
+
code: value.code,
|
|
2525
|
+
stderr: value.stderr,
|
|
2526
|
+
stdout: value.stdout,
|
|
2527
|
+
cause: value.cause
|
|
2528
|
+
};
|
|
2064
2529
|
}
|
|
2065
|
-
function
|
|
2066
|
-
|
|
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";
|
|
2067
2535
|
}
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
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));
|
|
2071
2542
|
}
|
|
2072
|
-
function
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
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}`);
|
|
2094
2567
|
}
|
|
2095
|
-
return {
|
|
2568
|
+
return {
|
|
2569
|
+
ok: true,
|
|
2570
|
+
value: {
|
|
2571
|
+
...draft,
|
|
2572
|
+
body: [draft.body, "", "## Diagnostics", "", ...lines].join(`
|
|
2573
|
+
`)
|
|
2574
|
+
}
|
|
2575
|
+
};
|
|
2096
2576
|
}
|
|
2097
|
-
function
|
|
2098
|
-
|
|
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
|
+
}
|
|
2099
2645
|
}
|
|
2100
|
-
function
|
|
2101
|
-
|
|
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();
|
|
2102
2653
|
}
|
|
2103
|
-
function
|
|
2104
|
-
const
|
|
2105
|
-
|
|
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
|
+
};
|
|
2106
2682
|
}
|
|
2107
|
-
function
|
|
2108
|
-
const
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
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
|
|
2122
2710
|
}
|
|
2123
|
-
}
|
|
2124
|
-
|
|
2125
|
-
|
|
2711
|
+
};
|
|
2712
|
+
}
|
|
2713
|
+
if (failure !== undefined && isAuthenticationFailure2(failure)) {
|
|
2126
2714
|
return {
|
|
2127
2715
|
ok: false,
|
|
2128
|
-
error:
|
|
2716
|
+
error: {
|
|
2717
|
+
tag: "GitHubAuthenticationRequired",
|
|
2718
|
+
message: "GitHub CLI authentication required; run gh auth login",
|
|
2719
|
+
cause
|
|
2720
|
+
}
|
|
2129
2721
|
};
|
|
2130
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
|
+
};
|
|
2131
2731
|
}
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
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)
|
|
2141
2798
|
return;
|
|
2142
|
-
|
|
2143
|
-
|
|
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;
|
|
2144
2808
|
}
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
}
|
|
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;
|
|
2148
2925
|
return {
|
|
2149
|
-
|
|
2150
|
-
|
|
2926
|
+
kind,
|
|
2927
|
+
title,
|
|
2928
|
+
problem,
|
|
2929
|
+
reproduction,
|
|
2930
|
+
expectedBehavior
|
|
2151
2931
|
};
|
|
2152
2932
|
}
|
|
2153
|
-
|
|
2154
|
-
|
|
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;
|
|
2155
2943
|
return {
|
|
2156
|
-
|
|
2157
|
-
|
|
2944
|
+
kind,
|
|
2945
|
+
title,
|
|
2946
|
+
problem,
|
|
2947
|
+
desiredOutcome,
|
|
2948
|
+
constraints
|
|
2158
2949
|
};
|
|
2159
2950
|
}
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
let content;
|
|
2165
|
-
try {
|
|
2166
|
-
content = await readFile(path, "utf8");
|
|
2167
|
-
} catch (cause) {
|
|
2168
|
-
if (isMissingFile(cause))
|
|
2169
|
-
return { ok: true, value: undefined };
|
|
2951
|
+
case "other": {
|
|
2952
|
+
const details = await promptForValue(api, signal, "Details", true);
|
|
2953
|
+
if (details === undefined)
|
|
2954
|
+
return;
|
|
2170
2955
|
return {
|
|
2171
|
-
|
|
2172
|
-
|
|
2956
|
+
kind,
|
|
2957
|
+
title,
|
|
2958
|
+
details
|
|
2173
2959
|
};
|
|
2174
2960
|
}
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
decoded = JSON.parse(content);
|
|
2178
|
-
} catch {
|
|
2179
|
-
return invalidStateFile;
|
|
2180
|
-
}
|
|
2181
|
-
return parseState(decoded);
|
|
2182
|
-
}
|
|
2183
|
-
async function read(sessionID) {
|
|
2184
|
-
const result = await readExisting(sessionID);
|
|
2185
|
-
if (!result.ok)
|
|
2186
|
-
return result;
|
|
2187
|
-
return { ok: true, value: result.value ?? [] };
|
|
2961
|
+
default:
|
|
2962
|
+
return casesHandled(kind);
|
|
2188
2963
|
}
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
await writeFile(temporary, `${JSON.stringify(state, null, 2)}
|
|
2202
|
-
`, { mode: 384 });
|
|
2203
|
-
await rename(temporary, destination);
|
|
2204
|
-
return { ok: true, value: undefined };
|
|
2205
|
-
} catch (cause) {
|
|
2206
|
-
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)
|
|
2207
2976
|
return;
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
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"
|
|
2212
2985
|
};
|
|
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
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
if (next.length === current.value.length)
|
|
2248
|
-
return { ok: true, value: "absent" };
|
|
2249
|
-
const written = await write(sessionID, next);
|
|
2250
|
-
if (!written.ok)
|
|
2251
|
-
return written;
|
|
2252
|
-
return { ok: true, value: "removed" };
|
|
2253
|
-
});
|
|
2254
|
-
},
|
|
2255
|
-
async detachByNumber(sessionID, number) {
|
|
2256
|
-
return withLock(sessionID, async () => {
|
|
2257
|
-
const current = await read(sessionID);
|
|
2258
|
-
if (!current.ok)
|
|
2259
|
-
return current;
|
|
2260
|
-
const matches = current.value.filter((attachment) => attachment.pullRequest.number === number);
|
|
2261
|
-
if (matches.length === 0)
|
|
2262
|
-
return { ok: true, value: { tag: "absent" } };
|
|
2263
|
-
if (matches.length > 1) {
|
|
2264
|
-
return {
|
|
2265
|
-
ok: true,
|
|
2266
|
-
value: { tag: "ambiguous", pullRequests: matches.map((attachment) => attachment.pullRequest) }
|
|
2267
|
-
};
|
|
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
|
+
});
|
|
2268
3020
|
}
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
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
|
|
2277
3028
|
});
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
return { ok: true, value: "absent" };
|
|
2286
|
-
try {
|
|
2287
|
-
await rm(join(directory, fileName(sessionID)), { force: true });
|
|
2288
|
-
return { ok: true, value: "removed" };
|
|
2289
|
-
} catch (cause) {
|
|
2290
|
-
return {
|
|
2291
|
-
ok: false,
|
|
2292
|
-
error: stateUnavailable("write", "Unable to remove the session pull request state", cause)
|
|
2293
|
-
};
|
|
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
|
+
});
|
|
2294
3036
|
}
|
|
2295
|
-
})
|
|
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
|
+
}
|
|
2296
3044
|
}
|
|
2297
3045
|
};
|
|
2298
3046
|
}
|
|
2299
3047
|
|
|
2300
|
-
// src/tui.tsx
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
};
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
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);
|
|
2311
3061
|
}
|
|
2312
|
-
function
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
const
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
let queued;
|
|
2321
|
-
function project(attachments) {
|
|
2322
|
-
return attachments.map((attachment) => ({
|
|
2323
|
-
attachment,
|
|
2324
|
-
status: statuses.get(attachment.pullRequest.url) ?? {
|
|
2325
|
-
tag: "Unavailable"
|
|
2326
|
-
}
|
|
2327
|
-
}));
|
|
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 });
|
|
2328
3070
|
}
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
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))
|
|
3092
|
+
continue;
|
|
3093
|
+
if (latest === undefined || Bun.semver.order(release.version, latest) > 0)
|
|
3094
|
+
latest = release.version;
|
|
3095
|
+
} catch {
|
|
3096
|
+
continue;
|
|
2345
3097
|
}
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
return {
|
|
2361
|
-
ok: true,
|
|
2362
|
-
value: "stopped"
|
|
2363
|
-
};
|
|
2364
|
-
let batchDiagnostic;
|
|
2365
|
-
let failure;
|
|
2366
|
-
if (!batch.ok) {
|
|
2367
|
-
if (batch.error.tag === "GitHubCancelled")
|
|
2368
|
-
return batch;
|
|
2369
|
-
batchDiagnostic = batch.error.tag === "GitHubBatchLimitExceeded" ? "GitHubUnavailable" : batch.error.tag;
|
|
2370
|
-
failure = batch.error;
|
|
3098
|
+
}
|
|
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" } };
|
|
2371
3112
|
}
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
3113
|
+
return {
|
|
3114
|
+
ok: false,
|
|
3115
|
+
error: { tag: "UpdateCheckUnavailable", message: "Unable to check for plugin updates", cause }
|
|
3116
|
+
};
|
|
3117
|
+
}
|
|
3118
|
+
if (!response.ok) {
|
|
3119
|
+
return {
|
|
3120
|
+
ok: false,
|
|
3121
|
+
error: {
|
|
3122
|
+
tag: "UpdateCheckUnavailable",
|
|
3123
|
+
message: "Unable to check for plugin updates",
|
|
3124
|
+
cause: new Error(`npm registry returned HTTP ${response.status}`)
|
|
2378
3125
|
}
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
} : {
|
|
2387
|
-
tag: "Unavailable",
|
|
2388
|
-
diagnostic
|
|
2389
|
-
});
|
|
2390
|
-
}
|
|
2391
|
-
if (!stopped)
|
|
2392
|
-
input.publish(project(attachments.value));
|
|
2393
|
-
return failure === undefined ? {
|
|
2394
|
-
ok: true,
|
|
2395
|
-
value: "refreshed"
|
|
2396
|
-
} : {
|
|
3126
|
+
};
|
|
3127
|
+
}
|
|
3128
|
+
let decoded;
|
|
3129
|
+
try {
|
|
3130
|
+
decoded = await response.json();
|
|
3131
|
+
} catch {
|
|
3132
|
+
return {
|
|
2397
3133
|
ok: false,
|
|
2398
|
-
error:
|
|
3134
|
+
error: {
|
|
3135
|
+
tag: "InvalidUpdateResponse",
|
|
3136
|
+
message: "The npm registry returned invalid plugin release metadata"
|
|
3137
|
+
}
|
|
2399
3138
|
};
|
|
2400
3139
|
}
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
3140
|
+
const releases = parseRegistryReleases(decoded);
|
|
3141
|
+
if (releases === undefined) {
|
|
3142
|
+
return {
|
|
3143
|
+
ok: false,
|
|
3144
|
+
error: {
|
|
3145
|
+
tag: "InvalidUpdateResponse",
|
|
3146
|
+
message: "The npm registry returned invalid plugin release metadata"
|
|
3147
|
+
}
|
|
3148
|
+
};
|
|
3149
|
+
}
|
|
3150
|
+
return { ok: true, value: newestCompatibleUpdate(releases, versions) };
|
|
3151
|
+
}
|
|
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))
|
|
2412
3168
|
return;
|
|
2413
|
-
|
|
2414
|
-
|
|
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);
|
|
3228
|
+
}
|
|
3229
|
+
};
|
|
3230
|
+
}
|
|
3231
|
+
function writeUpdateCache(api, versions, availableVersion, now) {
|
|
3232
|
+
const cache = {
|
|
3233
|
+
checkedAt: now(),
|
|
3234
|
+
...versions,
|
|
3235
|
+
availableVersion
|
|
3236
|
+
};
|
|
3237
|
+
api.kv.set(updateCacheKey, cache);
|
|
3238
|
+
}
|
|
3239
|
+
function waitForKvReady(api) {
|
|
3240
|
+
return new Promise((resolve) => {
|
|
3241
|
+
let timer;
|
|
3242
|
+
const finish = (ready) => {
|
|
3243
|
+
if (timer !== undefined)
|
|
3244
|
+
clearTimeout(timer);
|
|
3245
|
+
api.lifecycle.signal.removeEventListener("abort", onAbort);
|
|
3246
|
+
resolve(ready);
|
|
3247
|
+
};
|
|
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
|
|
3260
|
+
});
|
|
3261
|
+
check();
|
|
3262
|
+
});
|
|
3263
|
+
}
|
|
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
|
+
});
|
|
3325
|
+
return;
|
|
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));
|
|
2415
3558
|
}
|
|
2416
3559
|
function requestRefresh() {
|
|
2417
3560
|
if (stopped)
|
|
2418
|
-
return Promise.resolve({
|
|
2419
|
-
ok: true,
|
|
2420
|
-
value: "stopped"
|
|
2421
|
-
});
|
|
3561
|
+
return Promise.resolve({ ok: true, value: "stopped" });
|
|
2422
3562
|
if (inFlight) {
|
|
2423
3563
|
if (queued !== undefined)
|
|
2424
3564
|
return queued.promise;
|
|
@@ -2428,11 +3568,7 @@ function startSessionPolling(input) {
|
|
|
2428
3568
|
resolve = resolvePromise;
|
|
2429
3569
|
reject = rejectPromise;
|
|
2430
3570
|
});
|
|
2431
|
-
queued = {
|
|
2432
|
-
promise,
|
|
2433
|
-
resolve,
|
|
2434
|
-
reject
|
|
2435
|
-
};
|
|
3571
|
+
queued = { promise, resolve, reject };
|
|
2436
3572
|
return promise;
|
|
2437
3573
|
}
|
|
2438
3574
|
const current = poll();
|
|
@@ -2469,37 +3605,18 @@ function startSessionPolling(input) {
|
|
|
2469
3605
|
}
|
|
2470
3606
|
};
|
|
2471
3607
|
}
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
}
|
|
2485
|
-
try {
|
|
2486
|
-
await (options.runner ?? execFileRunner)(executable, [pullRequest.url], options.signal ? {
|
|
2487
|
-
signal: options.signal
|
|
2488
|
-
} : {});
|
|
2489
|
-
return {
|
|
2490
|
-
ok: true,
|
|
2491
|
-
value: undefined
|
|
2492
|
-
};
|
|
2493
|
-
} catch (cause) {
|
|
2494
|
-
return {
|
|
2495
|
-
ok: false,
|
|
2496
|
-
error: {
|
|
2497
|
-
tag: "OpenPullRequestFailed",
|
|
2498
|
-
message: "Unable to open the pull request",
|
|
2499
|
-
cause
|
|
2500
|
-
}
|
|
2501
|
-
};
|
|
2502
|
-
}
|
|
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
|
+
} : {});
|
|
2503
3620
|
}
|
|
2504
3621
|
function createRefreshBus() {
|
|
2505
3622
|
const listeners = new Map;
|
|
@@ -2542,26 +3659,36 @@ function promptForPullRequest(api, options) {
|
|
|
2542
3659
|
const controller = new AbortController;
|
|
2543
3660
|
const signal = AbortSignal.any([options.signal, controller.signal]);
|
|
2544
3661
|
let finished = false;
|
|
2545
|
-
const
|
|
3662
|
+
const onAbort = () => finish(undefined);
|
|
3663
|
+
const finish = (value, clearDialog = true) => {
|
|
2546
3664
|
if (finished)
|
|
2547
3665
|
return;
|
|
2548
3666
|
finished = true;
|
|
3667
|
+
options.signal.removeEventListener("abort", onAbort);
|
|
2549
3668
|
controller.abort();
|
|
2550
|
-
|
|
3669
|
+
if (clearDialog)
|
|
3670
|
+
api.ui.dialog.clear();
|
|
2551
3671
|
resolve(value);
|
|
2552
3672
|
};
|
|
3673
|
+
if (options.signal.aborted) {
|
|
3674
|
+
finish(undefined, false);
|
|
3675
|
+
return;
|
|
3676
|
+
}
|
|
3677
|
+
options.signal.addEventListener("abort", onAbort, {
|
|
3678
|
+
once: true
|
|
3679
|
+
});
|
|
2553
3680
|
api.ui.dialog.setSize("medium");
|
|
2554
3681
|
api.ui.dialog.replace(() => {
|
|
2555
|
-
const [error, setError] =
|
|
2556
|
-
const [busy, setBusy] =
|
|
3682
|
+
const [error, setError] = createSignal2();
|
|
3683
|
+
const [busy, setBusy] = createSignal2(false);
|
|
2557
3684
|
const DialogPrompt = api.ui.DialogPrompt;
|
|
2558
|
-
return _$
|
|
3685
|
+
return _$createComponent3(DialogPrompt, {
|
|
2559
3686
|
title: "Attach pull request",
|
|
2560
|
-
placeholder: "https://github.com/owner/repository/pull/123 or 123",
|
|
3687
|
+
placeholder: "https://github.com/owner/repository/pull/123, github.com/owner/repository/pull/123, or 123",
|
|
2561
3688
|
description: () => error() ? (() => {
|
|
2562
|
-
var _el$ = _$
|
|
2563
|
-
_$
|
|
2564
|
-
_$
|
|
3689
|
+
var _el$ = _$createElement2("text");
|
|
3690
|
+
_$insert2(_el$, error);
|
|
3691
|
+
_$effect2((_$p) => _$setProp2(_el$, "fg", api.theme.current.error, _$p));
|
|
2565
3692
|
return _el$;
|
|
2566
3693
|
})() : null,
|
|
2567
3694
|
get busy() {
|
|
@@ -2569,7 +3696,7 @@ function promptForPullRequest(api, options) {
|
|
|
2569
3696
|
},
|
|
2570
3697
|
busyText: "Resolving repository",
|
|
2571
3698
|
onConfirm: (value) => {
|
|
2572
|
-
if (busy())
|
|
3699
|
+
if (finished || busy())
|
|
2573
3700
|
return;
|
|
2574
3701
|
setBusy(true);
|
|
2575
3702
|
resolvePullRequestInput(value, {
|
|
@@ -2595,395 +3722,753 @@ function promptForPullRequest(api, options) {
|
|
|
2595
3722
|
},
|
|
2596
3723
|
onCancel: () => finish(undefined)
|
|
2597
3724
|
});
|
|
2598
|
-
}, () =>
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
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
|
+
}))
|
|
4319
|
+
};
|
|
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;
|
|
4329
|
+
});
|
|
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" };
|
|
4344
|
+
}
|
|
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
|
+
};
|
|
4354
|
+
}
|
|
4355
|
+
const validation = await attachOptions.validate();
|
|
4356
|
+
if (!validation.ok)
|
|
4357
|
+
return validation;
|
|
4358
|
+
}
|
|
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" };
|
|
4365
|
+
}
|
|
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
|
+
};
|
|
4375
|
+
}
|
|
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
|
+
});
|
|
2604
4381
|
});
|
|
2605
|
-
}
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
title,
|
|
2622
|
-
get options() {
|
|
2623
|
-
return attachments.map((attachment) => ({
|
|
2624
|
-
title: formatPullRequestRef(attachment.pullRequest),
|
|
2625
|
-
value: attachment.pullRequest,
|
|
2626
|
-
description: attachment.pullRequest.url
|
|
2627
|
-
}));
|
|
2628
|
-
},
|
|
2629
|
-
onSelect: (option) => finish(option.value)
|
|
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" };
|
|
2630
4398
|
});
|
|
2631
|
-
}, () => {
|
|
2632
|
-
if (!finished)
|
|
2633
|
-
resolve(undefined);
|
|
2634
|
-
});
|
|
2635
|
-
});
|
|
2636
|
-
}
|
|
2637
|
-
function showStateFailure(api, failure) {
|
|
2638
|
-
api.ui.toast({
|
|
2639
|
-
variant: "error",
|
|
2640
|
-
title: "Pull request tracker",
|
|
2641
|
-
message: failure.message
|
|
2642
|
-
});
|
|
2643
|
-
}
|
|
2644
|
-
function toneColor(theme, tone) {
|
|
2645
|
-
const colors = {
|
|
2646
|
-
green: theme.success,
|
|
2647
|
-
yellow: theme.warning,
|
|
2648
|
-
red: theme.error,
|
|
2649
|
-
purple: theme.secondary,
|
|
2650
|
-
gray: theme.textMuted
|
|
2651
|
-
};
|
|
2652
|
-
return colors[tone];
|
|
2653
|
-
}
|
|
2654
|
-
function PullRequestSidebar(props) {
|
|
2655
|
-
const [items, setItems] = createSignal([]);
|
|
2656
|
-
const [failure, setFailure] = createSignal();
|
|
2657
|
-
const polling = startSessionPolling({
|
|
2658
|
-
sessionID: props.sessionID,
|
|
2659
|
-
store: props.dependencies.store,
|
|
2660
|
-
github: props.dependencies.github,
|
|
2661
|
-
publish: (value) => {
|
|
2662
|
-
setFailure(undefined);
|
|
2663
|
-
setItems(value);
|
|
2664
4399
|
},
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
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
|
+
};
|
|
4413
|
+
}
|
|
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
|
+
});
|
|
2672
4423
|
},
|
|
2673
|
-
async
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
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
|
+
};
|
|
4439
|
+
}
|
|
4440
|
+
});
|
|
2680
4441
|
}
|
|
2681
|
-
}
|
|
2682
|
-
const onAbort = () => polling.stop();
|
|
2683
|
-
props.api.lifecycle.signal.addEventListener("abort", onAbort, {
|
|
2684
|
-
once: true
|
|
2685
|
-
});
|
|
2686
|
-
onCleanup(() => {
|
|
2687
|
-
unsubscribe();
|
|
2688
|
-
polling.stop();
|
|
2689
|
-
props.api.lifecycle.signal.removeEventListener("abort", onAbort);
|
|
2690
|
-
});
|
|
2691
|
-
return (() => {
|
|
2692
|
-
var _el$2 = _$createElement("box"), _el$3 = _$createElement("text"), _el$4 = _$createElement("b");
|
|
2693
|
-
_$insertNode(_el$2, _el$3);
|
|
2694
|
-
_$setProp(_el$2, "flexDirection", "column");
|
|
2695
|
-
_$setProp(_el$2, "gap", 1);
|
|
2696
|
-
_$insertNode(_el$3, _el$4);
|
|
2697
|
-
_$insertNode(_el$4, _$createTextNode(`Pull requests`));
|
|
2698
|
-
_$insert(_el$2, (() => {
|
|
2699
|
-
var _c$ = _$memo(() => !!failure());
|
|
2700
|
-
return () => _c$() ? (() => {
|
|
2701
|
-
var _el$6 = _$createElement("text");
|
|
2702
|
-
_$insert(_el$6, failure);
|
|
2703
|
-
_$effect((_$p) => _$setProp(_el$6, "fg", props.api.theme.current.error, _$p));
|
|
2704
|
-
return _el$6;
|
|
2705
|
-
})() : null;
|
|
2706
|
-
})(), null);
|
|
2707
|
-
_$insert(_el$2, (() => {
|
|
2708
|
-
var _c$2 = _$memo(() => !!(!failure() && items().length === 0));
|
|
2709
|
-
return () => _c$2() ? (() => {
|
|
2710
|
-
var _el$7 = _$createElement("text");
|
|
2711
|
-
_$insertNode(_el$7, _$createTextNode(`No pull requests attached`));
|
|
2712
|
-
_$effect((_$p) => _$setProp(_el$7, "fg", props.api.theme.current.textMuted, _$p));
|
|
2713
|
-
return _el$7;
|
|
2714
|
-
})() : null;
|
|
2715
|
-
})(), null);
|
|
2716
|
-
_$insert(_el$2, () => items().map((item) => {
|
|
2717
|
-
const appearance = statusAppearance(item.status);
|
|
2718
|
-
const attributes = appearance.strikethrough ? TextAttributes.STRIKETHROUGH : TextAttributes.NONE;
|
|
2719
|
-
const title = item.status.tag === "Available" ? item.status.title : "Title unavailable";
|
|
2720
|
-
return (() => {
|
|
2721
|
-
var _el$9 = _$createElement("box"), _el$0 = _$createElement("text"), _el$1 = _$createElement("b"), _el$10 = _$createTextNode(` `), _el$11 = _$createElement("text");
|
|
2722
|
-
_$insertNode(_el$9, _el$0);
|
|
2723
|
-
_$insertNode(_el$9, _el$11);
|
|
2724
|
-
_$setProp(_el$9, "flexDirection", "column");
|
|
2725
|
-
_$setProp(_el$9, "onMouseUp", () => {
|
|
2726
|
-
openPullRequest(item.attachment.pullRequest, {
|
|
2727
|
-
...props.dependencies.runner ? {
|
|
2728
|
-
runner: props.dependencies.runner
|
|
2729
|
-
} : {},
|
|
2730
|
-
signal: props.api.lifecycle.signal
|
|
2731
|
-
}).then((result) => {
|
|
2732
|
-
if (!result.ok) {
|
|
2733
|
-
props.api.ui.toast({
|
|
2734
|
-
variant: "error",
|
|
2735
|
-
title: "Pull request tracker",
|
|
2736
|
-
message: result.error.message
|
|
2737
|
-
});
|
|
2738
|
-
}
|
|
2739
|
-
}).catch(() => {
|
|
2740
|
-
props.api.ui.toast({
|
|
2741
|
-
variant: "error",
|
|
2742
|
-
title: "Pull request tracker",
|
|
2743
|
-
message: "Unable to open the pull request"
|
|
2744
|
-
});
|
|
2745
|
-
});
|
|
2746
|
-
});
|
|
2747
|
-
_$insertNode(_el$0, _el$1);
|
|
2748
|
-
_$insertNode(_el$0, _el$10);
|
|
2749
|
-
_$setProp(_el$0, "attributes", attributes);
|
|
2750
|
-
_$insert(_el$1, () => formatPullRequestRef(item.attachment.pullRequest));
|
|
2751
|
-
_$insert(_el$0, () => appearance.label, null);
|
|
2752
|
-
_$setProp(_el$11, "attributes", attributes);
|
|
2753
|
-
_$insert(_el$11, title);
|
|
2754
|
-
_$effect((_p$) => {
|
|
2755
|
-
var _v$ = toneColor(props.api.theme.current, appearance.tone), _v$2 = props.api.theme.current.textMuted;
|
|
2756
|
-
_v$ !== _p$.e && (_p$.e = _$setProp(_el$0, "fg", _v$, _p$.e));
|
|
2757
|
-
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$11, "fg", _v$2, _p$.t));
|
|
2758
|
-
return _p$;
|
|
2759
|
-
}, {
|
|
2760
|
-
e: undefined,
|
|
2761
|
-
t: undefined
|
|
2762
|
-
});
|
|
2763
|
-
return _el$9;
|
|
2764
|
-
})();
|
|
2765
|
-
}), null);
|
|
2766
|
-
_$effect((_$p) => _$setProp(_el$3, "fg", props.api.theme.current.text, _$p));
|
|
2767
|
-
return _el$2;
|
|
2768
|
-
})();
|
|
4442
|
+
};
|
|
2769
4443
|
}
|
|
2770
|
-
|
|
4444
|
+
|
|
4445
|
+
// src/tui.tsx
|
|
4446
|
+
function registerTui(api, dependencies, release) {
|
|
2771
4447
|
const refreshBus = dependencies.refreshBus ?? createRefreshBus();
|
|
4448
|
+
const updates = createPluginUpdateController(api, dependencies, release);
|
|
2772
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))];
|
|
2773
4450
|
const disposeCommands = api.keymap.registerLayer({
|
|
2774
|
-
commands: [
|
|
2775
|
-
name: "pr.attach",
|
|
2776
|
-
title: "Attach pull request",
|
|
2777
|
-
category: "Plugin",
|
|
2778
|
-
namespace: "palette",
|
|
2779
|
-
slashName: "pr-attach",
|
|
2780
|
-
async run() {
|
|
2781
|
-
const sessionID = currentSessionID(api);
|
|
2782
|
-
if (sessionID === undefined) {
|
|
2783
|
-
api.ui.toast({
|
|
2784
|
-
variant: "warning",
|
|
2785
|
-
title: "Pull request tracker",
|
|
2786
|
-
message: "Open a session first"
|
|
2787
|
-
});
|
|
2788
|
-
return;
|
|
2789
|
-
}
|
|
2790
|
-
const pullRequest = await promptForPullRequest(api, {
|
|
2791
|
-
directory: api.state.path.directory,
|
|
2792
|
-
...dependencies.runner ? {
|
|
2793
|
-
runner: dependencies.runner
|
|
2794
|
-
} : {},
|
|
2795
|
-
signal: api.lifecycle.signal
|
|
2796
|
-
});
|
|
2797
|
-
if (pullRequest === undefined)
|
|
2798
|
-
return;
|
|
2799
|
-
const result = await dependencies.store.attach(sessionID, pullRequest);
|
|
2800
|
-
if (!result.ok) {
|
|
2801
|
-
showStateFailure(api, result.error);
|
|
2802
|
-
return;
|
|
2803
|
-
}
|
|
2804
|
-
const message = result.value === "added" ? `Attached ${formatPullRequestRef(pullRequest)}` : `${formatPullRequestRef(pullRequest)} is already attached`;
|
|
2805
|
-
api.ui.toast({
|
|
2806
|
-
variant: "success",
|
|
2807
|
-
title: "Pull request tracker",
|
|
2808
|
-
message
|
|
2809
|
-
});
|
|
2810
|
-
refreshBus.emit(sessionID);
|
|
2811
|
-
}
|
|
2812
|
-
}, {
|
|
2813
|
-
name: "pr.open",
|
|
2814
|
-
title: "Open pull request",
|
|
2815
|
-
category: "Plugin",
|
|
2816
|
-
namespace: "palette",
|
|
2817
|
-
slashName: "pr-open",
|
|
2818
|
-
async run() {
|
|
2819
|
-
const sessionID = currentSessionID(api);
|
|
2820
|
-
if (sessionID === undefined) {
|
|
2821
|
-
api.ui.toast({
|
|
2822
|
-
variant: "warning",
|
|
2823
|
-
title: "Pull request tracker",
|
|
2824
|
-
message: "Open a session first"
|
|
2825
|
-
});
|
|
2826
|
-
return;
|
|
2827
|
-
}
|
|
2828
|
-
const attachments = await dependencies.store.list(sessionID);
|
|
2829
|
-
if (!attachments.ok) {
|
|
2830
|
-
showStateFailure(api, attachments.error);
|
|
2831
|
-
return;
|
|
2832
|
-
}
|
|
2833
|
-
if (attachments.value.length === 0) {
|
|
2834
|
-
api.ui.toast({
|
|
2835
|
-
variant: "info",
|
|
2836
|
-
title: "Pull request tracker",
|
|
2837
|
-
message: "No pull requests are attached"
|
|
2838
|
-
});
|
|
2839
|
-
return;
|
|
2840
|
-
}
|
|
2841
|
-
const pullRequest = await selectPullRequest(api, "Open pull request", attachments.value);
|
|
2842
|
-
if (pullRequest === undefined)
|
|
2843
|
-
return;
|
|
2844
|
-
const result = await openPullRequest(pullRequest, {
|
|
2845
|
-
...dependencies.runner ? {
|
|
2846
|
-
runner: dependencies.runner
|
|
2847
|
-
} : {},
|
|
2848
|
-
signal: api.lifecycle.signal
|
|
2849
|
-
});
|
|
2850
|
-
if (!result.ok) {
|
|
2851
|
-
api.ui.toast({
|
|
2852
|
-
variant: "error",
|
|
2853
|
-
title: "Pull request tracker",
|
|
2854
|
-
message: result.error.message
|
|
2855
|
-
});
|
|
2856
|
-
}
|
|
2857
|
-
}
|
|
2858
|
-
}, {
|
|
2859
|
-
name: "pr.detach",
|
|
2860
|
-
title: "Detach pull request",
|
|
2861
|
-
category: "Plugin",
|
|
2862
|
-
namespace: "palette",
|
|
2863
|
-
slashName: "pr-detach",
|
|
2864
|
-
async run() {
|
|
2865
|
-
const sessionID = currentSessionID(api);
|
|
2866
|
-
if (sessionID === undefined) {
|
|
2867
|
-
api.ui.toast({
|
|
2868
|
-
variant: "warning",
|
|
2869
|
-
title: "Pull request tracker",
|
|
2870
|
-
message: "Open a session first"
|
|
2871
|
-
});
|
|
2872
|
-
return;
|
|
2873
|
-
}
|
|
2874
|
-
const attachments = await dependencies.store.list(sessionID);
|
|
2875
|
-
if (!attachments.ok) {
|
|
2876
|
-
showStateFailure(api, attachments.error);
|
|
2877
|
-
return;
|
|
2878
|
-
}
|
|
2879
|
-
if (attachments.value.length === 0) {
|
|
2880
|
-
api.ui.toast({
|
|
2881
|
-
variant: "info",
|
|
2882
|
-
title: "Pull request tracker",
|
|
2883
|
-
message: "No pull requests are attached"
|
|
2884
|
-
});
|
|
2885
|
-
return;
|
|
2886
|
-
}
|
|
2887
|
-
const pullRequest = await selectPullRequest(api, "Detach pull request", attachments.value);
|
|
2888
|
-
if (pullRequest === undefined)
|
|
2889
|
-
return;
|
|
2890
|
-
const result = await dependencies.store.detach(sessionID, pullRequest);
|
|
2891
|
-
if (!result.ok) {
|
|
2892
|
-
showStateFailure(api, result.error);
|
|
2893
|
-
return;
|
|
2894
|
-
}
|
|
2895
|
-
const message = result.value === "removed" ? `Detached ${formatPullRequestRef(pullRequest)}` : `${formatPullRequestRef(pullRequest)} was not attached`;
|
|
2896
|
-
api.ui.toast({
|
|
2897
|
-
variant: "success",
|
|
2898
|
-
title: "Pull request tracker",
|
|
2899
|
-
message
|
|
2900
|
-
});
|
|
2901
|
-
refreshBus.emit(sessionID);
|
|
2902
|
-
}
|
|
2903
|
-
}, {
|
|
2904
|
-
name: "pr.sync",
|
|
2905
|
-
title: "Sync pull request status",
|
|
2906
|
-
category: "Plugin",
|
|
2907
|
-
namespace: "palette",
|
|
2908
|
-
slashName: "pr-sync",
|
|
2909
|
-
async run() {
|
|
2910
|
-
const sessionID = currentSessionID(api);
|
|
2911
|
-
if (sessionID === undefined) {
|
|
2912
|
-
api.ui.toast({
|
|
2913
|
-
variant: "warning",
|
|
2914
|
-
title: "Pull request tracker",
|
|
2915
|
-
message: "Open a session first"
|
|
2916
|
-
});
|
|
2917
|
-
return;
|
|
2918
|
-
}
|
|
2919
|
-
try {
|
|
2920
|
-
const result = await refreshBus.forceRefresh(sessionID);
|
|
2921
|
-
if (result === undefined) {
|
|
2922
|
-
api.ui.toast({
|
|
2923
|
-
variant: "warning",
|
|
2924
|
-
title: "Pull request tracker",
|
|
2925
|
-
message: "Pull request sidebar is not available"
|
|
2926
|
-
});
|
|
2927
|
-
return;
|
|
2928
|
-
}
|
|
2929
|
-
if (!result.ok) {
|
|
2930
|
-
api.ui.toast({
|
|
2931
|
-
variant: "error",
|
|
2932
|
-
title: "Pull request tracker",
|
|
2933
|
-
message: result.error.message
|
|
2934
|
-
});
|
|
2935
|
-
return;
|
|
2936
|
-
}
|
|
2937
|
-
switch (result.value) {
|
|
2938
|
-
case "refreshed":
|
|
2939
|
-
api.ui.toast({
|
|
2940
|
-
variant: "success",
|
|
2941
|
-
title: "Pull request tracker",
|
|
2942
|
-
message: "Pull request status synced"
|
|
2943
|
-
});
|
|
2944
|
-
return;
|
|
2945
|
-
case "no_attachments":
|
|
2946
|
-
api.ui.toast({
|
|
2947
|
-
variant: "info",
|
|
2948
|
-
title: "Pull request tracker",
|
|
2949
|
-
message: "No pull requests are attached"
|
|
2950
|
-
});
|
|
2951
|
-
return;
|
|
2952
|
-
case "stopped":
|
|
2953
|
-
api.ui.toast({
|
|
2954
|
-
variant: "error",
|
|
2955
|
-
title: "Pull request tracker",
|
|
2956
|
-
message: "Unable to refresh pull request status"
|
|
2957
|
-
});
|
|
2958
|
-
return;
|
|
2959
|
-
}
|
|
2960
|
-
} catch {
|
|
2961
|
-
api.ui.toast({
|
|
2962
|
-
variant: "error",
|
|
2963
|
-
title: "Pull request tracker",
|
|
2964
|
-
message: "Unable to refresh pull request status"
|
|
2965
|
-
});
|
|
2966
|
-
}
|
|
2967
|
-
}
|
|
2968
|
-
}],
|
|
4451
|
+
commands: [...createPullRequestCommands(api, dependencies, refreshBus), updates.command, createFeedbackCommand(api, dependencies, release)],
|
|
2969
4452
|
bindings: []
|
|
2970
4453
|
});
|
|
2971
|
-
api.lifecycle.onDispose(() => {
|
|
4454
|
+
api.lifecycle.onDispose(async () => {
|
|
2972
4455
|
disposeCommands();
|
|
2973
4456
|
for (const disposeEvent of disposeEvents)
|
|
2974
4457
|
disposeEvent();
|
|
4458
|
+
await updates.startup;
|
|
2975
4459
|
});
|
|
2976
4460
|
api.slots.register({
|
|
2977
4461
|
order: 250,
|
|
2978
4462
|
slots: {
|
|
2979
4463
|
sidebar_content(_context, value) {
|
|
2980
|
-
return _$
|
|
4464
|
+
return _$createComponent4(PullRequestSidebar, {
|
|
2981
4465
|
api,
|
|
2982
4466
|
get sessionID() {
|
|
2983
4467
|
return value.session_id;
|
|
2984
4468
|
},
|
|
2985
4469
|
dependencies,
|
|
2986
|
-
refreshBus
|
|
4470
|
+
refreshBus,
|
|
4471
|
+
updates
|
|
2987
4472
|
});
|
|
2988
4473
|
}
|
|
2989
4474
|
}
|
|
@@ -2991,22 +4476,23 @@ function registerTui(api, dependencies) {
|
|
|
2991
4476
|
}
|
|
2992
4477
|
var plugin = {
|
|
2993
4478
|
id: "opencode-pr-tracker",
|
|
2994
|
-
async tui(api, options) {
|
|
4479
|
+
async tui(api, options, meta) {
|
|
2995
4480
|
if (options?.enabled === false)
|
|
2996
4481
|
return;
|
|
2997
4482
|
registerTui(api, {
|
|
2998
4483
|
store: createStateStore(),
|
|
2999
4484
|
github: createGitHubClient()
|
|
3000
|
-
});
|
|
4485
|
+
}, meta);
|
|
3001
4486
|
}
|
|
3002
4487
|
};
|
|
3003
4488
|
var tui_default = plugin;
|
|
3004
4489
|
export {
|
|
4490
|
+
updateStatusLabel,
|
|
3005
4491
|
startSessionPolling,
|
|
3006
4492
|
registerTui,
|
|
3007
4493
|
openPullRequest,
|
|
3008
4494
|
tui_default as default,
|
|
3009
|
-
attachPullRequest
|
|
4495
|
+
attachPullRequest2 as attachPullRequest
|
|
3010
4496
|
};
|
|
3011
4497
|
|
|
3012
|
-
//# debugId=
|
|
4498
|
+
//# debugId=9AE11FCB9BB4D09764756E2164756E21
|