heyhank 0.1.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 +40 -0
- package/bin/cli.ts +168 -0
- package/bin/ctl.ts +528 -0
- package/bin/generate-token.ts +28 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/AgentsPage-BPhirnCe.js +7 -0
- package/dist/assets/AssistantPage-DJ-cMQfb.js +1 -0
- package/dist/assets/CronManager-DDbz-yiT.js +1 -0
- package/dist/assets/HelpPage-DMfkzERp.js +1 -0
- package/dist/assets/IntegrationsPage-CrOitCmJ.js +1 -0
- package/dist/assets/MediaPage-CE5rdvkC.js +1 -0
- package/dist/assets/PlatformDashboard-Do6F0O2p.js +1 -0
- package/dist/assets/Playground-Fc5cdc5p.js +109 -0
- package/dist/assets/ProcessPanel-CslEiZkI.js +2 -0
- package/dist/assets/PromptsPage-D2EhsdNO.js +4 -0
- package/dist/assets/RunsPage-C5BZF5Rx.js +1 -0
- package/dist/assets/SandboxManager-a1AVI5q2.js +8 -0
- package/dist/assets/SettingsPage-DirhjQrJ.js +51 -0
- package/dist/assets/SocialMediaPage-DBuM28vD.js +1 -0
- package/dist/assets/TailscalePage-CHiFhZXF.js +1 -0
- package/dist/assets/TelephonyPage-x0VV0fOo.js +1 -0
- package/dist/assets/TerminalPage-Drwyrnfd.js +1 -0
- package/dist/assets/gemini-audio-t-TSU-To.js +17 -0
- package/dist/assets/gemini-live-client-C7rqAW7G.js +166 -0
- package/dist/assets/index-C8M_PUmX.css +32 -0
- package/dist/assets/index-CEqZnThB.js +204 -0
- package/dist/assets/sw-register-LSSpj6RU.js +1 -0
- package/dist/assets/time-ago-B6r_l9u1.js +1 -0
- package/dist/assets/workbox-window.prod.es5-BIl4cyR9.js +2 -0
- package/dist/favicon-32-original.png +0 -0
- package/dist/favicon-32.png +0 -0
- package/dist/favicon.ico +0 -0
- package/dist/favicon.svg +8 -0
- package/dist/fonts/MesloLGSNerdFontMono-Bold.woff2 +0 -0
- package/dist/fonts/MesloLGSNerdFontMono-Regular.woff2 +0 -0
- package/dist/heyhank-mascot-poster.png +0 -0
- package/dist/heyhank-mascot.mp4 +0 -0
- package/dist/heyhank-mascot.webm +0 -0
- package/dist/icon-192-original.png +0 -0
- package/dist/icon-192.png +0 -0
- package/dist/icon-512-original.png +0 -0
- package/dist/icon-512.png +0 -0
- package/dist/index.html +21 -0
- package/dist/logo-192.png +0 -0
- package/dist/logo-512.png +0 -0
- package/dist/logo-codex.svg +14 -0
- package/dist/logo-docker.svg +4 -0
- package/dist/logo-original.png +0 -0
- package/dist/logo.png +0 -0
- package/dist/logo.svg +14 -0
- package/dist/manifest.json +24 -0
- package/dist/push-sw.js +34 -0
- package/dist/sw.js +1 -0
- package/dist/workbox-d2a0910a.js +1 -0
- package/package.json +109 -0
- package/server/agent-cron-migrator.ts +85 -0
- package/server/agent-executor.ts +357 -0
- package/server/agent-store.ts +185 -0
- package/server/agent-timeout.ts +107 -0
- package/server/agent-types.ts +122 -0
- package/server/ai-validation-settings.ts +37 -0
- package/server/ai-validator.ts +181 -0
- package/server/anthropic-provider-migration.ts +48 -0
- package/server/assistant-store.ts +272 -0
- package/server/auth-manager.ts +150 -0
- package/server/auto-approve.ts +153 -0
- package/server/auto-namer.ts +36 -0
- package/server/backend-adapter.ts +54 -0
- package/server/cache-headers.ts +61 -0
- package/server/calendar-service.ts +434 -0
- package/server/claude-adapter.ts +889 -0
- package/server/claude-container-auth.ts +30 -0
- package/server/claude-session-discovery.ts +157 -0
- package/server/claude-session-history.ts +410 -0
- package/server/cli-launcher.ts +1303 -0
- package/server/codex-adapter.ts +3027 -0
- package/server/codex-container-auth.ts +24 -0
- package/server/codex-home.ts +27 -0
- package/server/codex-ws-proxy.cjs +226 -0
- package/server/commands-discovery.ts +81 -0
- package/server/constants.ts +7 -0
- package/server/container-manager.ts +1053 -0
- package/server/cost-tracker.ts +222 -0
- package/server/cron-scheduler.ts +243 -0
- package/server/cron-store.ts +148 -0
- package/server/cron-types.ts +63 -0
- package/server/email-service.ts +354 -0
- package/server/env-manager.ts +161 -0
- package/server/event-bus-types.ts +75 -0
- package/server/event-bus.ts +124 -0
- package/server/execution-store.ts +170 -0
- package/server/federation/node-connection.ts +190 -0
- package/server/federation/node-manager.ts +366 -0
- package/server/federation/node-store.ts +86 -0
- package/server/federation/node-types.ts +121 -0
- package/server/fs-utils.ts +15 -0
- package/server/git-utils.ts +421 -0
- package/server/github-pr.ts +379 -0
- package/server/google-media.ts +342 -0
- package/server/image-pull-manager.ts +279 -0
- package/server/index.ts +491 -0
- package/server/internal-ai.ts +237 -0
- package/server/kill-switch.ts +99 -0
- package/server/llm-providers.ts +342 -0
- package/server/logger.ts +259 -0
- package/server/mcp-registry.ts +401 -0
- package/server/message-bus.ts +271 -0
- package/server/message-delivery.ts +128 -0
- package/server/metrics-collector.ts +350 -0
- package/server/metrics-types.ts +108 -0
- package/server/middleware/managed-auth.ts +195 -0
- package/server/novnc-proxy.ts +99 -0
- package/server/path-resolver.ts +186 -0
- package/server/paths.ts +13 -0
- package/server/pr-poller.ts +162 -0
- package/server/prompt-manager.ts +211 -0
- package/server/protocol/claude-upstream/README.md +19 -0
- package/server/protocol/claude-upstream/sdk.d.ts.txt +1943 -0
- package/server/protocol/codex-upstream/ClientNotification.ts.txt +5 -0
- package/server/protocol/codex-upstream/ClientRequest.ts.txt +60 -0
- package/server/protocol/codex-upstream/README.md +18 -0
- package/server/protocol/codex-upstream/ServerNotification.ts.txt +41 -0
- package/server/protocol/codex-upstream/ServerRequest.ts.txt +16 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallParams.ts.txt +6 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallResponse.ts.txt +6 -0
- package/server/protocol-monitor.ts +50 -0
- package/server/provider-manager.ts +111 -0
- package/server/provider-registry.ts +393 -0
- package/server/push-notifications.ts +221 -0
- package/server/recorder.ts +374 -0
- package/server/recording-hub/compat-validator.ts +284 -0
- package/server/recording-hub/diagnostics.ts +299 -0
- package/server/recording-hub/hub-config.ts +19 -0
- package/server/recording-hub/hub-routes.ts +236 -0
- package/server/recording-hub/hub-store.ts +265 -0
- package/server/recording-hub/replay-adapter.ts +207 -0
- package/server/relay-client.ts +320 -0
- package/server/reminder-scheduler.ts +38 -0
- package/server/replay.ts +78 -0
- package/server/routes/agent-routes.ts +264 -0
- package/server/routes/assistant-routes.ts +90 -0
- package/server/routes/cron-routes.ts +103 -0
- package/server/routes/env-routes.ts +95 -0
- package/server/routes/federation-routes.ts +76 -0
- package/server/routes/fs-routes.ts +622 -0
- package/server/routes/git-routes.ts +97 -0
- package/server/routes/llm-routes.ts +166 -0
- package/server/routes/media-routes.ts +135 -0
- package/server/routes/metrics-routes.ts +13 -0
- package/server/routes/platform-routes.ts +1379 -0
- package/server/routes/prompt-routes.ts +67 -0
- package/server/routes/provider-routes.ts +109 -0
- package/server/routes/sandbox-routes.ts +127 -0
- package/server/routes/settings-routes.ts +285 -0
- package/server/routes/skills-routes.ts +100 -0
- package/server/routes/socialmedia-routes.ts +208 -0
- package/server/routes/system-routes.ts +228 -0
- package/server/routes/tailscale-routes.ts +22 -0
- package/server/routes/telephony-routes.ts +259 -0
- package/server/routes.ts +1379 -0
- package/server/sandbox-manager.ts +168 -0
- package/server/service.ts +718 -0
- package/server/session-creation-service.ts +457 -0
- package/server/session-git-info.ts +104 -0
- package/server/session-names.ts +67 -0
- package/server/session-orchestrator.ts +824 -0
- package/server/session-state-machine.ts +207 -0
- package/server/session-store.ts +146 -0
- package/server/session-types.ts +511 -0
- package/server/settings-manager.ts +149 -0
- package/server/shared-context.ts +157 -0
- package/server/socialmedia/adapter.ts +15 -0
- package/server/socialmedia/adapters/ayrshare-adapter.ts +169 -0
- package/server/socialmedia/adapters/buffer-adapter.ts +299 -0
- package/server/socialmedia/adapters/postiz-adapter.ts +298 -0
- package/server/socialmedia/manager.ts +227 -0
- package/server/socialmedia/store.ts +98 -0
- package/server/socialmedia/types.ts +89 -0
- package/server/tailscale-manager.ts +451 -0
- package/server/telephony/audio-bridge.ts +331 -0
- package/server/telephony/call-manager.ts +457 -0
- package/server/telephony/call-types.ts +108 -0
- package/server/telephony/telephony-store.ts +119 -0
- package/server/terminal-manager.ts +240 -0
- package/server/update-checker.ts +192 -0
- package/server/usage-limits.ts +225 -0
- package/server/web-push.d.ts +51 -0
- package/server/worktree-tracker.ts +84 -0
- package/server/ws-auth.ts +41 -0
- package/server/ws-bridge-browser-ingest.ts +72 -0
- package/server/ws-bridge-browser.ts +112 -0
- package/server/ws-bridge-cli-ingest.ts +81 -0
- package/server/ws-bridge-codex.ts +266 -0
- package/server/ws-bridge-controls.ts +20 -0
- package/server/ws-bridge-persist.ts +66 -0
- package/server/ws-bridge-publish.ts +79 -0
- package/server/ws-bridge-replay.ts +61 -0
- package/server/ws-bridge-types.ts +121 -0
- package/server/ws-bridge.ts +1240 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export interface GitHubCheckStatus {
|
|
6
|
+
name: string;
|
|
7
|
+
status: string;
|
|
8
|
+
conclusion: string | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface GitHubPRInfo {
|
|
12
|
+
number: number;
|
|
13
|
+
title: string;
|
|
14
|
+
url: string;
|
|
15
|
+
state: "OPEN" | "CLOSED" | "MERGED";
|
|
16
|
+
isDraft: boolean;
|
|
17
|
+
reviewDecision: "APPROVED" | "CHANGES_REQUESTED" | "REVIEW_REQUIRED" | null;
|
|
18
|
+
additions: number;
|
|
19
|
+
deletions: number;
|
|
20
|
+
changedFiles: number;
|
|
21
|
+
checks: GitHubCheckStatus[];
|
|
22
|
+
checksSummary: {
|
|
23
|
+
total: number;
|
|
24
|
+
success: number;
|
|
25
|
+
failure: number;
|
|
26
|
+
pending: number;
|
|
27
|
+
};
|
|
28
|
+
reviewThreads: {
|
|
29
|
+
total: number;
|
|
30
|
+
resolved: number;
|
|
31
|
+
unresolved: number;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface PRStatusResponse {
|
|
36
|
+
available: boolean;
|
|
37
|
+
pr: GitHubPRInfo | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── gh CLI Detection ────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
let _ghAvailable: boolean | null = null;
|
|
43
|
+
|
|
44
|
+
export function isGhAvailable(): boolean {
|
|
45
|
+
if (_ghAvailable !== null) return _ghAvailable;
|
|
46
|
+
try {
|
|
47
|
+
execSync("which gh", { stdio: "pipe", timeout: 5_000 });
|
|
48
|
+
_ghAvailable = true;
|
|
49
|
+
} catch {
|
|
50
|
+
_ghAvailable = false;
|
|
51
|
+
}
|
|
52
|
+
return _ghAvailable;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Exported for testing
|
|
56
|
+
export function _resetGhAvailable() {
|
|
57
|
+
_ghAvailable = null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ─── Repo Slug Resolution ────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
const repoSlugCache = new Map<string, { slug: string | null; timestamp: number }>();
|
|
63
|
+
const REPO_SLUG_TTL = 5 * 60_000; // 5 minutes
|
|
64
|
+
|
|
65
|
+
function getRepoSlug(cwd: string): string | null {
|
|
66
|
+
const cached = repoSlugCache.get(cwd);
|
|
67
|
+
if (cached && Date.now() - cached.timestamp < REPO_SLUG_TTL) {
|
|
68
|
+
return cached.slug;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const slug = execSync("gh repo view --json nameWithOwner --jq .nameWithOwner", {
|
|
72
|
+
cwd,
|
|
73
|
+
stdio: "pipe",
|
|
74
|
+
timeout: 10_000,
|
|
75
|
+
})
|
|
76
|
+
.toString()
|
|
77
|
+
.trim();
|
|
78
|
+
const result = slug || null;
|
|
79
|
+
repoSlugCache.set(cwd, { slug: result, timestamp: Date.now() });
|
|
80
|
+
return result;
|
|
81
|
+
} catch {
|
|
82
|
+
repoSlugCache.set(cwd, { slug: null, timestamp: Date.now() });
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function getRepoSlugAsync(cwd: string): Promise<string | null> {
|
|
88
|
+
const cached = repoSlugCache.get(cwd);
|
|
89
|
+
if (cached && Date.now() - cached.timestamp < REPO_SLUG_TTL) {
|
|
90
|
+
return cached.slug;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const proc = Bun.spawn(
|
|
94
|
+
["gh", "repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"],
|
|
95
|
+
{ cwd, stdout: "pipe", stderr: "pipe" },
|
|
96
|
+
);
|
|
97
|
+
const timeout = setTimeout(() => proc.kill(), 10_000);
|
|
98
|
+
const exitCode = await proc.exited;
|
|
99
|
+
clearTimeout(timeout);
|
|
100
|
+
if (exitCode !== 0) {
|
|
101
|
+
repoSlugCache.set(cwd, { slug: null, timestamp: Date.now() });
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
const slug = (await new Response(proc.stdout).text()).trim();
|
|
105
|
+
const result = slug || null;
|
|
106
|
+
repoSlugCache.set(cwd, { slug: result, timestamp: Date.now() });
|
|
107
|
+
return result;
|
|
108
|
+
} catch {
|
|
109
|
+
repoSlugCache.set(cwd, { slug: null, timestamp: Date.now() });
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─── PR Data Cache ───────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
const prCache = new Map<string, { data: GitHubPRInfo | null; timestamp: number; ttl: number }>();
|
|
117
|
+
const PR_CACHE_TTL = 30_000; // 30 seconds (default / legacy)
|
|
118
|
+
|
|
119
|
+
// Exported for testing
|
|
120
|
+
export function _clearCaches() {
|
|
121
|
+
prCache.clear();
|
|
122
|
+
repoSlugCache.clear();
|
|
123
|
+
_ghAvailable = null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ─── Adaptive TTL ───────────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
/** Compute polling interval based on PR state. */
|
|
129
|
+
export function computeAdaptiveTTL(pr: GitHubPRInfo | null): number {
|
|
130
|
+
if (!pr) return 60_000; // No PR found — check again in 60s
|
|
131
|
+
|
|
132
|
+
// Merged or closed — terminal state, rarely changes
|
|
133
|
+
if (pr.state === "MERGED" || pr.state === "CLOSED") return 300_000; // 5 minutes
|
|
134
|
+
|
|
135
|
+
// CI actively running (pending checks) — user is watching
|
|
136
|
+
if (pr.checksSummary.pending > 0) return 10_000; // 10 seconds
|
|
137
|
+
|
|
138
|
+
// CI failed — user likely pushing fixes
|
|
139
|
+
if (pr.checksSummary.failure > 0) return 30_000; // 30 seconds
|
|
140
|
+
|
|
141
|
+
// Changes requested — moderate frequency
|
|
142
|
+
if (pr.reviewDecision === "CHANGES_REQUESTED") return 30_000; // 30 seconds
|
|
143
|
+
|
|
144
|
+
// Approved, all checks passed — stable
|
|
145
|
+
if (pr.reviewDecision === "APPROVED" && pr.checksSummary.pending === 0) return 120_000; // 2 minutes
|
|
146
|
+
|
|
147
|
+
// Review pending, checks passed — waiting on human reviewer
|
|
148
|
+
if (pr.reviewDecision === "REVIEW_REQUIRED" || pr.reviewDecision === null) return 45_000; // 45 seconds
|
|
149
|
+
|
|
150
|
+
// Default fallback
|
|
151
|
+
return 30_000;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ─── GraphQL Query ───────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
const PR_QUERY = `
|
|
157
|
+
query($owner: String!, $name: String!, $branch: String!) {
|
|
158
|
+
repository(owner: $owner, name: $name) {
|
|
159
|
+
pullRequests(headRefName: $branch, first: 5, orderBy: {field: UPDATED_AT, direction: DESC}, states: [OPEN, MERGED]) {
|
|
160
|
+
nodes {
|
|
161
|
+
number
|
|
162
|
+
title
|
|
163
|
+
url
|
|
164
|
+
state
|
|
165
|
+
isDraft
|
|
166
|
+
isCrossRepository
|
|
167
|
+
reviewDecision
|
|
168
|
+
additions
|
|
169
|
+
deletions
|
|
170
|
+
changedFiles
|
|
171
|
+
reviewThreads(first: 100) {
|
|
172
|
+
totalCount
|
|
173
|
+
nodes {
|
|
174
|
+
isResolved
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
commits(last: 1) {
|
|
178
|
+
nodes {
|
|
179
|
+
commit {
|
|
180
|
+
statusCheckRollup {
|
|
181
|
+
contexts(first: 50) {
|
|
182
|
+
nodes {
|
|
183
|
+
__typename
|
|
184
|
+
... on CheckRun {
|
|
185
|
+
name
|
|
186
|
+
status
|
|
187
|
+
conclusion
|
|
188
|
+
}
|
|
189
|
+
... on StatusContext {
|
|
190
|
+
context
|
|
191
|
+
state
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}`;
|
|
203
|
+
|
|
204
|
+
// ─── Response Parsing ────────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
interface GraphQLCheckRunNode {
|
|
207
|
+
__typename: "CheckRun";
|
|
208
|
+
name: string;
|
|
209
|
+
status: string;
|
|
210
|
+
conclusion: string | null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
interface GraphQLStatusContextNode {
|
|
214
|
+
__typename: "StatusContext";
|
|
215
|
+
context: string;
|
|
216
|
+
state: string;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
type GraphQLContextNode = GraphQLCheckRunNode | GraphQLStatusContextNode;
|
|
220
|
+
|
|
221
|
+
export function parseGraphQLResponse(data: unknown): GitHubPRInfo | null {
|
|
222
|
+
try {
|
|
223
|
+
const repo = (data as any)?.data?.repository;
|
|
224
|
+
const nodes = repo?.pullRequests?.nodes;
|
|
225
|
+
if (!nodes || nodes.length === 0) return null;
|
|
226
|
+
|
|
227
|
+
// Filter out cross-repository (fork) PRs — we only want same-repo PRs
|
|
228
|
+
const sameRepoPRs = nodes.filter((n: any) => !n.isCrossRepository);
|
|
229
|
+
if (sameRepoPRs.length === 0) return null;
|
|
230
|
+
|
|
231
|
+
const pr = sameRepoPRs[0];
|
|
232
|
+
|
|
233
|
+
// Normalize checks
|
|
234
|
+
const rawContexts: GraphQLContextNode[] =
|
|
235
|
+
pr.commits?.nodes?.[0]?.commit?.statusCheckRollup?.contexts?.nodes ?? [];
|
|
236
|
+
|
|
237
|
+
const checks: GitHubCheckStatus[] = rawContexts.map((node) => {
|
|
238
|
+
if (node.__typename === "CheckRun") {
|
|
239
|
+
return {
|
|
240
|
+
name: node.name,
|
|
241
|
+
status: node.status,
|
|
242
|
+
conclusion: node.conclusion,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
// StatusContext
|
|
246
|
+
return {
|
|
247
|
+
name: node.context,
|
|
248
|
+
status: node.state === "PENDING" ? "IN_PROGRESS" : "COMPLETED",
|
|
249
|
+
conclusion: node.state === "SUCCESS" ? "SUCCESS" : (node.state === "FAILURE" || node.state === "ERROR") ? "FAILURE" : null,
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Compute summary
|
|
254
|
+
let success = 0;
|
|
255
|
+
let failure = 0;
|
|
256
|
+
let pending = 0;
|
|
257
|
+
for (const check of checks) {
|
|
258
|
+
if (check.conclusion === "SUCCESS" || check.conclusion === "NEUTRAL" || check.conclusion === "SKIPPED") {
|
|
259
|
+
success++;
|
|
260
|
+
} else if (check.conclusion === "FAILURE" || check.conclusion === "CANCELLED" || check.conclusion === "TIMED_OUT") {
|
|
261
|
+
failure++;
|
|
262
|
+
} else {
|
|
263
|
+
pending++;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Compute review threads
|
|
268
|
+
const threadNodes: { isResolved: boolean }[] = pr.reviewThreads?.nodes ?? [];
|
|
269
|
+
const resolved = threadNodes.filter((t) => t.isResolved).length;
|
|
270
|
+
const unresolved = threadNodes.filter((t) => !t.isResolved).length;
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
number: pr.number,
|
|
274
|
+
title: pr.title,
|
|
275
|
+
url: pr.url,
|
|
276
|
+
state: pr.state,
|
|
277
|
+
isDraft: pr.isDraft ?? false,
|
|
278
|
+
reviewDecision: pr.reviewDecision || null,
|
|
279
|
+
additions: pr.additions ?? 0,
|
|
280
|
+
deletions: pr.deletions ?? 0,
|
|
281
|
+
changedFiles: pr.changedFiles ?? 0,
|
|
282
|
+
checks,
|
|
283
|
+
checksSummary: { total: checks.length, success, failure, pending },
|
|
284
|
+
reviewThreads: {
|
|
285
|
+
total: pr.reviewThreads?.totalCount ?? 0,
|
|
286
|
+
resolved,
|
|
287
|
+
unresolved,
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
} catch {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ─── Main Fetch Function (sync — legacy, used by tests) ─────────────────────
|
|
296
|
+
|
|
297
|
+
export async function fetchPRInfo(cwd: string, branch: string): Promise<GitHubPRInfo | null> {
|
|
298
|
+
if (!isGhAvailable()) return null;
|
|
299
|
+
|
|
300
|
+
const cacheKey = `${cwd}:${branch}`;
|
|
301
|
+
const cached = prCache.get(cacheKey);
|
|
302
|
+
if (cached && Date.now() - cached.timestamp < cached.ttl) {
|
|
303
|
+
return cached.data;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const slug = getRepoSlug(cwd);
|
|
307
|
+
if (!slug) {
|
|
308
|
+
prCache.set(cacheKey, { data: null, timestamp: Date.now(), ttl: PR_CACHE_TTL });
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const [owner, name] = slug.split("/");
|
|
313
|
+
if (!owner || !name) return null;
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
const result = execFileSync(
|
|
317
|
+
"gh",
|
|
318
|
+
["api", "graphql", "-f", `query=${PR_QUERY}`, "-f", `owner=${owner}`, "-f", `name=${name}`, "-f", `branch=${branch}`],
|
|
319
|
+
{ cwd, stdio: "pipe", timeout: 15_000 },
|
|
320
|
+
)
|
|
321
|
+
.toString()
|
|
322
|
+
.trim();
|
|
323
|
+
|
|
324
|
+
const parsed = JSON.parse(result);
|
|
325
|
+
const prInfo = parseGraphQLResponse(parsed);
|
|
326
|
+
const ttl = computeAdaptiveTTL(prInfo);
|
|
327
|
+
prCache.set(cacheKey, { data: prInfo, timestamp: Date.now(), ttl });
|
|
328
|
+
return prInfo;
|
|
329
|
+
} catch {
|
|
330
|
+
prCache.set(cacheKey, { data: null, timestamp: Date.now(), ttl: PR_CACHE_TTL });
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ─── Async Fetch Function (non-blocking, uses Bun.spawn) ────────────────────
|
|
336
|
+
|
|
337
|
+
export async function fetchPRInfoAsync(cwd: string, branch: string): Promise<GitHubPRInfo | null> {
|
|
338
|
+
if (!isGhAvailable()) return null;
|
|
339
|
+
|
|
340
|
+
const cacheKey = `${cwd}:${branch}`;
|
|
341
|
+
const cached = prCache.get(cacheKey);
|
|
342
|
+
if (cached && Date.now() - cached.timestamp < cached.ttl) {
|
|
343
|
+
return cached.data;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const slug = await getRepoSlugAsync(cwd);
|
|
347
|
+
if (!slug) {
|
|
348
|
+
prCache.set(cacheKey, { data: null, timestamp: Date.now(), ttl: 60_000 });
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const [owner, name] = slug.split("/");
|
|
353
|
+
if (!owner || !name) return null;
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
const proc = Bun.spawn(
|
|
357
|
+
["gh", "api", "graphql", "-f", `query=${PR_QUERY}`, "-f", `owner=${owner}`, "-f", `name=${name}`, "-f", `branch=${branch}`],
|
|
358
|
+
{ cwd, stdout: "pipe", stderr: "pipe" },
|
|
359
|
+
);
|
|
360
|
+
const timeout = setTimeout(() => proc.kill(), 15_000);
|
|
361
|
+
const exitCode = await proc.exited;
|
|
362
|
+
clearTimeout(timeout);
|
|
363
|
+
|
|
364
|
+
if (exitCode !== 0) {
|
|
365
|
+
prCache.set(cacheKey, { data: null, timestamp: Date.now(), ttl: PR_CACHE_TTL });
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const stdout = (await new Response(proc.stdout).text()).trim();
|
|
370
|
+
const parsed = JSON.parse(stdout);
|
|
371
|
+
const prInfo = parseGraphQLResponse(parsed);
|
|
372
|
+
const ttl = computeAdaptiveTTL(prInfo);
|
|
373
|
+
prCache.set(cacheKey, { data: prInfo, timestamp: Date.now(), ttl });
|
|
374
|
+
return prInfo;
|
|
375
|
+
} catch {
|
|
376
|
+
prCache.set(cacheKey, { data: null, timestamp: Date.now(), ttl: PR_CACHE_TTL });
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
}
|