@algochad/archcoder 2.0.2
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 +113 -0
- package/bin/cli-entry.js +55 -0
- package/bin/cli-output.js +145 -0
- package/bin/cli.js +5108 -0
- package/bin/cli.test.js +56 -0
- package/dist/apple-touch-icon-120x120.png +0 -0
- package/dist/apple-touch-icon-152x152.png +0 -0
- package/dist/apple-touch-icon-167x167.png +0 -0
- package/dist/apple-touch-icon-180x180.png +0 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/apple-touch-icon.svg +67 -0
- package/dist/assets/MultiRunWindow-BZp3MjJP.js +1 -0
- package/dist/assets/SettingsWindow-DoGYXpX7.js +1 -0
- package/dist/assets/TerminalView-BN7BR5Ff.js +3 -0
- package/dist/assets/TimelineDialog-ZQ33oVQR.js +1 -0
- package/dist/assets/ToolOutputDialog-Blv3pnug.js +16 -0
- package/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/dist/assets/index-CtCEGYrr.css +1 -0
- package/dist/assets/index-o_d2wtWC.js +48 -0
- package/dist/assets/main-5QGBtzdq.css +1 -0
- package/dist/assets/main-B6oiMU86.js +8033 -0
- package/dist/assets/vendor--DbVqbJpV.css +1 -0
- package/dist/assets/vendor-.bun-HTKwyaEM.js +10086 -0
- package/dist/assets/wasm-CG6Dc4jp.js +1 -0
- package/dist/assets/worker-bqd4RMrj.js +155 -0
- package/dist/favicon-16.png +0 -0
- package/dist/favicon-32.png +0 -0
- package/dist/favicon.png +0 -0
- package/dist/favicon.svg +67 -0
- package/dist/index.html +533 -0
- package/dist/logo-dark-192x192.png +0 -0
- package/dist/logo-dark-512x512.svg +16 -0
- package/dist/logo-light-192x192.png +0 -0
- package/dist/logo-light-512x512.svg +16 -0
- package/dist/pwa-192.png +0 -0
- package/dist/pwa-512.png +0 -0
- package/dist/pwa-maskable-192.png +0 -0
- package/dist/pwa-maskable-512.png +0 -0
- package/dist/site.webmanifest +22 -0
- package/dist/sw.js +1 -0
- package/package.json +107 -0
- package/public/apple-touch-icon-120x120.png +0 -0
- package/public/apple-touch-icon-152x152.png +0 -0
- package/public/apple-touch-icon-167x167.png +0 -0
- package/public/apple-touch-icon-180x180.png +0 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/apple-touch-icon.svg +67 -0
- package/public/favicon-16.png +0 -0
- package/public/favicon-32.png +0 -0
- package/public/favicon.png +0 -0
- package/public/favicon.svg +67 -0
- package/public/logo-dark-192x192.png +0 -0
- package/public/logo-dark-512x512.svg +16 -0
- package/public/logo-light-192x192.png +0 -0
- package/public/logo-light-512x512.svg +16 -0
- package/public/pwa-192.png +0 -0
- package/public/pwa-512.png +0 -0
- package/public/pwa-maskable-192.png +0 -0
- package/public/pwa-maskable-512.png +0 -0
- package/public/site.webmanifest +22 -0
- package/server/TERMINAL_INPUT_WS_PROTOCOL.md +44 -0
- package/server/index.d.ts +37 -0
- package/server/index.js +14694 -0
- package/server/lib/cloudflare-tunnel.js +650 -0
- package/server/lib/git/DOCUMENTATION.md +146 -0
- package/server/lib/git/credentials.js +74 -0
- package/server/lib/git/identity-storage.js +110 -0
- package/server/lib/git/index.js +6 -0
- package/server/lib/git/service.js +3117 -0
- package/server/lib/github/DOCUMENTATION.md +170 -0
- package/server/lib/github/auth.js +307 -0
- package/server/lib/github/device-flow.js +50 -0
- package/server/lib/github/index.js +24 -0
- package/server/lib/github/octokit.js +10 -0
- package/server/lib/github/pr-status.js +478 -0
- package/server/lib/github/repo/index.js +55 -0
- package/server/lib/installer/desktop.js +289 -0
- package/server/lib/installer/download.js +208 -0
- package/server/lib/installer/index.js +45 -0
- package/server/lib/installer/platform.js +100 -0
- package/server/lib/notifications/DOCUMENTATION.md +61 -0
- package/server/lib/notifications/index.js +1 -0
- package/server/lib/notifications/message.js +49 -0
- package/server/lib/notifications/message.test.js +59 -0
- package/server/lib/opencode/DOCUMENTATION.md +59 -0
- package/server/lib/opencode/agents.js +634 -0
- package/server/lib/opencode/auth.js +81 -0
- package/server/lib/opencode/commands.js +339 -0
- package/server/lib/opencode/index.js +66 -0
- package/server/lib/opencode/mcp.js +206 -0
- package/server/lib/opencode/providers.js +96 -0
- package/server/lib/opencode/shared.js +527 -0
- package/server/lib/opencode/skills.js +480 -0
- package/server/lib/opencode/tunnel-auth.js +591 -0
- package/server/lib/opencode/ui-auth.js +510 -0
- package/server/lib/package-manager.js +505 -0
- package/server/lib/quota/DOCUMENTATION.md +55 -0
- package/server/lib/quota/index.js +24 -0
- package/server/lib/quota/providers/claude.js +107 -0
- package/server/lib/quota/providers/codex.js +113 -0
- package/server/lib/quota/providers/copilot.js +165 -0
- package/server/lib/quota/providers/google/api.js +92 -0
- package/server/lib/quota/providers/google/auth.js +108 -0
- package/server/lib/quota/providers/google/index.js +124 -0
- package/server/lib/quota/providers/google/transforms.js +109 -0
- package/server/lib/quota/providers/index.js +152 -0
- package/server/lib/quota/providers/interface.js +55 -0
- package/server/lib/quota/providers/kimi.js +108 -0
- package/server/lib/quota/providers/minimax-cn-coding-plan.js +15 -0
- package/server/lib/quota/providers/minimax-coding-plan.js +15 -0
- package/server/lib/quota/providers/minimax-shared.js +136 -0
- package/server/lib/quota/providers/nanogpt.js +124 -0
- package/server/lib/quota/providers/ollama-cloud.js +112 -0
- package/server/lib/quota/providers/openai.js +91 -0
- package/server/lib/quota/providers/openrouter.js +92 -0
- package/server/lib/quota/providers/zai.js +91 -0
- package/server/lib/quota/utils/auth.js +46 -0
- package/server/lib/quota/utils/formatters.js +76 -0
- package/server/lib/quota/utils/index.js +10 -0
- package/server/lib/quota/utils/transformers.js +55 -0
- package/server/lib/skills-catalog/DOCUMENTATION.md +178 -0
- package/server/lib/skills-catalog/cache.js +32 -0
- package/server/lib/skills-catalog/clawdhub/api.js +158 -0
- package/server/lib/skills-catalog/clawdhub/index.js +30 -0
- package/server/lib/skills-catalog/clawdhub/install.js +238 -0
- package/server/lib/skills-catalog/clawdhub/scan.js +113 -0
- package/server/lib/skills-catalog/curated-sources.js +21 -0
- package/server/lib/skills-catalog/git.js +77 -0
- package/server/lib/skills-catalog/index.js +42 -0
- package/server/lib/skills-catalog/install.js +294 -0
- package/server/lib/skills-catalog/scan.js +221 -0
- package/server/lib/skills-catalog/source.js +85 -0
- package/server/lib/terminal/DOCUMENTATION.md +114 -0
- package/server/lib/terminal/index.js +12 -0
- package/server/lib/terminal/input-ws-protocol.js +66 -0
- package/server/lib/terminal/input-ws-protocol.test.js +138 -0
- package/server/lib/tts/DOCUMENTATION.md +134 -0
- package/server/lib/tts/index.js +16 -0
- package/server/lib/tts/service.js +162 -0
- package/server/lib/tts/summarization.js +171 -0
- package/server/lib/tunnels/index.js +166 -0
- package/server/lib/tunnels/providers/cloudflare.js +260 -0
- package/server/lib/tunnels/registry.js +51 -0
- package/server/lib/tunnels/types.js +219 -0
- package/server/lib/utils/lru.js +107 -0
- package/server/lib/utils/sse.js +121 -0
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import { getRemotes, getStatus } from '../git/index.js';
|
|
2
|
+
import { resolveGitHubRepoFromDirectory } from './repo/index.js';
|
|
3
|
+
|
|
4
|
+
const REPO_DEFAULT_BRANCH_TTL_MS = 5 * 60_000;
|
|
5
|
+
const defaultBranchCache = new Map();
|
|
6
|
+
const repoMetadataCache = new Map();
|
|
7
|
+
|
|
8
|
+
const normalizeText = (value) => typeof value === 'string' ? value.trim() : '';
|
|
9
|
+
const normalizeLower = (value) => normalizeText(value).toLowerCase();
|
|
10
|
+
const normalizeRepoKey = (owner, repo) => {
|
|
11
|
+
const normalizedOwner = normalizeLower(owner);
|
|
12
|
+
const normalizedRepo = normalizeLower(repo);
|
|
13
|
+
if (!normalizedOwner || !normalizedRepo) {
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
return `${normalizedOwner}/${normalizedRepo}`;
|
|
17
|
+
};
|
|
18
|
+
const parseTrackingRemoteName = (trackingBranch) => {
|
|
19
|
+
const normalized = normalizeText(trackingBranch);
|
|
20
|
+
if (!normalized) {
|
|
21
|
+
return '';
|
|
22
|
+
}
|
|
23
|
+
const slashIndex = normalized.indexOf('/');
|
|
24
|
+
if (slashIndex <= 0) {
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
return normalized.slice(0, slashIndex).trim();
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const pushUnique = (collection, value, keyFn = normalizeLower) => {
|
|
31
|
+
const normalizedValue = normalizeText(value);
|
|
32
|
+
if (!normalizedValue) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const nextKey = keyFn(normalizedValue);
|
|
36
|
+
if (!nextKey) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (collection.some((item) => keyFn(item) === nextKey)) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
collection.push(normalizedValue);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const rankRemoteNames = (remoteNames, explicitRemoteName, trackingRemoteName) => {
|
|
46
|
+
const ranked = [];
|
|
47
|
+
pushUnique(ranked, explicitRemoteName);
|
|
48
|
+
|
|
49
|
+
if (trackingRemoteName) {
|
|
50
|
+
pushUnique(ranked, trackingRemoteName);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
pushUnique(ranked, 'origin');
|
|
54
|
+
pushUnique(ranked, 'upstream');
|
|
55
|
+
remoteNames.forEach((name) => pushUnique(ranked, name));
|
|
56
|
+
return ranked;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const getHeadOwner = (pr) => {
|
|
60
|
+
const repoOwner = normalizeText(pr?.head?.repo?.owner?.login);
|
|
61
|
+
if (repoOwner) {
|
|
62
|
+
return repoOwner;
|
|
63
|
+
}
|
|
64
|
+
const userOwner = normalizeText(pr?.head?.user?.login);
|
|
65
|
+
if (userOwner) {
|
|
66
|
+
return userOwner;
|
|
67
|
+
}
|
|
68
|
+
const headLabel = normalizeText(pr?.head?.label);
|
|
69
|
+
const separatorIndex = headLabel.indexOf(':');
|
|
70
|
+
if (separatorIndex > 0) {
|
|
71
|
+
return headLabel.slice(0, separatorIndex).trim();
|
|
72
|
+
}
|
|
73
|
+
return '';
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const getHeadRepoKey = (pr, fallbackRepoName) => {
|
|
77
|
+
const repoOwner = normalizeText(pr?.head?.repo?.owner?.login);
|
|
78
|
+
const repoName = normalizeText(pr?.head?.repo?.name);
|
|
79
|
+
if (repoOwner && repoName) {
|
|
80
|
+
return normalizeRepoKey(repoOwner, repoName);
|
|
81
|
+
}
|
|
82
|
+
const headLabel = normalizeText(pr?.head?.label);
|
|
83
|
+
const separatorIndex = headLabel.indexOf(':');
|
|
84
|
+
if (separatorIndex > 0) {
|
|
85
|
+
const labelOwner = headLabel.slice(0, separatorIndex).trim();
|
|
86
|
+
if (labelOwner && fallbackRepoName) {
|
|
87
|
+
return normalizeRepoKey(labelOwner, fallbackRepoName);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return '';
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const buildSourceMatcher = (sourceCandidates) => {
|
|
94
|
+
const repoRank = new Map();
|
|
95
|
+
const ownerRank = new Map();
|
|
96
|
+
|
|
97
|
+
sourceCandidates.forEach((candidate, index) => {
|
|
98
|
+
const repoKey = normalizeRepoKey(candidate.repo?.owner, candidate.repo?.repo);
|
|
99
|
+
if (repoKey && !repoRank.has(repoKey)) {
|
|
100
|
+
repoRank.set(repoKey, index);
|
|
101
|
+
}
|
|
102
|
+
const owner = normalizeLower(candidate.repo?.owner);
|
|
103
|
+
if (owner && !ownerRank.has(owner)) {
|
|
104
|
+
ownerRank.set(owner, index);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const matches = (pr, fallbackRepoName) => {
|
|
109
|
+
const repoKey = getHeadRepoKey(pr, fallbackRepoName);
|
|
110
|
+
if (repoKey && repoRank.has(repoKey)) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
const owner = normalizeLower(getHeadOwner(pr));
|
|
114
|
+
return Boolean(owner) && ownerRank.has(owner);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const compare = (left, right, fallbackRepoName) => {
|
|
118
|
+
const leftRepoRank = repoRank.get(getHeadRepoKey(left, fallbackRepoName));
|
|
119
|
+
const rightRepoRank = repoRank.get(getHeadRepoKey(right, fallbackRepoName));
|
|
120
|
+
const leftRepoScore = typeof leftRepoRank === 'number' ? leftRepoRank : Number.POSITIVE_INFINITY;
|
|
121
|
+
const rightRepoScore = typeof rightRepoRank === 'number' ? rightRepoRank : Number.POSITIVE_INFINITY;
|
|
122
|
+
if (leftRepoScore !== rightRepoScore) {
|
|
123
|
+
return leftRepoScore - rightRepoScore;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const leftOwnerRank = ownerRank.get(normalizeLower(getHeadOwner(left)));
|
|
127
|
+
const rightOwnerRank = ownerRank.get(normalizeLower(getHeadOwner(right)));
|
|
128
|
+
const leftOwnerScore = typeof leftOwnerRank === 'number' ? leftOwnerRank : Number.POSITIVE_INFINITY;
|
|
129
|
+
const rightOwnerScore = typeof rightOwnerRank === 'number' ? rightOwnerRank : Number.POSITIVE_INFINITY;
|
|
130
|
+
if (leftOwnerScore !== rightOwnerScore) {
|
|
131
|
+
return leftOwnerScore - rightOwnerScore;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return 0;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
return { matches, compare };
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const getRepoDefaultBranch = async (octokit, repo) => {
|
|
141
|
+
const repoKey = normalizeRepoKey(repo?.owner, repo?.repo);
|
|
142
|
+
if (!repoKey) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const cached = defaultBranchCache.get(repoKey);
|
|
147
|
+
if (cached && Date.now() - cached.fetchedAt < REPO_DEFAULT_BRANCH_TTL_MS) {
|
|
148
|
+
return cached.defaultBranch;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const response = await octokit.rest.repos.get({
|
|
153
|
+
owner: repo.owner,
|
|
154
|
+
repo: repo.repo,
|
|
155
|
+
});
|
|
156
|
+
const defaultBranch = normalizeText(response?.data?.default_branch) || null;
|
|
157
|
+
defaultBranchCache.set(repoKey, {
|
|
158
|
+
defaultBranch,
|
|
159
|
+
fetchedAt: Date.now(),
|
|
160
|
+
});
|
|
161
|
+
return defaultBranch;
|
|
162
|
+
} catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const getRepoMetadata = async (octokit, repo) => {
|
|
168
|
+
const repoKey = normalizeRepoKey(repo?.owner, repo?.repo);
|
|
169
|
+
if (!repoKey) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const cached = repoMetadataCache.get(repoKey);
|
|
174
|
+
if (cached && Date.now() - cached.fetchedAt < REPO_DEFAULT_BRANCH_TTL_MS) {
|
|
175
|
+
return cached.data;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const response = await octokit.rest.repos.get({
|
|
180
|
+
owner: repo.owner,
|
|
181
|
+
repo: repo.repo,
|
|
182
|
+
});
|
|
183
|
+
const data = response?.data ?? null;
|
|
184
|
+
repoMetadataCache.set(repoKey, {
|
|
185
|
+
data,
|
|
186
|
+
fetchedAt: Date.now(),
|
|
187
|
+
});
|
|
188
|
+
return data;
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (error?.status === 403 || error?.status === 404) {
|
|
191
|
+
repoMetadataCache.set(repoKey, {
|
|
192
|
+
data: null,
|
|
193
|
+
fetchedAt: Date.now(),
|
|
194
|
+
});
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const resolveRemoteCandidates = async (directory, rankedRemoteNames) => {
|
|
202
|
+
const results = [];
|
|
203
|
+
const seenRepoKeys = new Set();
|
|
204
|
+
|
|
205
|
+
for (const remoteName of rankedRemoteNames) {
|
|
206
|
+
const resolved = await resolveGitHubRepoFromDirectory(directory, remoteName).catch(() => ({ repo: null }));
|
|
207
|
+
const repo = resolved?.repo || null;
|
|
208
|
+
const repoKey = normalizeRepoKey(repo?.owner, repo?.repo);
|
|
209
|
+
if (!repo || !repoKey || seenRepoKeys.has(repoKey)) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
seenRepoKeys.add(repoKey);
|
|
213
|
+
results.push({
|
|
214
|
+
remoteName,
|
|
215
|
+
repo,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return results;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const expandRepoNetwork = async (octokit, candidates) => {
|
|
223
|
+
const expanded = [];
|
|
224
|
+
const seenRepoKeys = new Set();
|
|
225
|
+
|
|
226
|
+
const pushCandidate = (repo, remoteName, priority) => {
|
|
227
|
+
const repoKey = normalizeRepoKey(repo?.owner, repo?.repo);
|
|
228
|
+
if (!repoKey || seenRepoKeys.has(repoKey)) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
seenRepoKeys.add(repoKey);
|
|
232
|
+
expanded.push({ repo, remoteName, priority });
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
for (const candidate of candidates) {
|
|
236
|
+
const metadata = await getRepoMetadata(octokit, candidate.repo);
|
|
237
|
+
if (!metadata) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
pushCandidate(candidate.repo, candidate.remoteName, candidate.priority);
|
|
242
|
+
|
|
243
|
+
const parent = metadata?.parent;
|
|
244
|
+
if (parent?.owner?.login && parent?.name) {
|
|
245
|
+
pushCandidate({
|
|
246
|
+
owner: parent.owner.login,
|
|
247
|
+
repo: parent.name,
|
|
248
|
+
url: parent.html_url || `https://github.com/${parent.owner.login}/${parent.name}`,
|
|
249
|
+
}, candidate.remoteName, candidate.priority + 0.1);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const source = metadata?.source;
|
|
253
|
+
if (source?.owner?.login && source?.name) {
|
|
254
|
+
pushCandidate({
|
|
255
|
+
owner: source.owner.login,
|
|
256
|
+
repo: source.name,
|
|
257
|
+
url: source.html_url || `https://github.com/${source.owner.login}/${source.name}`,
|
|
258
|
+
}, candidate.remoteName, candidate.priority + 0.2);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return expanded.sort((left, right) => left.priority - right.priority);
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const safeListPulls = async (octokit, options) => {
|
|
266
|
+
try {
|
|
267
|
+
const response = await octokit.rest.pulls.list(options);
|
|
268
|
+
return Array.isArray(response?.data) ? response.data : [];
|
|
269
|
+
} catch (error) {
|
|
270
|
+
if (error?.status === 404 || error?.status === 403) {
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const parseRepoFromApiUrl = (value) => {
|
|
278
|
+
const normalized = normalizeText(value);
|
|
279
|
+
if (!normalized) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
const url = new URL(normalized);
|
|
284
|
+
const parts = url.pathname.replace(/^\/+/, '').split('/').filter(Boolean);
|
|
285
|
+
if (parts.length < 2 || parts[0] !== 'repos') {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
const owner = parts[1];
|
|
289
|
+
const repo = parts[2];
|
|
290
|
+
if (!owner || !repo) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
return { owner, repo };
|
|
294
|
+
} catch {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const searchFallbackPr = async ({ octokit, branch, repoNames }) => {
|
|
300
|
+
const normalizedRepoNames = new Set(repoNames.map((name) => normalizeLower(name)).filter(Boolean));
|
|
301
|
+
|
|
302
|
+
for (const state of ['open', 'closed']) {
|
|
303
|
+
let response;
|
|
304
|
+
try {
|
|
305
|
+
response = await octokit.rest.search.issuesAndPullRequests({
|
|
306
|
+
q: `is:pr state:${state} head:${branch}`,
|
|
307
|
+
per_page: 20,
|
|
308
|
+
});
|
|
309
|
+
} catch (error) {
|
|
310
|
+
if (error?.status === 403 || error?.status === 404) {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const items = Array.isArray(response?.data?.items) ? response.data.items : [];
|
|
317
|
+
for (const item of items) {
|
|
318
|
+
const repo = parseRepoFromApiUrl(item?.repository_url);
|
|
319
|
+
if (!repo) {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (normalizedRepoNames.size > 0 && !normalizedRepoNames.has(normalizeLower(repo.repo))) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
const prResponse = await octokit.rest.pulls.get({
|
|
327
|
+
owner: repo.owner,
|
|
328
|
+
repo: repo.repo,
|
|
329
|
+
pull_number: item.number,
|
|
330
|
+
});
|
|
331
|
+
const pr = prResponse?.data;
|
|
332
|
+
if (!pr || normalizeText(pr.head?.ref) !== branch) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
repo: {
|
|
337
|
+
owner: repo.owner,
|
|
338
|
+
repo: repo.repo,
|
|
339
|
+
url: `https://github.com/${repo.owner}/${repo.repo}`,
|
|
340
|
+
},
|
|
341
|
+
pr,
|
|
342
|
+
};
|
|
343
|
+
} catch (error) {
|
|
344
|
+
if (error?.status === 403 || error?.status === 404) {
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
throw error;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return null;
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const findFirstMatchingPr = async ({ octokit, target, branch, sourceCandidates }) => {
|
|
356
|
+
const matcher = buildSourceMatcher(sourceCandidates);
|
|
357
|
+
const sourceOwners = [];
|
|
358
|
+
sourceCandidates.forEach((candidate) => pushUnique(sourceOwners, candidate.repo?.owner));
|
|
359
|
+
|
|
360
|
+
const pickPreferred = (prs) => prs
|
|
361
|
+
.filter((pr) => normalizeText(pr?.head?.ref) === branch)
|
|
362
|
+
.filter((pr) => matcher.matches(pr, target.repo.repo))
|
|
363
|
+
.sort((left, right) => matcher.compare(left, right, target.repo.repo))[0] ?? null;
|
|
364
|
+
|
|
365
|
+
for (const state of ['open', 'closed']) {
|
|
366
|
+
for (const owner of sourceOwners) {
|
|
367
|
+
const directCandidates = await safeListPulls(octokit, {
|
|
368
|
+
owner: target.repo.owner,
|
|
369
|
+
repo: target.repo.repo,
|
|
370
|
+
state,
|
|
371
|
+
head: `${owner}:${branch}`,
|
|
372
|
+
per_page: 100,
|
|
373
|
+
});
|
|
374
|
+
const direct = pickPreferred(directCandidates);
|
|
375
|
+
if (direct) {
|
|
376
|
+
return direct;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const fallbackCandidates = await safeListPulls(octokit, {
|
|
381
|
+
owner: target.repo.owner,
|
|
382
|
+
repo: target.repo.repo,
|
|
383
|
+
state,
|
|
384
|
+
per_page: 100,
|
|
385
|
+
});
|
|
386
|
+
const fallback = pickPreferred(fallbackCandidates);
|
|
387
|
+
if (fallback) {
|
|
388
|
+
return fallback;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return null;
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
export async function resolveGitHubPrStatus({ octokit, directory, branch, remoteName }) {
|
|
396
|
+
const normalizedBranch = normalizeText(branch);
|
|
397
|
+
const normalizedRemoteName = normalizeText(remoteName) || 'origin';
|
|
398
|
+
|
|
399
|
+
const [status, remotes] = await Promise.all([
|
|
400
|
+
getStatus(directory).catch(() => null),
|
|
401
|
+
getRemotes(directory).catch(() => []),
|
|
402
|
+
]);
|
|
403
|
+
|
|
404
|
+
const trackingRemoteName = parseTrackingRemoteName(status?.tracking);
|
|
405
|
+
const rankedRemoteNames = rankRemoteNames(
|
|
406
|
+
Array.isArray(remotes) ? remotes.map((remote) => remote?.name).filter(Boolean) : [],
|
|
407
|
+
normalizedRemoteName,
|
|
408
|
+
trackingRemoteName,
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
const resolvedRemoteTargets = await resolveRemoteCandidates(directory, rankedRemoteNames.slice(0, 3));
|
|
412
|
+
const resolvedTargets = await expandRepoNetwork(
|
|
413
|
+
octokit,
|
|
414
|
+
resolvedRemoteTargets.map((target, index) => ({ ...target, priority: index })),
|
|
415
|
+
);
|
|
416
|
+
if (resolvedTargets.length === 0) {
|
|
417
|
+
return {
|
|
418
|
+
repo: null,
|
|
419
|
+
pr: null,
|
|
420
|
+
defaultBranch: null,
|
|
421
|
+
resolvedRemoteName: null,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const sourceCandidates = resolvedTargets.slice();
|
|
426
|
+
|
|
427
|
+
let fallbackRepo = resolvedTargets[0].repo;
|
|
428
|
+
let fallbackRemoteName = resolvedTargets[0].remoteName;
|
|
429
|
+
let fallbackDefaultBranch = await getRepoDefaultBranch(octokit, fallbackRepo);
|
|
430
|
+
|
|
431
|
+
for (const target of resolvedTargets) {
|
|
432
|
+
const defaultBranch = await getRepoDefaultBranch(octokit, target.repo);
|
|
433
|
+
if (!fallbackRepo) {
|
|
434
|
+
fallbackRepo = target.repo;
|
|
435
|
+
fallbackRemoteName = target.remoteName;
|
|
436
|
+
fallbackDefaultBranch = defaultBranch;
|
|
437
|
+
}
|
|
438
|
+
if (defaultBranch && defaultBranch === normalizedBranch) {
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const pr = await findFirstMatchingPr({
|
|
443
|
+
octokit,
|
|
444
|
+
target,
|
|
445
|
+
branch: normalizedBranch,
|
|
446
|
+
sourceCandidates,
|
|
447
|
+
});
|
|
448
|
+
if (pr) {
|
|
449
|
+
return {
|
|
450
|
+
repo: target.repo,
|
|
451
|
+
pr,
|
|
452
|
+
defaultBranch,
|
|
453
|
+
resolvedRemoteName: target.remoteName,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const fallbackSearch = await searchFallbackPr({
|
|
459
|
+
octokit,
|
|
460
|
+
branch: normalizedBranch,
|
|
461
|
+
repoNames: resolvedTargets.map((target) => target.repo.repo),
|
|
462
|
+
});
|
|
463
|
+
if (fallbackSearch) {
|
|
464
|
+
return {
|
|
465
|
+
repo: fallbackSearch.repo,
|
|
466
|
+
pr: fallbackSearch.pr,
|
|
467
|
+
defaultBranch: await getRepoDefaultBranch(octokit, fallbackSearch.repo),
|
|
468
|
+
resolvedRemoteName: null,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return {
|
|
473
|
+
repo: fallbackRepo,
|
|
474
|
+
pr: null,
|
|
475
|
+
defaultBranch: fallbackDefaultBranch,
|
|
476
|
+
resolvedRemoteName: fallbackRemoteName,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { getRemoteUrl } from '../../git/index.js';
|
|
2
|
+
|
|
3
|
+
export const parseGitHubRemoteUrl = (raw) => {
|
|
4
|
+
if (typeof raw !== 'string') {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
const value = raw.trim();
|
|
8
|
+
if (!value) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// git@github.com:OWNER/REPO.git
|
|
13
|
+
if (value.startsWith('git@github.com:')) {
|
|
14
|
+
const rest = value.slice('git@github.com:'.length);
|
|
15
|
+
const cleaned = rest.endsWith('.git') ? rest.slice(0, -4) : rest;
|
|
16
|
+
const [owner, repo] = cleaned.split('/');
|
|
17
|
+
if (!owner || !repo) return null;
|
|
18
|
+
return { owner, repo, url: `https://github.com/${owner}/${repo}` };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ssh://git@github.com/OWNER/REPO.git
|
|
22
|
+
if (value.startsWith('ssh://git@github.com/')) {
|
|
23
|
+
const rest = value.slice('ssh://git@github.com/'.length);
|
|
24
|
+
const cleaned = rest.endsWith('.git') ? rest.slice(0, -4) : rest;
|
|
25
|
+
const [owner, repo] = cleaned.split('/');
|
|
26
|
+
if (!owner || !repo) return null;
|
|
27
|
+
return { owner, repo, url: `https://github.com/${owner}/${repo}` };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// https://github.com/OWNER/REPO(.git)
|
|
31
|
+
try {
|
|
32
|
+
const url = new URL(value);
|
|
33
|
+
if (url.hostname !== 'github.com') {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const path = url.pathname.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
37
|
+
const cleaned = path.endsWith('.git') ? path.slice(0, -4) : path;
|
|
38
|
+
const [owner, repo] = cleaned.split('/');
|
|
39
|
+
if (!owner || !repo) return null;
|
|
40
|
+
return { owner, repo, url: `https://github.com/${owner}/${repo}` };
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export async function resolveGitHubRepoFromDirectory(directory, remoteName = 'origin') {
|
|
47
|
+
const remoteUrl = await getRemoteUrl(directory, remoteName).catch(() => null);
|
|
48
|
+
if (!remoteUrl) {
|
|
49
|
+
return { repo: null, remoteUrl: null };
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
repo: parseGitHubRemoteUrl(remoteUrl),
|
|
53
|
+
remoteUrl,
|
|
54
|
+
};
|
|
55
|
+
}
|