@relayfile/sdk 0.10.62 → 0.10.64
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/dist/package-version.d.ts +1 -1
- package/dist/package-version.js +1 -1
- package/dist/relay-cli/cloud-preflight.d.ts +99 -0
- package/dist/relay-cli/cloud-preflight.js +352 -0
- package/dist/relay-cli/command-spec.json +1682 -0
- package/dist/relay-cli/index.d.ts +125 -0
- package/dist/relay-cli/index.js +197 -0
- package/dist/relay-cli/resolve-binary.d.ts +220 -0
- package/dist/relay-cli/resolve-binary.js +565 -0
- package/package.json +21 -8
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one implementation of "find the relayfile binary".
|
|
3
|
+
*
|
|
4
|
+
* The real relayfile CLI is the Go binary (`cmd/relayfile-cli`). Both entry
|
|
5
|
+
* points into it — the `relayfile` npm package's bin shim
|
|
6
|
+
* (`packages/cli/scripts/run.js`) and the `agent-relay file` CLI surface in
|
|
7
|
+
* this directory — resolve it through this module. Nothing else in the repo
|
|
8
|
+
* may reimplement the lookup.
|
|
9
|
+
*
|
|
10
|
+
* The binary reaches a machine two different ways, and the resolver has to
|
|
11
|
+
* cope with both:
|
|
12
|
+
*
|
|
13
|
+
* - As `@relayfile/cli-<platform>-<arch>`, an optional dependency of this
|
|
14
|
+
* package. npm installs only the one matching the host's `os`/`cpu`, so
|
|
15
|
+
* nothing is downloaded at install time, the install works offline and in
|
|
16
|
+
* CI, and integrity comes from the registry. This is the only path that
|
|
17
|
+
* exists for a consumer that depends on `@relayfile/sdk` without depending
|
|
18
|
+
* on `relayfile` — `agent-relay` is exactly that consumer.
|
|
19
|
+
* - Inside the `relayfile` package's own `bin/`, put there by that package's
|
|
20
|
+
* `postinstall` (`packages/cli/scripts/install.js`), which downloads the
|
|
21
|
+
* per-platform build from GitHub Releases.
|
|
22
|
+
*/
|
|
23
|
+
import { spawnSync } from "node:child_process";
|
|
24
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
25
|
+
import { existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync } from "node:fs";
|
|
26
|
+
import { createRequire } from "node:module";
|
|
27
|
+
import os from "node:os";
|
|
28
|
+
import path from "node:path";
|
|
29
|
+
import { fileURLToPath } from "node:url";
|
|
30
|
+
const PLATFORM_MAP = {
|
|
31
|
+
darwin: "darwin",
|
|
32
|
+
linux: "linux",
|
|
33
|
+
win32: "windows"
|
|
34
|
+
};
|
|
35
|
+
const ARCH_MAP = {
|
|
36
|
+
x64: "amd64",
|
|
37
|
+
arm64: "arm64"
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Node platform/arch pairs that have a published `@relayfile/cli-*` package.
|
|
41
|
+
*
|
|
42
|
+
* Kept in lockstep with this package's `optionalDependencies`, the target list
|
|
43
|
+
* in `packages/cli/scripts/build-binaries.js`, and the package directories
|
|
44
|
+
* under `packages/cli-*`. `platform-packages.test.ts` fails when they diverge.
|
|
45
|
+
*/
|
|
46
|
+
const PLATFORM_PACKAGE_TARGETS = [
|
|
47
|
+
"darwin-arm64",
|
|
48
|
+
"darwin-x64",
|
|
49
|
+
"linux-arm64",
|
|
50
|
+
"linux-x64",
|
|
51
|
+
"win32-arm64",
|
|
52
|
+
"win32-x64"
|
|
53
|
+
];
|
|
54
|
+
/** Environment variable that pins the binary, bypassing every other step. */
|
|
55
|
+
export const RELAYFILE_CLI_BIN_ENV = "RELAYFILE_CLI_BIN";
|
|
56
|
+
/**
|
|
57
|
+
* Name of the npm package carrying the prebuilt binary for a target.
|
|
58
|
+
*
|
|
59
|
+
* @param platform - Node platform id; defaults to this host's.
|
|
60
|
+
* @param arch - Node arch id; defaults to this host's.
|
|
61
|
+
* @returns The package name, or null when no package is published for the
|
|
62
|
+
* target (nothing to suggest installing, so callers say "build from source").
|
|
63
|
+
*/
|
|
64
|
+
export function platformPackageName(platform = os.platform(), arch = os.arch()) {
|
|
65
|
+
const target = `${platform}-${arch}`;
|
|
66
|
+
return PLATFORM_PACKAGE_TARGETS.includes(target)
|
|
67
|
+
? `@relayfile/cli-${target}`
|
|
68
|
+
: null;
|
|
69
|
+
}
|
|
70
|
+
/** Every `@relayfile/cli-*` package name, for tests and release tooling. */
|
|
71
|
+
export function platformPackageNames() {
|
|
72
|
+
return PLATFORM_PACKAGE_TARGETS.map((target) => `@relayfile/cli-${target}`);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Name of the binary inside a `@relayfile/cli-*` package.
|
|
76
|
+
*
|
|
77
|
+
* It keeps the Go binary's own name (matching `relayfile-mount` inside the
|
|
78
|
+
* `@relayfile/mount-*` packages) rather than the `relayfile` name the CLI
|
|
79
|
+
* package installs, so the two never collide on a machine that has both.
|
|
80
|
+
*
|
|
81
|
+
* @param platform - Node platform id; defaults to this host's.
|
|
82
|
+
* @returns The file name, with `.exe` on Windows.
|
|
83
|
+
*/
|
|
84
|
+
export function platformPackageBinaryName(platform = os.platform()) {
|
|
85
|
+
return platform === "win32" ? "relayfile-cli.exe" : "relayfile-cli";
|
|
86
|
+
}
|
|
87
|
+
/** Thrown when neither a binary nor a usable source checkout was found. */
|
|
88
|
+
export class RelayfileBinaryNotFoundError extends Error {
|
|
89
|
+
platform;
|
|
90
|
+
arch;
|
|
91
|
+
/** The `@relayfile/cli-*` package for this target, when one is published. */
|
|
92
|
+
platformPackage;
|
|
93
|
+
constructor(platform, arch) {
|
|
94
|
+
super(formatBinaryNotFoundMessage(platform, arch));
|
|
95
|
+
this.name = "RelayfileBinaryNotFoundError";
|
|
96
|
+
this.platform = platform;
|
|
97
|
+
this.arch = arch;
|
|
98
|
+
this.platformPackage = platformPackageName(platform, arch);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Explain what to install, for this exact platform.
|
|
103
|
+
*
|
|
104
|
+
* A bare ENOENT sends people looking for a bug in their PATH. The prebuilt
|
|
105
|
+
* binary is an optional dependency, so the two things that actually cause this
|
|
106
|
+
* — `--omit=optional` and an unsupported target — both need naming.
|
|
107
|
+
*
|
|
108
|
+
* @param platform - Node platform id.
|
|
109
|
+
* @param arch - Node arch id.
|
|
110
|
+
* @returns The message carried by `RelayfileBinaryNotFoundError`.
|
|
111
|
+
*/
|
|
112
|
+
export function formatBinaryNotFoundMessage(platform, arch) {
|
|
113
|
+
const packageName = platformPackageName(platform, arch);
|
|
114
|
+
if (!packageName) {
|
|
115
|
+
return (`relayfile has no prebuilt CLI binary for ${platform} ${arch}. ` +
|
|
116
|
+
`Prebuilt binaries exist for ${PLATFORM_PACKAGE_TARGETS.join(", ")}. ` +
|
|
117
|
+
`Build one from source with \`go build ./cmd/relayfile-cli\` and point ` +
|
|
118
|
+
`${RELAYFILE_CLI_BIN_ENV} at it.`);
|
|
119
|
+
}
|
|
120
|
+
return (`relayfile could not find a relayfile-cli binary for ${platform} ${arch}. ` +
|
|
121
|
+
`The prebuilt binary ships as ${packageName}, an optional dependency of ` +
|
|
122
|
+
`@relayfile/sdk, so it is missing when the install ran with ` +
|
|
123
|
+
`--omit=optional or could not fetch optional packages. Fix it with one of:\n` +
|
|
124
|
+
` npm install ${packageName}\n` +
|
|
125
|
+
` npm install @relayfile/sdk --include=optional\n` +
|
|
126
|
+
` npm install -g relayfile (the standalone CLI)\n` +
|
|
127
|
+
`Or set ${RELAYFILE_CLI_BIN_ENV} to a binary you built or downloaded from ` +
|
|
128
|
+
`https://github.com/AgentWorkforce/relayfile/releases.`);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Name of the per-platform binary shipped inside the `relayfile` package.
|
|
132
|
+
*
|
|
133
|
+
* @returns The packaged binary's file name, or null on an unsupported target.
|
|
134
|
+
*/
|
|
135
|
+
export function platformBinaryName(platform = os.platform(), arch = os.arch()) {
|
|
136
|
+
const goPlatform = PLATFORM_MAP[platform];
|
|
137
|
+
const goArch = ARCH_MAP[arch];
|
|
138
|
+
if (!goPlatform || !goArch) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const extension = goPlatform === "windows" ? ".exe" : "";
|
|
142
|
+
return `relayfile-cli-${goPlatform}-${goArch}${extension}`;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Name of the binary the CLI package installs and runs, `bin/relayfile`.
|
|
146
|
+
*
|
|
147
|
+
* @param platform - Node platform id; defaults to this host's.
|
|
148
|
+
* @returns The file name, with `.exe` on Windows.
|
|
149
|
+
*/
|
|
150
|
+
export function genericBinaryName(platform = os.platform()) {
|
|
151
|
+
return platform === "win32" ? "relayfile.exe" : "relayfile";
|
|
152
|
+
}
|
|
153
|
+
function moduleDirectory() {
|
|
154
|
+
return path.dirname(fileURLToPath(import.meta.url));
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Walk up from `start` looking for a directory that satisfies `matches`.
|
|
158
|
+
*
|
|
159
|
+
* @returns The matching directory, or null when the filesystem root is reached.
|
|
160
|
+
*/
|
|
161
|
+
function findUpward(start, matches) {
|
|
162
|
+
let current = path.resolve(start);
|
|
163
|
+
for (;;) {
|
|
164
|
+
if (matches(current)) {
|
|
165
|
+
return current;
|
|
166
|
+
}
|
|
167
|
+
const parent = path.dirname(current);
|
|
168
|
+
if (parent === current) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
current = parent;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Locate the `bin` directory of the installed `relayfile` CLI package.
|
|
176
|
+
*
|
|
177
|
+
* @returns The directory, or null when the package is not installed here.
|
|
178
|
+
*/
|
|
179
|
+
function installedCliBinDir() {
|
|
180
|
+
try {
|
|
181
|
+
const require = createRequire(import.meta.url);
|
|
182
|
+
const manifest = require.resolve("relayfile/package.json");
|
|
183
|
+
return path.join(path.dirname(manifest), "bin");
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Locate `packages/cli/bin` when running from inside this repo, where the SDK
|
|
191
|
+
* and the CLI package are siblings rather than dependencies.
|
|
192
|
+
*
|
|
193
|
+
* @returns The directory, or null outside a checkout.
|
|
194
|
+
*/
|
|
195
|
+
function workspaceCliBinDir(exists) {
|
|
196
|
+
const repoRoot = findUpward(moduleDirectory(), (directory) => exists(path.join(directory, "packages", "cli", "package.json")));
|
|
197
|
+
return repoRoot ? path.join(repoRoot, "packages", "cli", "bin") : null;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Locate a relayfile source checkout: a directory with both `go.mod` and
|
|
201
|
+
* `cmd/relayfile-cli`.
|
|
202
|
+
*
|
|
203
|
+
* @returns The checkout root, or null when there is none above `start`.
|
|
204
|
+
*/
|
|
205
|
+
export function findSourceCheckoutRoot(start, exists = existsSync) {
|
|
206
|
+
return findUpward(start, (directory) => exists(path.join(directory, "go.mod")) &&
|
|
207
|
+
exists(path.join(directory, "cmd", "relayfile-cli")));
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Go's GOARCH ("amd64") differs from Node's `process.arch` ("x64"). The
|
|
211
|
+
* platform package name follows Node; `make build-all` / `make release` output
|
|
212
|
+
* file names follow Go.
|
|
213
|
+
*/
|
|
214
|
+
function goArch(arch) {
|
|
215
|
+
return ARCH_MAP[arch] ?? arch;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Locate the binary inside `@relayfile/cli-<platform>-<arch>`.
|
|
219
|
+
*
|
|
220
|
+
* Resolved by `require.resolve` from several anchors rather than a fixed
|
|
221
|
+
* relative path: the package can sit anywhere npm decides to hoist it, and the
|
|
222
|
+
* SDK itself may be nested under a consumer's `node_modules` or bundled.
|
|
223
|
+
*
|
|
224
|
+
* @returns The binary path, or null when the package is not installed here
|
|
225
|
+
* (expected under `--omit=optional`, or on an unsupported target).
|
|
226
|
+
*/
|
|
227
|
+
function platformPackageBinary(platform, arch, exists, resolveFrom) {
|
|
228
|
+
const packageName = platformPackageName(platform, arch);
|
|
229
|
+
if (!packageName) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
const binaryName = platformPackageBinaryName(platform);
|
|
233
|
+
for (const anchor of resolveFrom) {
|
|
234
|
+
let manifest;
|
|
235
|
+
try {
|
|
236
|
+
manifest = createRequire(anchor).resolve(`${packageName}/package.json`);
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const candidate = path.join(path.dirname(manifest), "bin", binaryName);
|
|
242
|
+
if (exists(candidate)) {
|
|
243
|
+
return candidate;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Default anchors for resolving the platform package: this module, the entry
|
|
250
|
+
* script (a consumer's CLI, where the SDK lives under their `node_modules`),
|
|
251
|
+
* and the cwd.
|
|
252
|
+
*/
|
|
253
|
+
function defaultResolveFrom() {
|
|
254
|
+
const anchors = [path.join(moduleDirectory(), "resolve-binary.js")];
|
|
255
|
+
if (process.argv[1]) {
|
|
256
|
+
anchors.push(process.argv[1]);
|
|
257
|
+
}
|
|
258
|
+
anchors.push(path.join(process.cwd(), "package.json"));
|
|
259
|
+
return [...new Set(anchors)];
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* `make build` and `make release` outputs inside a source checkout.
|
|
263
|
+
*
|
|
264
|
+
* Both names carry `.exe` on Windows: `go build -o bin/relayfile-cli` appends
|
|
265
|
+
* it for GOOS=windows, and `scripts/build-cli-npm-packages.mjs` looks for
|
|
266
|
+
* `dist/relayfile-cli-windows-<arch>.exe`. Without the suffix a successful
|
|
267
|
+
* `make build` is invisible here and resolution falls through to `go run`.
|
|
268
|
+
*
|
|
269
|
+
* @returns Candidate binary paths, highest priority first.
|
|
270
|
+
*/
|
|
271
|
+
function sourceCheckoutBinaries(platform, arch, exists, searchRoots) {
|
|
272
|
+
const candidates = [];
|
|
273
|
+
const seenRoots = new Set();
|
|
274
|
+
for (const start of searchRoots) {
|
|
275
|
+
const root = findSourceCheckoutRoot(start, exists);
|
|
276
|
+
if (!root || seenRoots.has(root)) {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
seenRoots.add(root);
|
|
280
|
+
const goOs = PLATFORM_MAP[platform] ?? platform;
|
|
281
|
+
const extension = goOs === "windows" ? ".exe" : "";
|
|
282
|
+
candidates.push(path.join(root, "bin", `relayfile-cli${extension}`));
|
|
283
|
+
candidates.push(path.join(root, "dist", `relayfile-cli-${goOs}-${goArch(arch)}${extension}`));
|
|
284
|
+
}
|
|
285
|
+
return candidates;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Last-resort `PATH` scan, for a Go binary placed outside npm.
|
|
289
|
+
*
|
|
290
|
+
* Deliberately matches `relayfile-cli` only, never the generic `relayfile`:
|
|
291
|
+
* on PATH that name is usually the npm bin shim
|
|
292
|
+
* (`packages/cli/scripts/run.js`), which resolves its binary through this
|
|
293
|
+
* module. Spawning it here would make the resolver call itself forever.
|
|
294
|
+
*
|
|
295
|
+
* @returns The first match, or null.
|
|
296
|
+
*/
|
|
297
|
+
function findOnPath(platform, exists, pathEntries) {
|
|
298
|
+
const name = platformPackageBinaryName(platform);
|
|
299
|
+
for (const entry of pathEntries) {
|
|
300
|
+
const candidate = path.join(entry, name);
|
|
301
|
+
if (exists(candidate)) {
|
|
302
|
+
return candidate;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
function binary(binaryPath) {
|
|
308
|
+
return { kind: "binary", command: binaryPath, args: [], binaryPath };
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Resolve how to launch the relayfile CLI on this machine.
|
|
312
|
+
*
|
|
313
|
+
* Search order, and why it is this order:
|
|
314
|
+
* 1. `RELAYFILE_CLI_BIN` — the escape hatch, so a developer or operator can
|
|
315
|
+
* always pin an exact binary.
|
|
316
|
+
* 2. `@relayfile/cli-<platform>-<arch>` — the prebuilt binary npm installed
|
|
317
|
+
* for this host. First because it is the only path that exists for a
|
|
318
|
+
* consumer of `@relayfile/sdk` that does not also depend on `relayfile`
|
|
319
|
+
* (`agent-relay file` is that consumer), and because it needs no network.
|
|
320
|
+
* 3. The `binDirs` chain: a generic `bin/relayfile` (the `relayfile`
|
|
321
|
+
* package's postinstall download, or a local build), then the
|
|
322
|
+
* per-platform `bin/relayfile-cli-<os>-<arch>` that package ships.
|
|
323
|
+
* 4. `make build` / `make release` outputs in an enclosing source checkout.
|
|
324
|
+
* 5. `go run ./cmd/relayfile-cli` from that checkout.
|
|
325
|
+
* 6. `PATH`, for a binary installed outside npm.
|
|
326
|
+
*
|
|
327
|
+
* @param options - Search overrides; all are optional.
|
|
328
|
+
* @returns The command, argv prefix, and cwd to spawn.
|
|
329
|
+
* @throws {RelayfileBinaryNotFoundError} When nothing usable was found.
|
|
330
|
+
*/
|
|
331
|
+
export function resolveRelayfileBinary(options = {}) {
|
|
332
|
+
const exists = options.fileExists ?? existsSync;
|
|
333
|
+
const platform = options.platform ?? os.platform();
|
|
334
|
+
const arch = options.arch ?? os.arch();
|
|
335
|
+
const env = options.env ?? process.env;
|
|
336
|
+
const override = env[RELAYFILE_CLI_BIN_ENV];
|
|
337
|
+
if (override) {
|
|
338
|
+
const resolved = path.resolve(override);
|
|
339
|
+
if (exists(resolved)) {
|
|
340
|
+
return binary(resolved);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const fromPlatformPackage = platformPackageBinary(platform, arch, exists, options.resolveFrom ?? defaultResolveFrom());
|
|
344
|
+
if (fromPlatformPackage) {
|
|
345
|
+
return binary(fromPlatformPackage);
|
|
346
|
+
}
|
|
347
|
+
const binDirs = options.binDirs ??
|
|
348
|
+
[installedCliBinDir(), workspaceCliBinDir(exists)].filter((directory) => Boolean(directory));
|
|
349
|
+
const packagedName = platformBinaryName(platform, arch);
|
|
350
|
+
for (const binDir of binDirs) {
|
|
351
|
+
const candidates = [
|
|
352
|
+
path.join(binDir, genericBinaryName(platform)),
|
|
353
|
+
packagedName ? path.join(binDir, packagedName) : null
|
|
354
|
+
].filter((candidate) => Boolean(candidate));
|
|
355
|
+
for (const candidate of candidates) {
|
|
356
|
+
if (exists(candidate)) {
|
|
357
|
+
return binary(candidate);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
const searchRoots = [...(options.searchFrom ?? []), moduleDirectory(), process.cwd()];
|
|
362
|
+
for (const candidate of sourceCheckoutBinaries(platform, arch, exists, searchRoots)) {
|
|
363
|
+
if (exists(candidate)) {
|
|
364
|
+
return binary(candidate);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
for (const root of searchRoots) {
|
|
368
|
+
const checkout = findSourceCheckoutRoot(root, exists);
|
|
369
|
+
if (checkout) {
|
|
370
|
+
return {
|
|
371
|
+
kind: "go-run",
|
|
372
|
+
command: "go",
|
|
373
|
+
args: ["run", "./cmd/relayfile-cli"],
|
|
374
|
+
cwd: checkout
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const pathEntries = options.pathEntries ?? (env.PATH ?? "").split(path.delimiter).filter(Boolean);
|
|
379
|
+
const onPath = findOnPath(platform, exists, pathEntries);
|
|
380
|
+
if (onPath) {
|
|
381
|
+
return binary(onPath);
|
|
382
|
+
}
|
|
383
|
+
throw new RelayfileBinaryNotFoundError(platform, arch);
|
|
384
|
+
}
|
|
385
|
+
/** Message shown when a source checkout was found but Go is not installed. */
|
|
386
|
+
export const GO_TOOLCHAIN_MISSING_MESSAGE = "relayfile binary not found and Go is not installed to run from source. " +
|
|
387
|
+
"Install Go or run `npm run build --workspace=packages/cli`.";
|
|
388
|
+
/** Thrown by `buildGoRunBinary` when there is no `go` on PATH. */
|
|
389
|
+
export class GoToolchainMissingError extends Error {
|
|
390
|
+
constructor() {
|
|
391
|
+
super(GO_TOOLCHAIN_MISSING_MESSAGE);
|
|
392
|
+
this.name = "GoToolchainMissingError";
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/** Thrown by `buildGoRunBinary` when `go build` itself fails. */
|
|
396
|
+
export class GoBuildFailedError extends Error {
|
|
397
|
+
/** `go build`'s exit code, or null when it was killed by a signal. */
|
|
398
|
+
exitCode;
|
|
399
|
+
constructor(exitCode, stderr) {
|
|
400
|
+
super(`building relayfile from source failed (go build exited ${exitCode})` +
|
|
401
|
+
(stderr.trim() ? `\n${stderr.trim()}` : ""));
|
|
402
|
+
this.name = "GoBuildFailedError";
|
|
403
|
+
this.exitCode = exitCode;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Where a source-checkout build is cached: outside the checkout, keyed by it.
|
|
408
|
+
*
|
|
409
|
+
* Not `<checkout>/bin`, which is `make build`'s output — a fallback launch
|
|
410
|
+
* must not write into someone's working tree, and a read-only checkout still
|
|
411
|
+
* has to work.
|
|
412
|
+
*
|
|
413
|
+
* @param checkout - The source checkout root.
|
|
414
|
+
* @param platform - Node platform id; defaults to this host's.
|
|
415
|
+
* @returns The absolute path to build to.
|
|
416
|
+
*/
|
|
417
|
+
export function goRunBinaryPath(checkout, platform = os.platform()) {
|
|
418
|
+
const key = createHash("sha256").update(path.resolve(checkout)).digest("hex").slice(0, 16);
|
|
419
|
+
return path.join(os.tmpdir(), "relayfile-go-run", key, platformPackageBinaryName(platform));
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Turn a `go-run` resolution into an executable binary.
|
|
423
|
+
*
|
|
424
|
+
* `go run` cannot be used directly, because the program it launches inherits
|
|
425
|
+
* the `go` command's own working directory — and `go` only finds the module
|
|
426
|
+
* from that directory, so it has to be the checkout. Every relative path in
|
|
427
|
+
* the caller's argv would then resolve against the repository instead of the
|
|
428
|
+
* directory the caller actually ran in (`--output report.json` writing into
|
|
429
|
+
* the checkout root). `go -C <checkout> run` has the same effect, and passing
|
|
430
|
+
* an absolute package path fails outright outside a module.
|
|
431
|
+
*
|
|
432
|
+
* Building first and spawning the result separates the two: the build runs in
|
|
433
|
+
* the checkout, where the module is, and the binary runs wherever the caller
|
|
434
|
+
* asked for. Go's build cache makes the repeat cost a relink.
|
|
435
|
+
*
|
|
436
|
+
* The build never writes the shared path directly. That path is keyed by the
|
|
437
|
+
* checkout alone, so every process that falls back to source aims at the same
|
|
438
|
+
* file — and a `listen` or `mount` launched from it keeps running for hours.
|
|
439
|
+
* Writing it in place would mean overwriting a binary that is executing
|
|
440
|
+
* (impossible on Windows, and on Unix a window in which a concurrent build has
|
|
441
|
+
* replaced it with a partial file). So `go build` writes a private sibling and
|
|
442
|
+
* the result is published with one rename: atomic on POSIX, and an already
|
|
443
|
+
* running process keeps the inode it started from. If the rename cannot
|
|
444
|
+
* happen — Windows refuses to replace a running `.exe` — the caller gets the
|
|
445
|
+
* private path instead, which is just as runnable.
|
|
446
|
+
*
|
|
447
|
+
* @param resolution - The `go-run` resolution to materialize.
|
|
448
|
+
* @param options - Environment and output overrides.
|
|
449
|
+
* @returns The absolute path of the built binary.
|
|
450
|
+
* @throws {GoToolchainMissingError} When `go` is not on PATH.
|
|
451
|
+
* @throws {GoBuildFailedError} When `go build` exits non-zero.
|
|
452
|
+
*/
|
|
453
|
+
export function buildGoRunBinary(resolution, options = {}) {
|
|
454
|
+
const output = options.outputPath ?? goRunBinaryPath(resolution.cwd);
|
|
455
|
+
mkdirSync(path.dirname(output), { recursive: true });
|
|
456
|
+
sweepStaleBuildArtifacts(output);
|
|
457
|
+
// Same directory as the destination, so the publish below is a rename
|
|
458
|
+
// within one filesystem rather than a copy. pid plus random bytes: two
|
|
459
|
+
// builds in one process must not share it either. The destination's
|
|
460
|
+
// extension is kept last, because on Windows the fallback below hands this
|
|
461
|
+
// very path to the host to execute and that has to stay an `.exe`.
|
|
462
|
+
const staged = `${buildArtifactPrefix(output)}${process.pid}-${randomBytes(6).toString("hex")}` +
|
|
463
|
+
path.extname(output);
|
|
464
|
+
const result = spawnSync("go", ["build", "-o", staged, "./cmd/relayfile-cli"], {
|
|
465
|
+
cwd: resolution.cwd,
|
|
466
|
+
env: options.env ?? process.env,
|
|
467
|
+
encoding: "utf8"
|
|
468
|
+
});
|
|
469
|
+
if (result.error) {
|
|
470
|
+
discardBuildArtifact(staged);
|
|
471
|
+
if (result.error.code === "ENOENT") {
|
|
472
|
+
throw new GoToolchainMissingError();
|
|
473
|
+
}
|
|
474
|
+
throw result.error;
|
|
475
|
+
}
|
|
476
|
+
if (result.status !== 0) {
|
|
477
|
+
discardBuildArtifact(staged);
|
|
478
|
+
throw new GoBuildFailedError(result.status, result.stderr ?? "");
|
|
479
|
+
}
|
|
480
|
+
try {
|
|
481
|
+
renameSync(staged, output);
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
// The shared name is held by something that cannot be replaced — a
|
|
485
|
+
// running binary on Windows. The staged build is a complete binary, so
|
|
486
|
+
// run that instead of failing the command. sweepStaleBuildArtifacts
|
|
487
|
+
// collects it later.
|
|
488
|
+
return staged;
|
|
489
|
+
}
|
|
490
|
+
return output;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Prefix that marks a file in the build directory as a staged build.
|
|
494
|
+
*
|
|
495
|
+
* Inserted before the destination's extension rather than after it, so a
|
|
496
|
+
* staged `relayfile-cli.exe` is still named `...exe`.
|
|
497
|
+
*
|
|
498
|
+
* @param output - The shared build path.
|
|
499
|
+
* @returns The absolute path prefix every staged build starts with.
|
|
500
|
+
*/
|
|
501
|
+
function buildArtifactPrefix(output) {
|
|
502
|
+
const extension = path.extname(output);
|
|
503
|
+
return path.join(path.dirname(output), `${path.basename(output, extension)}.build-`);
|
|
504
|
+
}
|
|
505
|
+
/** How long an unpublished staged build is left alone before it is swept. */
|
|
506
|
+
const STALE_BUILD_ARTIFACT_MS = 24 * 60 * 60 * 1000;
|
|
507
|
+
/**
|
|
508
|
+
* Drop a staged build that was never published.
|
|
509
|
+
*
|
|
510
|
+
* Best effort throughout: a leftover artifact is wasted disk, never an error
|
|
511
|
+
* worth failing a command over.
|
|
512
|
+
*
|
|
513
|
+
* @param staged - The staged build path.
|
|
514
|
+
*/
|
|
515
|
+
function discardBuildArtifact(staged) {
|
|
516
|
+
try {
|
|
517
|
+
rmSync(staged, { force: true });
|
|
518
|
+
}
|
|
519
|
+
catch {
|
|
520
|
+
// Ignore.
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Collect staged builds that were left behind.
|
|
525
|
+
*
|
|
526
|
+
* A staged build normally disappears into the rename that publishes it, and a
|
|
527
|
+
* failed build is removed on the spot. What is left is the case the rename
|
|
528
|
+
* could not happen (Windows, shared name in use) and the case a process died
|
|
529
|
+
* mid-build — neither of which cleans up after itself, and both of which
|
|
530
|
+
* would otherwise grow a binary-sized file per invocation forever.
|
|
531
|
+
*
|
|
532
|
+
* The age cutoff is what keeps this safe: a returned fallback binary may be
|
|
533
|
+
* executing right now, and only artifacts far older than any plausible build
|
|
534
|
+
* are removed. On Unix unlinking a running binary is harmless anyway; on
|
|
535
|
+
* Windows the delete simply fails and is ignored.
|
|
536
|
+
*
|
|
537
|
+
* @param output - The shared build path whose directory is swept.
|
|
538
|
+
*/
|
|
539
|
+
function sweepStaleBuildArtifacts(output) {
|
|
540
|
+
const prefix = path.basename(buildArtifactPrefix(output));
|
|
541
|
+
const directory = path.dirname(output);
|
|
542
|
+
let entries;
|
|
543
|
+
try {
|
|
544
|
+
entries = readdirSync(directory);
|
|
545
|
+
}
|
|
546
|
+
catch {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const cutoff = Date.now() - STALE_BUILD_ARTIFACT_MS;
|
|
550
|
+
for (const entry of entries) {
|
|
551
|
+
if (!entry.startsWith(prefix)) {
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
const candidate = path.join(directory, entry);
|
|
555
|
+
try {
|
|
556
|
+
if (statSync(candidate).mtimeMs > cutoff) {
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
rmSync(candidate, { force: true });
|
|
560
|
+
}
|
|
561
|
+
catch {
|
|
562
|
+
// Ignore.
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@relayfile/sdk",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.64",
|
|
4
4
|
"description": "TypeScript SDK for relayfile — real-time filesystem for humans and agents",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
"types": "./dist/cli/index.d.ts",
|
|
15
15
|
"default": "./dist/cli/index.js"
|
|
16
16
|
},
|
|
17
|
+
"./relay-cli": {
|
|
18
|
+
"types": "./dist/relay-cli/index.d.ts",
|
|
19
|
+
"default": "./dist/relay-cli/index.js"
|
|
20
|
+
},
|
|
17
21
|
"./cloud-login": {
|
|
18
22
|
"types": "./dist/cloud-login.d.ts",
|
|
19
23
|
"default": "./dist/cloud-login.js"
|
|
@@ -46,7 +50,7 @@
|
|
|
46
50
|
"dist"
|
|
47
51
|
],
|
|
48
52
|
"scripts": {
|
|
49
|
-
"build": "node scripts/sync-package-version.mjs && tsc",
|
|
53
|
+
"build": "node scripts/sync-package-version.mjs && tsc && node scripts/copy-relay-cli-assets.mjs",
|
|
50
54
|
"typecheck": "tsc --noEmit",
|
|
51
55
|
"test": "vitest run",
|
|
52
56
|
"test:bundle:bun": "node scripts/verify-bun-workspace-mount.mjs",
|
|
@@ -57,20 +61,29 @@
|
|
|
57
61
|
"test:e2e:golden-path": "node scripts/agent-workspace-golden-path-e2e.mjs",
|
|
58
62
|
"demo:agent-workspace": "npm run build && node scripts/agent-workspace-demo.mjs",
|
|
59
63
|
"setup:e2e": "node scripts/setup-e2e.mjs",
|
|
60
|
-
"prepublishOnly": "npm run build"
|
|
64
|
+
"prepublishOnly": "npm run build",
|
|
65
|
+
"gen:command-spec": "node scripts/gen-command-spec.mjs",
|
|
66
|
+
"check:command-spec": "node scripts/gen-command-spec.mjs --check"
|
|
61
67
|
},
|
|
62
68
|
"dependencies": {
|
|
63
|
-
"@relayfile/core": "0.10.
|
|
69
|
+
"@relayfile/core": "0.10.64",
|
|
64
70
|
"ignore": "^7.0.5",
|
|
65
71
|
"tar": "^7.5.10"
|
|
66
72
|
},
|
|
67
73
|
"optionalDependencies": {
|
|
68
|
-
"@relayfile/
|
|
69
|
-
"@relayfile/
|
|
70
|
-
"@relayfile/
|
|
71
|
-
"@relayfile/
|
|
74
|
+
"@relayfile/cli-darwin-arm64": "0.10.64",
|
|
75
|
+
"@relayfile/cli-darwin-x64": "0.10.64",
|
|
76
|
+
"@relayfile/cli-linux-arm64": "0.10.64",
|
|
77
|
+
"@relayfile/cli-linux-x64": "0.10.64",
|
|
78
|
+
"@relayfile/cli-win32-arm64": "0.10.64",
|
|
79
|
+
"@relayfile/cli-win32-x64": "0.10.64",
|
|
80
|
+
"@relayfile/mount-darwin-arm64": "0.10.64",
|
|
81
|
+
"@relayfile/mount-darwin-x64": "0.10.64",
|
|
82
|
+
"@relayfile/mount-linux-arm64": "0.10.64",
|
|
83
|
+
"@relayfile/mount-linux-x64": "0.10.64"
|
|
72
84
|
},
|
|
73
85
|
"devDependencies": {
|
|
86
|
+
"@agent-relay/cli-surface": "^12.2.4",
|
|
74
87
|
"typescript": "^5.7.3",
|
|
75
88
|
"vitest": "^3.0.0"
|
|
76
89
|
},
|