@reviewflow/sdk 6.25.0 → 6.27.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/build/git-node.mjs +722 -7
- package/build/index-browser.es.js +205 -18
- package/build/index-node.mjs +205 -18
- package/package.json +1 -1
- package/src/checkout/overview.ts +109 -0
- package/src/checkout/sync.ts +85 -0
- package/src/checkout/worktreeRemoval.ts +136 -0
- package/src/config/defaultRepoPaths.ts +2 -0
- package/src/config/insertCheckoutEntry.ts +9 -15
- package/src/config/parseConfigText.ts +23 -0
- package/src/config/repoPathsSetting.ts +44 -0
- package/src/diff/types.ts +112 -0
- package/src/git/checkoutBranchDiff.ts +191 -0
- package/src/git/checkoutBranches.ts +187 -0
- package/src/git/checkoutCommits.ts +223 -0
- package/src/git/checkoutSync.ts +179 -0
- package/src/git/readBranchDiff.ts +9 -10
- package/src/git/run.ts +55 -1
- package/src/git/worktreeRemoval.ts +316 -0
- package/src/git.ts +52 -1
- package/src/github/openPullRequests.ts +154 -0
- package/src/index.ts +9 -1
- package/src/config/defaultRepoPath.ts +0 -2
|
@@ -1,3 +1,77 @@
|
|
|
1
|
+
const sameRepo = (left, right) => left !== null && left.toLowerCase() === right.toLowerCase();
|
|
2
|
+
const pullRequestOfBranch = (pullRequests, ownerRepo, branch) => pullRequests.find(
|
|
3
|
+
(pullRequest) => pullRequest.headRefName === branch && sameRepo(pullRequest.headOwnerRepo, ownerRepo)
|
|
4
|
+
) ?? null;
|
|
5
|
+
const pullRequestsWithoutBranch = (pullRequests, ownerRepo, branches) => {
|
|
6
|
+
const names = new Set(branches.map(({ name }) => name));
|
|
7
|
+
return pullRequests.filter(
|
|
8
|
+
(pullRequest) => !names.has(pullRequest.headRefName) || !sameRepo(pullRequest.headOwnerRepo, ownerRepo)
|
|
9
|
+
);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const upstreamGone = (upstream) => `${upstream} no longer exists on the remote`;
|
|
13
|
+
const pullRefusal = (status) => {
|
|
14
|
+
if (status.branch === null) return "a detached HEAD has nothing to pull into";
|
|
15
|
+
if (status.upstream === null) {
|
|
16
|
+
return `${status.branch} tracks no remote branch`;
|
|
17
|
+
}
|
|
18
|
+
if (status.behind === null) return upstreamGone(status.upstream);
|
|
19
|
+
if (status.behind === 0) {
|
|
20
|
+
return `${status.branch} is not behind ${status.upstream}`;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
};
|
|
24
|
+
const pushRefusal = (status, hasOrigin) => {
|
|
25
|
+
if (status.branch === null) return "a detached HEAD has no branch to push";
|
|
26
|
+
if (status.upstream === null) {
|
|
27
|
+
return hasOrigin ? null : "this repository has no origin remote";
|
|
28
|
+
}
|
|
29
|
+
if (status.ahead === null) return upstreamGone(status.upstream);
|
|
30
|
+
if (status.ahead === 0) {
|
|
31
|
+
return `${status.branch} has nothing to push to ${status.upstream}`;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const fileCount = (count, kind) => count === 0 ? null : `${String(count)} ${kind} file${count === 1 ? "" : "s"}`;
|
|
37
|
+
const mainWorktreeRefusal = "This is the main checkout, which holds the repository: it is never removed.";
|
|
38
|
+
const dirtyRefusal = ({
|
|
39
|
+
modified,
|
|
40
|
+
untracked
|
|
41
|
+
}) => {
|
|
42
|
+
const parts = [
|
|
43
|
+
fileCount(modified, "modified"),
|
|
44
|
+
fileCount(untracked, "untracked")
|
|
45
|
+
].filter((part) => part !== null);
|
|
46
|
+
if (parts.length === 0) return null;
|
|
47
|
+
return `${parts.join(" and ")}: git would refuse to remove this worktree. Commit or discard them first.`;
|
|
48
|
+
};
|
|
49
|
+
const unpushedRefusal = (count) => {
|
|
50
|
+
if (count === 0) return null;
|
|
51
|
+
const subject = count === 1 ? "1 commit exists" : `${String(count)} commits exist`;
|
|
52
|
+
return `${subject} only in this clone, on no remote branch and in no pull request. Push them first.`;
|
|
53
|
+
};
|
|
54
|
+
const lockRefusal = (lock) => {
|
|
55
|
+
if (lock === null) return null;
|
|
56
|
+
const reason = lock === "" ? "" : ` (${lock})`;
|
|
57
|
+
return `git holds this worktree locked${reason}, usually by a Claude Code process still running in it. Exit it first.`;
|
|
58
|
+
};
|
|
59
|
+
const processesRefusal = (count) => {
|
|
60
|
+
if (count === null || count === 0) return null;
|
|
61
|
+
const subject = count === 1 ? "1 process runs" : `${String(count)} processes run`;
|
|
62
|
+
return `${subject} inside this worktree (a shell, an editor or an agent): removing it would pull the directory out from under them. Exit them first.`;
|
|
63
|
+
};
|
|
64
|
+
const worktreeRemovalRefusals = (status) => {
|
|
65
|
+
if (status.main) return [mainWorktreeRefusal];
|
|
66
|
+
return [
|
|
67
|
+
dirtyRefusal(status),
|
|
68
|
+
unpushedRefusal(status.unpushedCommits),
|
|
69
|
+
lockRefusal(status.lock),
|
|
70
|
+
processesRefusal(status.processesInside)
|
|
71
|
+
].filter((refusal) => refusal !== null);
|
|
72
|
+
};
|
|
73
|
+
const worktreeRemovalRefusal = (status) => worktreeRemovalRefusals(status)[0] ?? null;
|
|
74
|
+
|
|
1
75
|
const cEscapes = {
|
|
2
76
|
a: 7,
|
|
3
77
|
b: 8,
|
|
@@ -196,6 +270,7 @@ const diffFontOptions = [
|
|
|
196
270
|
];
|
|
197
271
|
|
|
198
272
|
const isDiffWhitespace = (value) => value === "hide" || value === "show";
|
|
273
|
+
const isBranchDiffScope = (value) => value === "committed" || value === "uncommitted";
|
|
199
274
|
|
|
200
275
|
const fenceOpen = /^( {0,3})(`{3,}(?!`)|~{3,}(?!~))(.*)$/;
|
|
201
276
|
const withoutIndent = (line, indent) => {
|
|
@@ -669,7 +744,7 @@ const pullRequestReactionContents = [
|
|
|
669
744
|
];
|
|
670
745
|
const isPullRequestReactionContent = (value) => pullRequestReactionContents.includes(value);
|
|
671
746
|
|
|
672
|
-
const isStringArray = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
747
|
+
const isStringArray$1 = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
673
748
|
const isNonEmptyString = (value) => typeof value === "string" && value.trim() !== "";
|
|
674
749
|
const isPositiveInteger = (value) => typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
675
750
|
const isSha = (value) => typeof value === "string" && /^[\da-f]{40}$/i.test(value);
|
|
@@ -794,13 +869,13 @@ const parsePullRequestEdit = (value) => {
|
|
|
794
869
|
return { kind: "title", title: edit.title.trim() };
|
|
795
870
|
}
|
|
796
871
|
case "labels": {
|
|
797
|
-
if (!isStringArray(edit.add) || !isStringArray(edit.remove)) {
|
|
872
|
+
if (!isStringArray$1(edit.add) || !isStringArray$1(edit.remove)) {
|
|
798
873
|
return "labels take add and remove, arrays of names";
|
|
799
874
|
}
|
|
800
875
|
return { kind: "labels", add: edit.add, remove: edit.remove };
|
|
801
876
|
}
|
|
802
877
|
case "reviewers": {
|
|
803
|
-
if (!isStringArray(edit.add) || !isStringArray(edit.remove) || !isStringArray(edit.addTeams) || !isStringArray(edit.removeTeams)) {
|
|
878
|
+
if (!isStringArray$1(edit.add) || !isStringArray$1(edit.remove) || !isStringArray$1(edit.addTeams) || !isStringArray$1(edit.removeTeams)) {
|
|
804
879
|
return "reviewers take add, remove, addTeams and removeTeams, arrays of logins and team slugs";
|
|
805
880
|
}
|
|
806
881
|
return {
|
|
@@ -816,7 +891,7 @@ const parsePullRequestEdit = (value) => {
|
|
|
816
891
|
return { kind: "draft", draft: edit.draft };
|
|
817
892
|
}
|
|
818
893
|
case "assignees": {
|
|
819
|
-
if (!isStringArray(edit.add) || !isStringArray(edit.remove)) {
|
|
894
|
+
if (!isStringArray$1(edit.add) || !isStringArray$1(edit.remove)) {
|
|
820
895
|
return "assignees take add and remove, arrays of logins";
|
|
821
896
|
}
|
|
822
897
|
return { kind: "assignees", add: edit.add, remove: edit.remove };
|
|
@@ -1376,6 +1451,91 @@ const diffLines = ({
|
|
|
1376
1451
|
];
|
|
1377
1452
|
};
|
|
1378
1453
|
|
|
1454
|
+
const openPullRequestsLimit = 100;
|
|
1455
|
+
const openPullRequestsQuery = `query OpenPullRequests($owner: String!, $name: String!, $first: Int!) {
|
|
1456
|
+
repository(owner: $owner, name: $name) {
|
|
1457
|
+
pullRequests(states: OPEN, first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) {
|
|
1458
|
+
totalCount
|
|
1459
|
+
nodes {
|
|
1460
|
+
number
|
|
1461
|
+
title
|
|
1462
|
+
isDraft
|
|
1463
|
+
updatedAt
|
|
1464
|
+
headRefName
|
|
1465
|
+
headRepository { nameWithOwner }
|
|
1466
|
+
author { login }
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
}`;
|
|
1471
|
+
const parseOpenPullRequests = ({
|
|
1472
|
+
data,
|
|
1473
|
+
errors
|
|
1474
|
+
}) => {
|
|
1475
|
+
const connection = data?.repository?.pullRequests ?? null;
|
|
1476
|
+
if (connection === null) {
|
|
1477
|
+
throw new Error(errors.length > 0 ? errors.join("; ") : "not found");
|
|
1478
|
+
}
|
|
1479
|
+
const pullRequests = (connection.nodes ?? []).filter((node) => node !== null).map((node) => ({
|
|
1480
|
+
number: node.number,
|
|
1481
|
+
title: node.title,
|
|
1482
|
+
isDraft: node.isDraft,
|
|
1483
|
+
headRefName: node.headRefName,
|
|
1484
|
+
headOwnerRepo: node.headRepository?.nameWithOwner ?? null,
|
|
1485
|
+
author: node.author?.login ?? null,
|
|
1486
|
+
updatedAt: node.updatedAt
|
|
1487
|
+
}));
|
|
1488
|
+
return { pullRequests, totalCount: connection.totalCount };
|
|
1489
|
+
};
|
|
1490
|
+
const fetchOpenPullRequests = async (transport, ownerRepo) => {
|
|
1491
|
+
const split = splitOwnerRepo(ownerRepo);
|
|
1492
|
+
if (split === null) throw new Error(`${ownerRepo} is not owner/repo`);
|
|
1493
|
+
return parseOpenPullRequests(
|
|
1494
|
+
await transport.graphql(openPullRequestsQuery, {
|
|
1495
|
+
owner: split.owner,
|
|
1496
|
+
name: split.name,
|
|
1497
|
+
first: openPullRequestsLimit
|
|
1498
|
+
})
|
|
1499
|
+
);
|
|
1500
|
+
};
|
|
1501
|
+
const branchPullRequestsQuery = `query BranchPullRequests($owner: String!, $name: String!, $branch: String!) {
|
|
1502
|
+
repository(owner: $owner, name: $name) {
|
|
1503
|
+
pullRequests(headRefName: $branch, first: 20, orderBy: { field: UPDATED_AT, direction: DESC }) {
|
|
1504
|
+
nodes {
|
|
1505
|
+
number
|
|
1506
|
+
state
|
|
1507
|
+
headRefOid
|
|
1508
|
+
headRepository { nameWithOwner }
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}`;
|
|
1513
|
+
const parseBranchPullRequestHeads = ({ data, errors }, ownerRepo) => {
|
|
1514
|
+
const connection = data?.repository?.pullRequests ?? null;
|
|
1515
|
+
if (connection === null) {
|
|
1516
|
+
throw new Error(errors.length > 0 ? errors.join("; ") : "not found");
|
|
1517
|
+
}
|
|
1518
|
+
return (connection.nodes ?? []).filter((node) => node !== null).filter(
|
|
1519
|
+
(node) => node.headRepository?.nameWithOwner.toLowerCase() === ownerRepo.toLowerCase()
|
|
1520
|
+
).map(({ number, state, headRefOid }) => ({
|
|
1521
|
+
number,
|
|
1522
|
+
state,
|
|
1523
|
+
sha: headRefOid
|
|
1524
|
+
}));
|
|
1525
|
+
};
|
|
1526
|
+
const fetchBranchPullRequestHeads = async (transport, ownerRepo, branch) => {
|
|
1527
|
+
const split = splitOwnerRepo(ownerRepo);
|
|
1528
|
+
if (split === null) throw new Error(`${ownerRepo} is not owner/repo`);
|
|
1529
|
+
return parseBranchPullRequestHeads(
|
|
1530
|
+
await transport.graphql(branchPullRequestsQuery, {
|
|
1531
|
+
owner: split.owner,
|
|
1532
|
+
name: split.name,
|
|
1533
|
+
branch
|
|
1534
|
+
}),
|
|
1535
|
+
ownerRepo
|
|
1536
|
+
);
|
|
1537
|
+
};
|
|
1538
|
+
|
|
1379
1539
|
const imgTag = /<img\b[^>]*>/gi;
|
|
1380
1540
|
const decodeAttribute = (value) => value.replaceAll(""", '"').replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">").replaceAll("&", "&");
|
|
1381
1541
|
const attribute = (tag, name) => {
|
|
@@ -3488,21 +3648,49 @@ const createReviewflowFollower = ({
|
|
|
3488
3648
|
};
|
|
3489
3649
|
};
|
|
3490
3650
|
|
|
3491
|
-
const
|
|
3651
|
+
const defaultRepoPaths = ["~/dev/"];
|
|
3492
3652
|
|
|
3493
3653
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3494
|
-
const
|
|
3654
|
+
const parseConfigText = (source) => {
|
|
3655
|
+
if (source.trim() === "") return {};
|
|
3495
3656
|
let parsed;
|
|
3496
|
-
|
|
3497
|
-
parsed =
|
|
3498
|
-
}
|
|
3499
|
-
|
|
3500
|
-
parsed = JSON.parse(source);
|
|
3501
|
-
} catch {
|
|
3502
|
-
return null;
|
|
3503
|
-
}
|
|
3657
|
+
try {
|
|
3658
|
+
parsed = JSON.parse(source);
|
|
3659
|
+
} catch {
|
|
3660
|
+
return null;
|
|
3504
3661
|
}
|
|
3505
|
-
|
|
3662
|
+
return isRecord(parsed) ? parsed : null;
|
|
3663
|
+
};
|
|
3664
|
+
const stringifyConfig = (config) => `${JSON.stringify(config, null, 2)}
|
|
3665
|
+
`;
|
|
3666
|
+
|
|
3667
|
+
const repoPathsKeys = /* @__PURE__ */ new Set(["repoPaths", "defaultRepoPath"]);
|
|
3668
|
+
const isStringArray = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
3669
|
+
const readRepoPathsSetting = (source) => {
|
|
3670
|
+
const config = parseConfigText(source);
|
|
3671
|
+
if (config === null) return void 0;
|
|
3672
|
+
if (isStringArray(config.repoPaths)) return config.repoPaths;
|
|
3673
|
+
if (typeof config.defaultRepoPath === "string") {
|
|
3674
|
+
return [config.defaultRepoPath];
|
|
3675
|
+
}
|
|
3676
|
+
return void 0;
|
|
3677
|
+
};
|
|
3678
|
+
const writeRepoPathsSetting = (source, paths) => {
|
|
3679
|
+
const config = parseConfigText(source);
|
|
3680
|
+
if (config === null) return null;
|
|
3681
|
+
const repoPaths = [
|
|
3682
|
+
...new Set(paths.map((path) => path.trim()).filter((path) => path !== ""))
|
|
3683
|
+
];
|
|
3684
|
+
const entries = Object.entries(config);
|
|
3685
|
+
const at = entries.findIndex(([key]) => repoPathsKeys.has(key));
|
|
3686
|
+
const others = entries.filter(([key]) => !repoPathsKeys.has(key));
|
|
3687
|
+
others.splice(Math.max(at, 0), 0, ["repoPaths", repoPaths]);
|
|
3688
|
+
return stringifyConfig(Object.fromEntries(others));
|
|
3689
|
+
};
|
|
3690
|
+
|
|
3691
|
+
const insertCheckoutEntry = (source, ownerRepo) => {
|
|
3692
|
+
const parsed = parseConfigText(source);
|
|
3693
|
+
if (parsed === null) return null;
|
|
3506
3694
|
const repos = parsed.repos ?? {};
|
|
3507
3695
|
if (!isRecord(repos)) return null;
|
|
3508
3696
|
const wanted = ownerRepo.toLowerCase();
|
|
@@ -3511,8 +3699,7 @@ const insertCheckoutEntry = (source, ownerRepo) => {
|
|
|
3511
3699
|
);
|
|
3512
3700
|
const key = existing ?? ownerRepo;
|
|
3513
3701
|
const next = existing === void 0 ? { ...repos, [key]: "" } : repos;
|
|
3514
|
-
const text =
|
|
3515
|
-
`;
|
|
3702
|
+
const text = stringifyConfig({ ...parsed, repos: next });
|
|
3516
3703
|
const marker = `${JSON.stringify(key)}: "`;
|
|
3517
3704
|
const reposAt = text.indexOf('\n "repos": ');
|
|
3518
3705
|
const at = text.indexOf(marker, reposAt);
|
|
@@ -3521,5 +3708,5 @@ const insertCheckoutEntry = (source, ownerRepo) => {
|
|
|
3521
3708
|
return { source: text, cursor: value };
|
|
3522
3709
|
};
|
|
3523
3710
|
|
|
3524
|
-
export { accountOfOwnerRepo, applyPullRequestEdit, applyStreamEvent, applySuggestionsToText, assembleBoard, boardReadDelayMs, buildPrQuery, changedPullRequestParts, checkOutcomes, checksFromReviewflow, coAuthorTrailers, commitSuggestions, createGitHubTransport, createReviewflowClient, createReviewflowFollower, defaultDiffFont, defaultDiffTheme,
|
|
3711
|
+
export { accountOfOwnerRepo, applyPullRequestEdit, applyStreamEvent, applySuggestionsToText, assembleBoard, boardReadDelayMs, buildPrQuery, changedPullRequestParts, checkOutcomes, checksFromReviewflow, coAuthorTrailers, commitSuggestions, createGitHubTransport, createReviewflowClient, createReviewflowFollower, defaultDiffFont, defaultDiffTheme, defaultRepoPaths, diffFontOptions, diffLines, diffPaths, diffThemeNames, diffThemeOptions, diffThemes, dirtyRefusal, editPullRequest, fail, fetchBranchPullRequestHeads, fetchCompareDiff, fetchOpenPullRequests, fetchPullRequestCandidates, fetchPullRequestHead, fetchPullRequestPart, fetchPullRequestRevisions, formatPrChanges, formatRelativeDate, insertCheckoutEntry, interpretGraphql, isAttachablePath, isBranchDiffScope, isDiffFont, isDiffTheme, isDiffWhitespace, isPullRequestOnBoard, isPullRequestPart, isPullRequestReactionContent, isReviewflowFailure, lingerMs, lockRefusal, mainWorktreeRefusal, maxExtraPagesPerBucket, maxFailedCheckNames, maxLookupTargets, maxRetryMs, parseBranchPullRequestHeads, parseOpenPullRequests, parsePrQueryResponse, parsePullRequestCandidates, parsePullRequestChecks, parsePullRequestCommits, parsePullRequestDetail, parsePullRequestEdit, parsePullRequestFiles, parsePullRequestRevisions, parsePullRequestTimeline, parseSuggestions, partFingerprint, planSuggestions, pluralOf, prFields, prHasConflicts, prNumberOf, prStatus, prStatusLabels, prStatuses, prUrl, processesRefusal, pullRefusal, pullRequestCandidatesQuery, pullRequestChecksQuery, pullRequestCommitsQuery, pullRequestDetailQuery, pullRequestEditMovesBoard, pullRequestFilesQuery, pullRequestHeadQuery, pullRequestOfBranch, pullRequestParts, pullRequestPartsStaleAfter, pullRequestReactionContents, pullRequestRevisionsQuery, pullRequestTimelineQuery, pullRequestsWithoutBranch, pushRefusal, readAccountBoards, readPrDetail, readRepoPathsSetting, readReviewflowStream, reconnectDelayMs, responseFailure, restErrorMessage, reviewflowErrorCode, reviewflowPrChangesRequested, reviewflowPrFailure, reviewflowPrFlowDate, reviewflowPrOwners, reviewflowPrProgress, reviewflowPrStatus, reviewflowReviewDecisions, splitOwnerRepo, streamClosedWithinDays, streamIdleTimeoutMs, subjectsOf, suggestionCommitRefusal, suggestionOriginalLines, suggestionRangesOverlap, toBoardPr, toFetchedPr, unpushedRefusal, withAccountBoard, withFileViewed, worktreeRemovalRefusal, worktreeRemovalRefusals, writeRepoPathsSetting };
|
|
3525
3712
|
//# sourceMappingURL=index-browser.es.js.map
|
package/build/index-node.mjs
CHANGED
|
@@ -1,3 +1,77 @@
|
|
|
1
|
+
const sameRepo = (left, right) => left !== null && left.toLowerCase() === right.toLowerCase();
|
|
2
|
+
const pullRequestOfBranch = (pullRequests, ownerRepo, branch) => pullRequests.find(
|
|
3
|
+
(pullRequest) => pullRequest.headRefName === branch && sameRepo(pullRequest.headOwnerRepo, ownerRepo)
|
|
4
|
+
) ?? null;
|
|
5
|
+
const pullRequestsWithoutBranch = (pullRequests, ownerRepo, branches) => {
|
|
6
|
+
const names = new Set(branches.map(({ name }) => name));
|
|
7
|
+
return pullRequests.filter(
|
|
8
|
+
(pullRequest) => !names.has(pullRequest.headRefName) || !sameRepo(pullRequest.headOwnerRepo, ownerRepo)
|
|
9
|
+
);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const upstreamGone = (upstream) => `${upstream} no longer exists on the remote`;
|
|
13
|
+
const pullRefusal = (status) => {
|
|
14
|
+
if (status.branch === null) return "a detached HEAD has nothing to pull into";
|
|
15
|
+
if (status.upstream === null) {
|
|
16
|
+
return `${status.branch} tracks no remote branch`;
|
|
17
|
+
}
|
|
18
|
+
if (status.behind === null) return upstreamGone(status.upstream);
|
|
19
|
+
if (status.behind === 0) {
|
|
20
|
+
return `${status.branch} is not behind ${status.upstream}`;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
};
|
|
24
|
+
const pushRefusal = (status, hasOrigin) => {
|
|
25
|
+
if (status.branch === null) return "a detached HEAD has no branch to push";
|
|
26
|
+
if (status.upstream === null) {
|
|
27
|
+
return hasOrigin ? null : "this repository has no origin remote";
|
|
28
|
+
}
|
|
29
|
+
if (status.ahead === null) return upstreamGone(status.upstream);
|
|
30
|
+
if (status.ahead === 0) {
|
|
31
|
+
return `${status.branch} has nothing to push to ${status.upstream}`;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const fileCount = (count, kind) => count === 0 ? null : `${String(count)} ${kind} file${count === 1 ? "" : "s"}`;
|
|
37
|
+
const mainWorktreeRefusal = "This is the main checkout, which holds the repository: it is never removed.";
|
|
38
|
+
const dirtyRefusal = ({
|
|
39
|
+
modified,
|
|
40
|
+
untracked
|
|
41
|
+
}) => {
|
|
42
|
+
const parts = [
|
|
43
|
+
fileCount(modified, "modified"),
|
|
44
|
+
fileCount(untracked, "untracked")
|
|
45
|
+
].filter((part) => part !== null);
|
|
46
|
+
if (parts.length === 0) return null;
|
|
47
|
+
return `${parts.join(" and ")}: git would refuse to remove this worktree. Commit or discard them first.`;
|
|
48
|
+
};
|
|
49
|
+
const unpushedRefusal = (count) => {
|
|
50
|
+
if (count === 0) return null;
|
|
51
|
+
const subject = count === 1 ? "1 commit exists" : `${String(count)} commits exist`;
|
|
52
|
+
return `${subject} only in this clone, on no remote branch and in no pull request. Push them first.`;
|
|
53
|
+
};
|
|
54
|
+
const lockRefusal = (lock) => {
|
|
55
|
+
if (lock === null) return null;
|
|
56
|
+
const reason = lock === "" ? "" : ` (${lock})`;
|
|
57
|
+
return `git holds this worktree locked${reason}, usually by a Claude Code process still running in it. Exit it first.`;
|
|
58
|
+
};
|
|
59
|
+
const processesRefusal = (count) => {
|
|
60
|
+
if (count === null || count === 0) return null;
|
|
61
|
+
const subject = count === 1 ? "1 process runs" : `${String(count)} processes run`;
|
|
62
|
+
return `${subject} inside this worktree (a shell, an editor or an agent): removing it would pull the directory out from under them. Exit them first.`;
|
|
63
|
+
};
|
|
64
|
+
const worktreeRemovalRefusals = (status) => {
|
|
65
|
+
if (status.main) return [mainWorktreeRefusal];
|
|
66
|
+
return [
|
|
67
|
+
dirtyRefusal(status),
|
|
68
|
+
unpushedRefusal(status.unpushedCommits),
|
|
69
|
+
lockRefusal(status.lock),
|
|
70
|
+
processesRefusal(status.processesInside)
|
|
71
|
+
].filter((refusal) => refusal !== null);
|
|
72
|
+
};
|
|
73
|
+
const worktreeRemovalRefusal = (status) => worktreeRemovalRefusals(status)[0] ?? null;
|
|
74
|
+
|
|
1
75
|
const cEscapes = {
|
|
2
76
|
a: 7,
|
|
3
77
|
b: 8,
|
|
@@ -196,6 +270,7 @@ const diffFontOptions = [
|
|
|
196
270
|
];
|
|
197
271
|
|
|
198
272
|
const isDiffWhitespace = (value) => value === "hide" || value === "show";
|
|
273
|
+
const isBranchDiffScope = (value) => value === "committed" || value === "uncommitted";
|
|
199
274
|
|
|
200
275
|
const fenceOpen = /^( {0,3})(`{3,}(?!`)|~{3,}(?!~))(.*)$/;
|
|
201
276
|
const withoutIndent = (line, indent) => {
|
|
@@ -669,7 +744,7 @@ const pullRequestReactionContents = [
|
|
|
669
744
|
];
|
|
670
745
|
const isPullRequestReactionContent = (value) => pullRequestReactionContents.includes(value);
|
|
671
746
|
|
|
672
|
-
const isStringArray = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
747
|
+
const isStringArray$1 = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
673
748
|
const isNonEmptyString = (value) => typeof value === "string" && value.trim() !== "";
|
|
674
749
|
const isPositiveInteger = (value) => typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
675
750
|
const isSha = (value) => typeof value === "string" && /^[\da-f]{40}$/i.test(value);
|
|
@@ -794,13 +869,13 @@ const parsePullRequestEdit = (value) => {
|
|
|
794
869
|
return { kind: "title", title: edit.title.trim() };
|
|
795
870
|
}
|
|
796
871
|
case "labels": {
|
|
797
|
-
if (!isStringArray(edit.add) || !isStringArray(edit.remove)) {
|
|
872
|
+
if (!isStringArray$1(edit.add) || !isStringArray$1(edit.remove)) {
|
|
798
873
|
return "labels take add and remove, arrays of names";
|
|
799
874
|
}
|
|
800
875
|
return { kind: "labels", add: edit.add, remove: edit.remove };
|
|
801
876
|
}
|
|
802
877
|
case "reviewers": {
|
|
803
|
-
if (!isStringArray(edit.add) || !isStringArray(edit.remove) || !isStringArray(edit.addTeams) || !isStringArray(edit.removeTeams)) {
|
|
878
|
+
if (!isStringArray$1(edit.add) || !isStringArray$1(edit.remove) || !isStringArray$1(edit.addTeams) || !isStringArray$1(edit.removeTeams)) {
|
|
804
879
|
return "reviewers take add, remove, addTeams and removeTeams, arrays of logins and team slugs";
|
|
805
880
|
}
|
|
806
881
|
return {
|
|
@@ -816,7 +891,7 @@ const parsePullRequestEdit = (value) => {
|
|
|
816
891
|
return { kind: "draft", draft: edit.draft };
|
|
817
892
|
}
|
|
818
893
|
case "assignees": {
|
|
819
|
-
if (!isStringArray(edit.add) || !isStringArray(edit.remove)) {
|
|
894
|
+
if (!isStringArray$1(edit.add) || !isStringArray$1(edit.remove)) {
|
|
820
895
|
return "assignees take add and remove, arrays of logins";
|
|
821
896
|
}
|
|
822
897
|
return { kind: "assignees", add: edit.add, remove: edit.remove };
|
|
@@ -1376,6 +1451,91 @@ const diffLines = ({
|
|
|
1376
1451
|
];
|
|
1377
1452
|
};
|
|
1378
1453
|
|
|
1454
|
+
const openPullRequestsLimit = 100;
|
|
1455
|
+
const openPullRequestsQuery = `query OpenPullRequests($owner: String!, $name: String!, $first: Int!) {
|
|
1456
|
+
repository(owner: $owner, name: $name) {
|
|
1457
|
+
pullRequests(states: OPEN, first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) {
|
|
1458
|
+
totalCount
|
|
1459
|
+
nodes {
|
|
1460
|
+
number
|
|
1461
|
+
title
|
|
1462
|
+
isDraft
|
|
1463
|
+
updatedAt
|
|
1464
|
+
headRefName
|
|
1465
|
+
headRepository { nameWithOwner }
|
|
1466
|
+
author { login }
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
}`;
|
|
1471
|
+
const parseOpenPullRequests = ({
|
|
1472
|
+
data,
|
|
1473
|
+
errors
|
|
1474
|
+
}) => {
|
|
1475
|
+
const connection = data?.repository?.pullRequests ?? null;
|
|
1476
|
+
if (connection === null) {
|
|
1477
|
+
throw new Error(errors.length > 0 ? errors.join("; ") : "not found");
|
|
1478
|
+
}
|
|
1479
|
+
const pullRequests = (connection.nodes ?? []).filter((node) => node !== null).map((node) => ({
|
|
1480
|
+
number: node.number,
|
|
1481
|
+
title: node.title,
|
|
1482
|
+
isDraft: node.isDraft,
|
|
1483
|
+
headRefName: node.headRefName,
|
|
1484
|
+
headOwnerRepo: node.headRepository?.nameWithOwner ?? null,
|
|
1485
|
+
author: node.author?.login ?? null,
|
|
1486
|
+
updatedAt: node.updatedAt
|
|
1487
|
+
}));
|
|
1488
|
+
return { pullRequests, totalCount: connection.totalCount };
|
|
1489
|
+
};
|
|
1490
|
+
const fetchOpenPullRequests = async (transport, ownerRepo) => {
|
|
1491
|
+
const split = splitOwnerRepo(ownerRepo);
|
|
1492
|
+
if (split === null) throw new Error(`${ownerRepo} is not owner/repo`);
|
|
1493
|
+
return parseOpenPullRequests(
|
|
1494
|
+
await transport.graphql(openPullRequestsQuery, {
|
|
1495
|
+
owner: split.owner,
|
|
1496
|
+
name: split.name,
|
|
1497
|
+
first: openPullRequestsLimit
|
|
1498
|
+
})
|
|
1499
|
+
);
|
|
1500
|
+
};
|
|
1501
|
+
const branchPullRequestsQuery = `query BranchPullRequests($owner: String!, $name: String!, $branch: String!) {
|
|
1502
|
+
repository(owner: $owner, name: $name) {
|
|
1503
|
+
pullRequests(headRefName: $branch, first: 20, orderBy: { field: UPDATED_AT, direction: DESC }) {
|
|
1504
|
+
nodes {
|
|
1505
|
+
number
|
|
1506
|
+
state
|
|
1507
|
+
headRefOid
|
|
1508
|
+
headRepository { nameWithOwner }
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}`;
|
|
1513
|
+
const parseBranchPullRequestHeads = ({ data, errors }, ownerRepo) => {
|
|
1514
|
+
const connection = data?.repository?.pullRequests ?? null;
|
|
1515
|
+
if (connection === null) {
|
|
1516
|
+
throw new Error(errors.length > 0 ? errors.join("; ") : "not found");
|
|
1517
|
+
}
|
|
1518
|
+
return (connection.nodes ?? []).filter((node) => node !== null).filter(
|
|
1519
|
+
(node) => node.headRepository?.nameWithOwner.toLowerCase() === ownerRepo.toLowerCase()
|
|
1520
|
+
).map(({ number, state, headRefOid }) => ({
|
|
1521
|
+
number,
|
|
1522
|
+
state,
|
|
1523
|
+
sha: headRefOid
|
|
1524
|
+
}));
|
|
1525
|
+
};
|
|
1526
|
+
const fetchBranchPullRequestHeads = async (transport, ownerRepo, branch) => {
|
|
1527
|
+
const split = splitOwnerRepo(ownerRepo);
|
|
1528
|
+
if (split === null) throw new Error(`${ownerRepo} is not owner/repo`);
|
|
1529
|
+
return parseBranchPullRequestHeads(
|
|
1530
|
+
await transport.graphql(branchPullRequestsQuery, {
|
|
1531
|
+
owner: split.owner,
|
|
1532
|
+
name: split.name,
|
|
1533
|
+
branch
|
|
1534
|
+
}),
|
|
1535
|
+
ownerRepo
|
|
1536
|
+
);
|
|
1537
|
+
};
|
|
1538
|
+
|
|
1379
1539
|
const imgTag = /<img\b[^>]*>/gi;
|
|
1380
1540
|
const decodeAttribute = (value) => value.replaceAll(""", '"').replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">").replaceAll("&", "&");
|
|
1381
1541
|
const attribute = (tag, name) => {
|
|
@@ -3488,21 +3648,49 @@ const createReviewflowFollower = ({
|
|
|
3488
3648
|
};
|
|
3489
3649
|
};
|
|
3490
3650
|
|
|
3491
|
-
const
|
|
3651
|
+
const defaultRepoPaths = ["~/dev/"];
|
|
3492
3652
|
|
|
3493
3653
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3494
|
-
const
|
|
3654
|
+
const parseConfigText = (source) => {
|
|
3655
|
+
if (source.trim() === "") return {};
|
|
3495
3656
|
let parsed;
|
|
3496
|
-
|
|
3497
|
-
parsed =
|
|
3498
|
-
}
|
|
3499
|
-
|
|
3500
|
-
parsed = JSON.parse(source);
|
|
3501
|
-
} catch {
|
|
3502
|
-
return null;
|
|
3503
|
-
}
|
|
3657
|
+
try {
|
|
3658
|
+
parsed = JSON.parse(source);
|
|
3659
|
+
} catch {
|
|
3660
|
+
return null;
|
|
3504
3661
|
}
|
|
3505
|
-
|
|
3662
|
+
return isRecord(parsed) ? parsed : null;
|
|
3663
|
+
};
|
|
3664
|
+
const stringifyConfig = (config) => `${JSON.stringify(config, null, 2)}
|
|
3665
|
+
`;
|
|
3666
|
+
|
|
3667
|
+
const repoPathsKeys = /* @__PURE__ */ new Set(["repoPaths", "defaultRepoPath"]);
|
|
3668
|
+
const isStringArray = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
3669
|
+
const readRepoPathsSetting = (source) => {
|
|
3670
|
+
const config = parseConfigText(source);
|
|
3671
|
+
if (config === null) return void 0;
|
|
3672
|
+
if (isStringArray(config.repoPaths)) return config.repoPaths;
|
|
3673
|
+
if (typeof config.defaultRepoPath === "string") {
|
|
3674
|
+
return [config.defaultRepoPath];
|
|
3675
|
+
}
|
|
3676
|
+
return void 0;
|
|
3677
|
+
};
|
|
3678
|
+
const writeRepoPathsSetting = (source, paths) => {
|
|
3679
|
+
const config = parseConfigText(source);
|
|
3680
|
+
if (config === null) return null;
|
|
3681
|
+
const repoPaths = [
|
|
3682
|
+
...new Set(paths.map((path) => path.trim()).filter((path) => path !== ""))
|
|
3683
|
+
];
|
|
3684
|
+
const entries = Object.entries(config);
|
|
3685
|
+
const at = entries.findIndex(([key]) => repoPathsKeys.has(key));
|
|
3686
|
+
const others = entries.filter(([key]) => !repoPathsKeys.has(key));
|
|
3687
|
+
others.splice(Math.max(at, 0), 0, ["repoPaths", repoPaths]);
|
|
3688
|
+
return stringifyConfig(Object.fromEntries(others));
|
|
3689
|
+
};
|
|
3690
|
+
|
|
3691
|
+
const insertCheckoutEntry = (source, ownerRepo) => {
|
|
3692
|
+
const parsed = parseConfigText(source);
|
|
3693
|
+
if (parsed === null) return null;
|
|
3506
3694
|
const repos = parsed.repos ?? {};
|
|
3507
3695
|
if (!isRecord(repos)) return null;
|
|
3508
3696
|
const wanted = ownerRepo.toLowerCase();
|
|
@@ -3511,8 +3699,7 @@ const insertCheckoutEntry = (source, ownerRepo) => {
|
|
|
3511
3699
|
);
|
|
3512
3700
|
const key = existing ?? ownerRepo;
|
|
3513
3701
|
const next = existing === void 0 ? { ...repos, [key]: "" } : repos;
|
|
3514
|
-
const text =
|
|
3515
|
-
`;
|
|
3702
|
+
const text = stringifyConfig({ ...parsed, repos: next });
|
|
3516
3703
|
const marker = `${JSON.stringify(key)}: "`;
|
|
3517
3704
|
const reposAt = text.indexOf('\n "repos": ');
|
|
3518
3705
|
const at = text.indexOf(marker, reposAt);
|
|
@@ -3521,5 +3708,5 @@ const insertCheckoutEntry = (source, ownerRepo) => {
|
|
|
3521
3708
|
return { source: text, cursor: value };
|
|
3522
3709
|
};
|
|
3523
3710
|
|
|
3524
|
-
export { accountOfOwnerRepo, applyPullRequestEdit, applyStreamEvent, applySuggestionsToText, assembleBoard, boardReadDelayMs, buildPrQuery, changedPullRequestParts, checkOutcomes, checksFromReviewflow, coAuthorTrailers, commitSuggestions, createGitHubTransport, createReviewflowClient, createReviewflowFollower, defaultDiffFont, defaultDiffTheme,
|
|
3711
|
+
export { accountOfOwnerRepo, applyPullRequestEdit, applyStreamEvent, applySuggestionsToText, assembleBoard, boardReadDelayMs, buildPrQuery, changedPullRequestParts, checkOutcomes, checksFromReviewflow, coAuthorTrailers, commitSuggestions, createGitHubTransport, createReviewflowClient, createReviewflowFollower, defaultDiffFont, defaultDiffTheme, defaultRepoPaths, diffFontOptions, diffLines, diffPaths, diffThemeNames, diffThemeOptions, diffThemes, dirtyRefusal, editPullRequest, fail, fetchBranchPullRequestHeads, fetchCompareDiff, fetchOpenPullRequests, fetchPullRequestCandidates, fetchPullRequestHead, fetchPullRequestPart, fetchPullRequestRevisions, formatPrChanges, formatRelativeDate, insertCheckoutEntry, interpretGraphql, isAttachablePath, isBranchDiffScope, isDiffFont, isDiffTheme, isDiffWhitespace, isPullRequestOnBoard, isPullRequestPart, isPullRequestReactionContent, isReviewflowFailure, lingerMs, lockRefusal, mainWorktreeRefusal, maxExtraPagesPerBucket, maxFailedCheckNames, maxLookupTargets, maxRetryMs, parseBranchPullRequestHeads, parseOpenPullRequests, parsePrQueryResponse, parsePullRequestCandidates, parsePullRequestChecks, parsePullRequestCommits, parsePullRequestDetail, parsePullRequestEdit, parsePullRequestFiles, parsePullRequestRevisions, parsePullRequestTimeline, parseSuggestions, partFingerprint, planSuggestions, pluralOf, prFields, prHasConflicts, prNumberOf, prStatus, prStatusLabels, prStatuses, prUrl, processesRefusal, pullRefusal, pullRequestCandidatesQuery, pullRequestChecksQuery, pullRequestCommitsQuery, pullRequestDetailQuery, pullRequestEditMovesBoard, pullRequestFilesQuery, pullRequestHeadQuery, pullRequestOfBranch, pullRequestParts, pullRequestPartsStaleAfter, pullRequestReactionContents, pullRequestRevisionsQuery, pullRequestTimelineQuery, pullRequestsWithoutBranch, pushRefusal, readAccountBoards, readPrDetail, readRepoPathsSetting, readReviewflowStream, reconnectDelayMs, responseFailure, restErrorMessage, reviewflowErrorCode, reviewflowPrChangesRequested, reviewflowPrFailure, reviewflowPrFlowDate, reviewflowPrOwners, reviewflowPrProgress, reviewflowPrStatus, reviewflowReviewDecisions, splitOwnerRepo, streamClosedWithinDays, streamIdleTimeoutMs, subjectsOf, suggestionCommitRefusal, suggestionOriginalLines, suggestionRangesOverlap, toBoardPr, toFetchedPr, unpushedRefusal, withAccountBoard, withFileViewed, worktreeRemovalRefusal, worktreeRemovalRefusals, writeRepoPathsSetting };
|
|
3525
3712
|
//# sourceMappingURL=index-node.mjs.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reviewflow/sdk",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.27.0",
|
|
4
4
|
"description": "pull request model shared by the reviewflow app server and its ui: diff and api types, pure utils, the GitHub layer behind an injected transport, and (node only) reading a diff from a local checkout (@reviewflow/sdk/git) and editing a text file in place (@reviewflow/sdk/files)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"code-review",
|