-
Notifications
You must be signed in to change notification settings - Fork 54
Expand file tree
/
Copy pathrtk-binary.mjs
More file actions
191 lines (173 loc) · 6.4 KB
/
Copy pathrtk-binary.mjs
File metadata and controls
191 lines (173 loc) · 6.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
import { createHash } from "node:crypto";
import {
chmodSync,
copyFileSync,
createReadStream,
createWriteStream,
existsSync,
mkdirSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs";
import { dirname, join, resolve } from "node:path";
import { pipeline } from "node:stream/promises";
import { setTimeout as sleep } from "node:timers/promises";
import { fileURLToPath } from "node:url";
import { unzipSync } from "fflate";
import { extract } from "tar";
import { targetArch, targetPlatform } from "./native-binary.mjs";
/**
* Shared build-time helper for vendoring the RTK binary
* (https://github.com/rtk-ai/rtk) into the agent package's `dist/rtk/`.
*
* RTK compresses the output of common dev commands before it reaches the model
* (see `src/adapters/claude/session/rtk.ts`). We pin a single version here so
* every host — desktop and cloud runs — uses the same RTK, rather than
* depending on whatever happens to be on the machine's PATH.
*
* There is no npm package for RTK, so we download the pinned release from
* GitHub at build time and cache it under `node_modules/.cache`. Bundling is
* best-effort: if the download fails (e.g. offline build) the caller warns and
* continues, and the runtime resolver falls back to PATH.
*/
export const RTK_VERSION = "0.43.0";
const __dirname = dirname(fileURLToPath(import.meta.url));
// Rust target triples for the RTK GitHub release assets. Linux x64 ships musl
// (static, portable); linux arm64 ships gnu — RTK publishes no arm64 musl or
// Windows arm64 asset, so those combinations resolve to undefined and skip.
const RTK_TARGETS = {
darwin: { arm64: "aarch64-apple-darwin", x64: "x86_64-apple-darwin" },
linux: {
arm64: "aarch64-unknown-linux-gnu",
x64: "x86_64-unknown-linux-musl",
},
win32: { x64: "x86_64-pc-windows-msvc" },
};
// SHA-256 of each release archive (from the release's checksums.txt), pinned
// alongside RTK_VERSION so a swapped release asset or a corrupt/truncated
// download fails the build instead of being cached and bundled. Update both
// RTK_VERSION and these hashes together when bumping rtk.
const RTK_SHA256 = {
"aarch64-apple-darwin":
"8a17e49acbd378997eb21d0eb6f7f861111f35b4fc9b1c74edf4c7448e576c65",
"aarch64-unknown-linux-gnu":
"5519f7ca12e5c143a609f0d28a0a77b97413a8dce31c2681f1a41c24519a8731",
"x86_64-apple-darwin":
"a85f60e2637811be68366208b8d8b9c5ba1b748cb5df4477ab20cd73d3c5d9f8",
"x86_64-pc-windows-msvc":
"7c5e4a2ef816a4d4ed947ddd74ca3df851fc39ea87d49a3ca2bf3abc515a016b",
"x86_64-unknown-linux-musl":
"ff8a1e7766496e175291a85aeca1dc97c9ff6df33e51e5893d1fbc78fea2a609",
};
export function rtkBinName(platform = targetPlatform()) {
return platform === "win32" ? "rtk.exe" : "rtk";
}
export function rtkReleaseTarget(
platform = targetPlatform(),
arch = targetArch(),
) {
return RTK_TARGETS[platform]?.[arch];
}
function rtkAssetUrl(target) {
const ext = target.includes("windows") ? "zip" : "tar.gz";
return `https://github.com/rtk-ai/rtk/releases/download/v${RTK_VERSION}/rtk-${target}.${ext}`;
}
const MAX_DOWNLOAD_ATTEMPTS = 5;
const RETRIABLE_HTTP_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]);
class NonRetriableError extends Error {}
function backoffDelayMs(attempt) {
// Deterministic backoff — build scripts avoid Math.random for reproducibility.
return Math.min(1000 * 2 ** (attempt - 1), 15000);
}
async function downloadFile(url, destPath) {
for (let attempt = 1; attempt <= MAX_DOWNLOAD_ATTEMPTS; attempt++) {
try {
const response = await fetch(url, { redirect: "follow" });
if (!response.ok) {
const message = `HTTP ${response.status}: ${response.statusText}`;
if (RETRIABLE_HTTP_STATUSES.has(response.status))
throw new Error(message);
throw new NonRetriableError(message);
}
await pipeline(response.body, createWriteStream(destPath));
return;
} catch (error) {
if (
error instanceof NonRetriableError ||
attempt === MAX_DOWNLOAD_ATTEMPTS
) {
throw error;
}
await sleep(backoffDelayMs(attempt));
}
}
}
async function verifyChecksum(archivePath, target) {
const expected = RTK_SHA256[target];
if (!expected) {
throw new Error(`No pinned checksum for rtk target ${target}`);
}
const hash = createHash("sha256");
await pipeline(createReadStream(archivePath), hash);
const actual = hash.digest("hex");
if (actual !== expected) {
throw new Error(
`rtk checksum mismatch for ${target}: expected ${expected}, got ${actual}`,
);
}
}
async function extractArchive(archivePath, destDir, target) {
if (target.includes("windows")) {
const entries = unzipSync(readFileSync(archivePath));
const bin = rtkBinName("win32");
const data = entries[bin] ?? entries[`rtk-${target}/${bin}`];
if (!data) throw new Error(`rtk binary not found in ${archivePath}`);
writeFileSync(join(destDir, bin), data);
} else {
await extract({ file: archivePath, cwd: destDir });
}
}
/**
* Downloads and caches the pinned RTK binary for the build target, then copies
* it into `destDir`. Returns the path to the bundled binary, or null when RTK
* publishes no asset for the current platform/arch. Throws on download failure.
*/
export async function ensureRtkBinary(destDir) {
const target = rtkReleaseTarget();
if (!target) return null;
const binName = rtkBinName();
const cacheDir = resolve(
__dirname,
"../../../node_modules/.cache/posthog-rtk",
RTK_VERSION,
target,
);
const cachedBinary = join(cacheDir, binName);
if (!existsSync(cachedBinary)) {
mkdirSync(cacheDir, { recursive: true });
const url = rtkAssetUrl(target);
const archivePath = join(
cacheDir,
url.endsWith(".zip") ? "rtk.zip" : "rtk.tar.gz",
);
await downloadFile(url, archivePath);
try {
await verifyChecksum(archivePath, target);
} catch (error) {
// Don't leave an unverified archive on disk to be reused.
rmSync(archivePath, { force: true });
throw error;
}
await extractArchive(archivePath, cacheDir, target);
rmSync(archivePath, { force: true });
if (!existsSync(cachedBinary)) {
throw new Error(`rtk binary missing after extraction: ${cachedBinary}`);
}
}
mkdirSync(destDir, { recursive: true });
const dest = join(destDir, binName);
copyFileSync(cachedBinary, dest);
if (targetPlatform() !== "win32") chmodSync(dest, 0o755);
return dest;
}