rl-workspaces 0.0.0-stage → 0.5.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/LICENSE +21 -0
- package/README.md +703 -2
- package/bin/rl +54 -0
- package/config.example.zsh +9 -0
- package/dist/access.js +40 -0
- package/dist/activity.js +90 -0
- package/dist/agent-claude.js +16 -0
- package/dist/agent-codex.js +22 -0
- package/dist/agent-launch.js +74 -0
- package/dist/agent-presets.js +45 -0
- package/dist/agent-registry.js +8 -0
- package/dist/agent-resume.js +96 -0
- package/dist/agent-runner.js +366 -0
- package/dist/agent-workflows.js +231 -0
- package/dist/agents.js +73 -0
- package/dist/automation-config.js +242 -0
- package/dist/base.js +66 -0
- package/dist/cleanup-path.js +33 -0
- package/dist/cli.js +57 -0
- package/dist/codex-sessions.js +188 -0
- package/dist/command-context.js +67 -0
- package/dist/common.js +262 -0
- package/dist/delete.js +113 -0
- package/dist/display.js +134 -0
- package/dist/doctor.js +145 -0
- package/dist/git-safety.js +24 -0
- package/dist/git-workflow.js +183 -0
- package/dist/help.js +726 -0
- package/dist/hooks.js +159 -0
- package/dist/json.js +34 -0
- package/dist/mcp.js +336 -0
- package/dist/picker.js +182 -0
- package/dist/pr-discovery.js +223 -0
- package/dist/pr-update.js +419 -0
- package/dist/pr-workflow.js +161 -0
- package/dist/repos.js +537 -0
- package/dist/review-policy.js +150 -0
- package/dist/session-workflows.js +616 -0
- package/dist/session.js +12 -0
- package/dist/state.js +250 -0
- package/dist/storage.js +366 -0
- package/dist/store.js +379 -0
- package/dist/summary.js +164 -0
- package/dist/task-state.js +65 -0
- package/dist/task.js +192 -0
- package/dist/templates.js +4 -0
- package/dist/workflow-config.js +177 -0
- package/dist/workflow.js +211 -0
- package/lib/rl.zsh +330 -0
- package/node_modules/graceful-fs/LICENSE +15 -0
- package/node_modules/graceful-fs/README.md +143 -0
- package/node_modules/graceful-fs/clone.js +23 -0
- package/node_modules/graceful-fs/graceful-fs.js +448 -0
- package/node_modules/graceful-fs/legacy-streams.js +118 -0
- package/node_modules/graceful-fs/package.json +53 -0
- package/node_modules/graceful-fs/polyfills.js +355 -0
- package/node_modules/proper-lockfile/LICENSE +21 -0
- package/node_modules/proper-lockfile/README.md +183 -0
- package/node_modules/proper-lockfile/index.js +40 -0
- package/node_modules/proper-lockfile/lib/adapter.js +85 -0
- package/node_modules/proper-lockfile/lib/lockfile.js +342 -0
- package/node_modules/proper-lockfile/lib/mtime-precision.js +55 -0
- package/node_modules/proper-lockfile/package.json +71 -0
- package/node_modules/retry/.npmignore +3 -0
- package/node_modules/retry/.travis.yml +15 -0
- package/node_modules/retry/License +21 -0
- package/node_modules/retry/Makefile +18 -0
- package/node_modules/retry/README.md +227 -0
- package/node_modules/retry/equation.gif +0 -0
- package/node_modules/retry/example/dns.js +31 -0
- package/node_modules/retry/example/stop.js +40 -0
- package/node_modules/retry/index.js +1 -0
- package/node_modules/retry/lib/retry.js +100 -0
- package/node_modules/retry/lib/retry_operation.js +158 -0
- package/node_modules/retry/package.json +32 -0
- package/node_modules/retry/test/common.js +10 -0
- package/node_modules/retry/test/integration/test-forever.js +24 -0
- package/node_modules/retry/test/integration/test-retry-operation.js +258 -0
- package/node_modules/retry/test/integration/test-retry-wrap.js +101 -0
- package/node_modules/retry/test/integration/test-timeouts.js +69 -0
- package/node_modules/signal-exit/LICENSE.txt +16 -0
- package/node_modules/signal-exit/README.md +39 -0
- package/node_modules/signal-exit/index.js +202 -0
- package/node_modules/signal-exit/package.json +38 -0
- package/node_modules/signal-exit/signals.js +53 -0
- package/package.json +57 -4
- package/shell/rl.zsh +11 -0
package/dist/picker.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { discoverPrs } from "./pr-discovery.js";
|
|
2
|
+
import { accessLabel, lastAccessed } from "./access.js";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { args, arity, canonical, execute, git, inside, required, timestamp, } from "./common.js";
|
|
5
|
+
import { Store } from "./store.js";
|
|
6
|
+
import { worktrees } from "./repos.js";
|
|
7
|
+
import { repositoryBase, resolveBase, pullRequestBase } from "./base.js";
|
|
8
|
+
const plain = (value) => value.replace(/\x1b\[[0-9;]*m/g, "");
|
|
9
|
+
const color = (value, code) => process.env.NO_COLOR !== undefined ? value : `\x1b[${code}m${value}\x1b[0m`;
|
|
10
|
+
const stateColors = {
|
|
11
|
+
merged: 35,
|
|
12
|
+
open: 32,
|
|
13
|
+
draft: 33,
|
|
14
|
+
closed: 31,
|
|
15
|
+
none: 90,
|
|
16
|
+
unknown: 90,
|
|
17
|
+
};
|
|
18
|
+
export function candidates(repo, base) {
|
|
19
|
+
return worktrees(repo)
|
|
20
|
+
.slice(1)
|
|
21
|
+
.flatMap((record) => {
|
|
22
|
+
const root = canonical(record.worktree);
|
|
23
|
+
if (root === repo || !inside(root, base))
|
|
24
|
+
return [];
|
|
25
|
+
return [
|
|
26
|
+
{
|
|
27
|
+
name: path.relative(base, root),
|
|
28
|
+
path: root,
|
|
29
|
+
branch: (record.branch ?? "").replace(/^refs\/heads\//, ""),
|
|
30
|
+
locked: "locked" in record,
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export function prState(data) {
|
|
36
|
+
if (!data?.pr)
|
|
37
|
+
return "none";
|
|
38
|
+
const state = String(data.pr.state ?? "").toUpperCase();
|
|
39
|
+
if (state === "MERGED")
|
|
40
|
+
return "merged";
|
|
41
|
+
if (state === "CLOSED")
|
|
42
|
+
return "closed";
|
|
43
|
+
if (state === "OPEN")
|
|
44
|
+
return data.pr.isDraft ? "draft" : "open";
|
|
45
|
+
return "unknown";
|
|
46
|
+
}
|
|
47
|
+
const visible = (value) => value
|
|
48
|
+
.replaceAll("\t", "\\t")
|
|
49
|
+
.replaceAll("\n", "\\n")
|
|
50
|
+
.replaceAll("\r", "\\r")
|
|
51
|
+
.replaceAll("\x1b", "");
|
|
52
|
+
export function pickerRows(store, items, defaultBase, prs = new Map()) {
|
|
53
|
+
const records = new Map(store
|
|
54
|
+
.records()
|
|
55
|
+
.filter(([, data]) => data.status === "active")
|
|
56
|
+
.map(([, data]) => [data.worktree, data]));
|
|
57
|
+
const fallback = repositoryBase(store.repo, defaultBase);
|
|
58
|
+
const details = items.map((item) => {
|
|
59
|
+
const data = records.get(item.path);
|
|
60
|
+
const lookup = prs.get(item.branch);
|
|
61
|
+
const pr = lookup ? lookup.pr : data?.pr;
|
|
62
|
+
const prBase = pullRequestBase(store.repo, pr);
|
|
63
|
+
const savedBase = typeof data?.baseBranch === "string" ? data.baseBranch : undefined;
|
|
64
|
+
const explicit = savedBase && savedBase !== defaultBase ? savedBase : undefined;
|
|
65
|
+
// An unresolved PR target never falls back to an unrelated repository base.
|
|
66
|
+
const resolved = prBase
|
|
67
|
+
? prBase.resolved
|
|
68
|
+
: explicit
|
|
69
|
+
? resolveBase(store.repo, explicit)
|
|
70
|
+
: fallback;
|
|
71
|
+
let diff = "—";
|
|
72
|
+
if (resolved) {
|
|
73
|
+
try {
|
|
74
|
+
const [behind, ahead] = git(item.path, "rev-list", "--left-right", "--count", `${resolved.oid}...HEAD`, "--").split(/\s+/);
|
|
75
|
+
diff = `+${ahead} / -${behind}`;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
/* Unavailable worktrees are not clean worktrees. */
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
item,
|
|
83
|
+
accessed: lastAccessed(data),
|
|
84
|
+
state: pr
|
|
85
|
+
? prState({ pr }) + (lookup?.stale ? "*" : "")
|
|
86
|
+
: lookup && !lookup.stale && lookup.checkedAt
|
|
87
|
+
? "none"
|
|
88
|
+
: "unknown",
|
|
89
|
+
diff,
|
|
90
|
+
base: resolved?.ref ?? prBase?.target ?? explicit ?? "unknown",
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
details.sort((a, b) => {
|
|
94
|
+
const left = a.accessed ? timestamp(a.accessed) : -1n;
|
|
95
|
+
const right = b.accessed ? timestamp(b.accessed) : -1n;
|
|
96
|
+
return left === right
|
|
97
|
+
? a.item.name.localeCompare(b.item.name)
|
|
98
|
+
: left > right
|
|
99
|
+
? -1
|
|
100
|
+
: 1;
|
|
101
|
+
});
|
|
102
|
+
const columns = process.stderr.columns || 110;
|
|
103
|
+
const baseWidth = Math.min(24, Math.max(8, ...details.map((row) => visible(row.base).length)));
|
|
104
|
+
const nameWidth = Math.min(Math.max(12, ...items.map((item) => visible(item.name).length)), Math.max(16, Math.min(48, columns - baseWidth - 47)));
|
|
105
|
+
const diffWidth = Math.max(13, ...details.map((row) => row.diff.length));
|
|
106
|
+
const cell = (value, width) => {
|
|
107
|
+
const text = visible(value);
|
|
108
|
+
return (text.length > width ? text.slice(0, width - 1) + "…" : text).padEnd(width);
|
|
109
|
+
};
|
|
110
|
+
const format = (name, state, diff, base) => `${cell(name, nameWidth)} ${cell(state, 7)} ${cell(diff, diffWidth)} ${cell(base, baseWidth)} LAST ACCESSED`;
|
|
111
|
+
const header = format("WORKSPACE", "PR", "COMMITS + / −", "BASE");
|
|
112
|
+
return details.map(({ item, state, diff, base, accessed }) => ({
|
|
113
|
+
item,
|
|
114
|
+
header,
|
|
115
|
+
// fzf hides the identity field; selection always uses the untruncated name.
|
|
116
|
+
line: `${visible(item.name)}\t${cell((item.locked ? "[locked] " : "") + item.name, nameWidth)} ${color(cell(state, 7), stateColors[state.replace(/\*$/, "")])} ${cell(diff, diffWidth)
|
|
117
|
+
.replace(/\+\d+/g, (value) => color(value, 32))
|
|
118
|
+
.replace(/-\d+/g, (value) => color(value, 31))} ${color(cell(base, baseWidth), 90)} ${color(accessLabel(accessed), 90)}`,
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
export async function select(store, items, defaultBase, multiple) {
|
|
122
|
+
if (!items.length) {
|
|
123
|
+
console.error("No managed worktrees to " + (multiple ? "delete." : "open."));
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
const prs = await discoverPrs(store, items.map((item) => item.branch));
|
|
127
|
+
const rows = pickerRows(store, items, defaultBase, prs);
|
|
128
|
+
const header = (multiple
|
|
129
|
+
? "Tab select · Ctrl-A all · Enter delete · Esc cancel\nDeletes uncommitted work and local branches."
|
|
130
|
+
: "Enter open · Esc cancel") +
|
|
131
|
+
"\nPR auto · * stale · commits ahead / behind base\n\n" +
|
|
132
|
+
rows[0].header;
|
|
133
|
+
const options = [
|
|
134
|
+
"fzf",
|
|
135
|
+
"--ansi",
|
|
136
|
+
"--delimiter=\t",
|
|
137
|
+
"--with-nth=2..",
|
|
138
|
+
"--prompt=" + (multiple ? "Delete › " : "Workspace › "),
|
|
139
|
+
"--height=70%",
|
|
140
|
+
"--border=rounded",
|
|
141
|
+
"--info=inline",
|
|
142
|
+
"--reverse",
|
|
143
|
+
"--no-sort",
|
|
144
|
+
"--header=" + header,
|
|
145
|
+
];
|
|
146
|
+
if (multiple)
|
|
147
|
+
options.push("--multi", "--bind=ctrl-a:select-all,ctrl-d:deselect-all");
|
|
148
|
+
const result = await execute(options, store.repo, {
|
|
149
|
+
input: rows.map((row) => row.line).join("\n") + "\n",
|
|
150
|
+
terminalError: true,
|
|
151
|
+
});
|
|
152
|
+
if ([1, 130].includes(result.code))
|
|
153
|
+
return [];
|
|
154
|
+
if (result.code)
|
|
155
|
+
throw new Error(`fzf failed (exit ${result.code})`);
|
|
156
|
+
const selections = result.stdout
|
|
157
|
+
.split(/\r?\n/)
|
|
158
|
+
.filter(Boolean)
|
|
159
|
+
.map((line) => {
|
|
160
|
+
const row = rows.find((row) => plain(row.line) === plain(line) || row.item.name === line);
|
|
161
|
+
if (!row)
|
|
162
|
+
throw new Error("Picker returned an unknown workspace");
|
|
163
|
+
return row.item;
|
|
164
|
+
});
|
|
165
|
+
if (!multiple && selections.length > 1)
|
|
166
|
+
throw new Error("Picker returned multiple workspaces");
|
|
167
|
+
return [...new Map(selections.map((item) => [item.path, item])).values()];
|
|
168
|
+
}
|
|
169
|
+
export async function main(argv) {
|
|
170
|
+
const { values, positionals } = args(argv, [
|
|
171
|
+
"repo",
|
|
172
|
+
"state-dir",
|
|
173
|
+
"base-dir",
|
|
174
|
+
"default-base",
|
|
175
|
+
]);
|
|
176
|
+
arity(positionals, 0);
|
|
177
|
+
const store = new Store(required(values, "repo"), required(values, "state-dir"));
|
|
178
|
+
const selected = await select(store, candidates(store.repo, canonical(required(values, "base-dir"))), required(values, "default-base"), false);
|
|
179
|
+
if (selected.length)
|
|
180
|
+
process.stdout.write(selected[0].path + "\n");
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { execute, message, now, object, pooled, readJson, writeJson, } from "./common.js";
|
|
4
|
+
const TTL = 60_000;
|
|
5
|
+
const fields = "number url title state headRefName baseRefName isDraft updatedAt headRepository { nameWithOwner }";
|
|
6
|
+
export async function discoverPrs(store, branches) {
|
|
7
|
+
const file = path.join(store.root, "pr-cache.json"), cached = new Map();
|
|
8
|
+
try {
|
|
9
|
+
if (fs.existsSync(file)) {
|
|
10
|
+
const data = readJson(file);
|
|
11
|
+
if (data.schemaVersion === 1 && object(data.branches))
|
|
12
|
+
for (const [branch, entry] of Object.entries(data.branches)) {
|
|
13
|
+
if (object(entry) &&
|
|
14
|
+
typeof entry.checkedAt === "string" &&
|
|
15
|
+
Number.isFinite(Date.parse(entry.checkedAt)) &&
|
|
16
|
+
(entry.pr === null ||
|
|
17
|
+
(object(entry.pr) &&
|
|
18
|
+
typeof entry.pr.url === "string" &&
|
|
19
|
+
entry.pr.headRefName === branch)))
|
|
20
|
+
cached.set(branch, entry);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
/* A disposable lookup cache must not prevent opening a worktree. */
|
|
26
|
+
}
|
|
27
|
+
const results = new Map();
|
|
28
|
+
for (const [, data] of store.records()) {
|
|
29
|
+
if (data.status !== "active" || !data.pr)
|
|
30
|
+
continue;
|
|
31
|
+
const previous = cached.get(data.branch), stamp = data.pr.syncedAt;
|
|
32
|
+
if (!previous ||
|
|
33
|
+
(typeof stamp === "string" &&
|
|
34
|
+
Date.parse(stamp) > Date.parse(previous.checkedAt)))
|
|
35
|
+
results.set(data.branch, {
|
|
36
|
+
pr: data.pr,
|
|
37
|
+
checkedAt: typeof stamp === "string" ? stamp : undefined,
|
|
38
|
+
stale: false,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
for (const [branch, entry] of cached)
|
|
42
|
+
if (!results.has(branch))
|
|
43
|
+
results.set(branch, { ...entry, stale: false });
|
|
44
|
+
const pending = [...new Set(branches.filter(Boolean))].filter((branch) => {
|
|
45
|
+
const value = results.get(branch), age = value?.checkedAt
|
|
46
|
+
? Date.now() - Date.parse(value.checkedAt)
|
|
47
|
+
: Infinity;
|
|
48
|
+
return !(age >= 0 && age < TTL);
|
|
49
|
+
});
|
|
50
|
+
if (!pending.length)
|
|
51
|
+
return results;
|
|
52
|
+
const fail = (branch) => results.set(branch, {
|
|
53
|
+
pr: results.get(branch)?.pr ?? null,
|
|
54
|
+
checkedAt: results.get(branch)?.checkedAt,
|
|
55
|
+
stale: true,
|
|
56
|
+
});
|
|
57
|
+
if (process.env.RL_PR_OFFLINE === "1") {
|
|
58
|
+
pending.forEach(fail);
|
|
59
|
+
return results;
|
|
60
|
+
}
|
|
61
|
+
const started = now(), deadline = Date.now() + 8000;
|
|
62
|
+
const ghEnv = { ...process.env, GH_PROMPT_DISABLED: "1" };
|
|
63
|
+
delete ghEnv.GH_REPO;
|
|
64
|
+
async function gh(args) {
|
|
65
|
+
const remaining = deadline - Date.now();
|
|
66
|
+
if (remaining <= 0)
|
|
67
|
+
throw new Error("GitHub lookup timed out");
|
|
68
|
+
const result = await execute(["gh", ...args], store.repo, {
|
|
69
|
+
timeoutMs: remaining,
|
|
70
|
+
env: ghEnv,
|
|
71
|
+
});
|
|
72
|
+
if (result.code)
|
|
73
|
+
throw new Error(result.stderr.trim() || "GitHub lookup failed or timed out");
|
|
74
|
+
return JSON.parse(result.stdout);
|
|
75
|
+
}
|
|
76
|
+
const refreshed = new Map(), failures = [];
|
|
77
|
+
console.error("Checking GitHub PRs…");
|
|
78
|
+
try {
|
|
79
|
+
const identity = await gh(["repo", "view", "--json", "nameWithOwner,url"]);
|
|
80
|
+
if (!object(identity) ||
|
|
81
|
+
typeof identity.nameWithOwner !== "string" ||
|
|
82
|
+
typeof identity.url !== "string")
|
|
83
|
+
throw new Error("Invalid GitHub repository identity");
|
|
84
|
+
const repositoryName = identity.nameWithOwner;
|
|
85
|
+
const [owner, name, extra] = repositoryName.split("/"), url = new URL(identity.url);
|
|
86
|
+
if (!owner ||
|
|
87
|
+
!name ||
|
|
88
|
+
extra ||
|
|
89
|
+
url.protocol !== "https:" ||
|
|
90
|
+
url.pathname.replace(/\/$/, "") !== `/${owner}/${name}`)
|
|
91
|
+
throw new Error("Invalid GitHub repository URL");
|
|
92
|
+
const prefix = identity.url.replace(/\/$/, "") + "/pull/";
|
|
93
|
+
const batches = [];
|
|
94
|
+
for (let i = 0; i < pending.length; i += 20)
|
|
95
|
+
batches.push(pending.slice(i, i + 20));
|
|
96
|
+
await pooled(batches, 2, async (batch) => {
|
|
97
|
+
try {
|
|
98
|
+
const selections = batch
|
|
99
|
+
.map((branch, i) => [
|
|
100
|
+
`o${i}: pullRequests(headRefName: ${JSON.stringify(branch)}, states: [OPEN], first: 100, orderBy: {field: CREATED_AT, direction: DESC}) { nodes { ${fields} } pageInfo { hasNextPage } }`,
|
|
101
|
+
`h${i}: pullRequests(headRefName: ${JSON.stringify(branch)}, states: [CLOSED, MERGED], first: 100, orderBy: {field: CREATED_AT, direction: DESC}) { nodes { ${fields} } pageInfo { hasNextPage } }`,
|
|
102
|
+
].join("\n"))
|
|
103
|
+
.join("\n");
|
|
104
|
+
const query = `query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { ${selections} } }`;
|
|
105
|
+
const response = await gh([
|
|
106
|
+
"api",
|
|
107
|
+
"graphql",
|
|
108
|
+
"--hostname",
|
|
109
|
+
url.host,
|
|
110
|
+
"-f",
|
|
111
|
+
`query=${query}`,
|
|
112
|
+
"-f",
|
|
113
|
+
`owner=${owner}`,
|
|
114
|
+
"-f",
|
|
115
|
+
`name=${name}`,
|
|
116
|
+
]);
|
|
117
|
+
if (!object(response) ||
|
|
118
|
+
response.errors ||
|
|
119
|
+
!object(response.data) ||
|
|
120
|
+
!object(response.data.repository))
|
|
121
|
+
throw new Error("Incomplete GitHub PR response");
|
|
122
|
+
const repository = response.data.repository;
|
|
123
|
+
for (const [i, branch] of batch.entries()) {
|
|
124
|
+
try {
|
|
125
|
+
let selected = null;
|
|
126
|
+
for (const alias of [`o${i}`, `h${i}`]) {
|
|
127
|
+
const connection = repository[alias];
|
|
128
|
+
if (!object(connection) ||
|
|
129
|
+
!Array.isArray(connection.nodes) ||
|
|
130
|
+
!object(connection.pageInfo) ||
|
|
131
|
+
typeof connection.pageInfo.hasNextPage !== "boolean")
|
|
132
|
+
throw new Error("Incomplete branch lookup");
|
|
133
|
+
for (const pr of connection.nodes) {
|
|
134
|
+
if (!object(pr) ||
|
|
135
|
+
pr.headRefName !== branch ||
|
|
136
|
+
typeof pr.url !== "string" ||
|
|
137
|
+
!pr.url.startsWith(prefix) ||
|
|
138
|
+
!object(pr.headRepository) ||
|
|
139
|
+
typeof pr.headRepository.nameWithOwner !== "string")
|
|
140
|
+
throw new Error("Invalid branch PR identity");
|
|
141
|
+
// A fork can reuse the branch name; it is not this repository's workspace.
|
|
142
|
+
if (pr.headRepository.nameWithOwner.toLowerCase() !==
|
|
143
|
+
repositoryName.toLowerCase())
|
|
144
|
+
continue;
|
|
145
|
+
if (!Number.isInteger(pr.number) ||
|
|
146
|
+
typeof pr.baseRefName !== "string" ||
|
|
147
|
+
!pr.baseRefName ||
|
|
148
|
+
!["OPEN", "CLOSED", "MERGED"].includes(String(pr.state)) ||
|
|
149
|
+
typeof pr.isDraft !== "boolean" ||
|
|
150
|
+
typeof pr.updatedAt !== "string")
|
|
151
|
+
throw new Error("Incomplete PR metadata");
|
|
152
|
+
selected = { ...pr, url: pr.url, syncedAt: started };
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
if (selected)
|
|
156
|
+
break;
|
|
157
|
+
if (connection.pageInfo.hasNextPage)
|
|
158
|
+
throw new Error("Branch PR results truncated");
|
|
159
|
+
}
|
|
160
|
+
const entry = { pr: selected, checkedAt: started };
|
|
161
|
+
refreshed.set(branch, entry);
|
|
162
|
+
results.set(branch, { ...entry, stale: false });
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
fail(branch);
|
|
166
|
+
failures.push(`${branch}: ${message(error)}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
batch.forEach(fail);
|
|
172
|
+
failures.push(message(error));
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
pending.forEach(fail);
|
|
178
|
+
failures.push(message(error));
|
|
179
|
+
}
|
|
180
|
+
if (refreshed.size) {
|
|
181
|
+
try {
|
|
182
|
+
await store.locked(() => {
|
|
183
|
+
let existing = {};
|
|
184
|
+
try {
|
|
185
|
+
const data = readJson(file);
|
|
186
|
+
if (object(data.branches))
|
|
187
|
+
existing = data.branches;
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
/* New/disposable cache. */
|
|
191
|
+
}
|
|
192
|
+
const merged = new Map(Object.entries(existing));
|
|
193
|
+
for (const [branch, entry] of refreshed) {
|
|
194
|
+
const previous = merged.get(branch);
|
|
195
|
+
if (!object(previous) ||
|
|
196
|
+
typeof previous.checkedAt !== "string" ||
|
|
197
|
+
Date.parse(previous.checkedAt) <= Date.parse(started))
|
|
198
|
+
merged.set(branch, entry);
|
|
199
|
+
}
|
|
200
|
+
writeJson(file, {
|
|
201
|
+
schemaVersion: 1,
|
|
202
|
+
branches: Object.fromEntries(merged),
|
|
203
|
+
});
|
|
204
|
+
for (const [record, data] of store.records()) {
|
|
205
|
+
const entry = refreshed.get(data.branch);
|
|
206
|
+
if (data.status !== "active" || !entry)
|
|
207
|
+
continue;
|
|
208
|
+
if (typeof data.pr?.syncedAt === "string" &&
|
|
209
|
+
Date.parse(data.pr.syncedAt) > Date.parse(started))
|
|
210
|
+
continue;
|
|
211
|
+
data.pr = entry.pr;
|
|
212
|
+
store.save(record, data);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
failures.push(`Could not save PR cache: ${message(error)}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (failures.length)
|
|
221
|
+
console.error(`rl: PR lookup incomplete; * marks a saved snapshot, unknown means not verified. ${failures[0]}`);
|
|
222
|
+
return results;
|
|
223
|
+
}
|