@relayfile/sdk 0.7.1 → 0.7.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/dist/client.d.ts +8 -0
- package/dist/client.js +10 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/mount-launcher.d.ts +22 -0
- package/dist/mount-launcher.js +371 -0
- package/dist/onWrite.js +15 -2
- package/dist/setup-errors.d.ts +38 -0
- package/dist/setup-errors.js +65 -0
- package/dist/setup-types.d.ts +103 -0
- package/dist/setup.d.ts +15 -2
- package/dist/setup.js +390 -8
- package/package.json +4 -2
package/dist/client.d.ts
CHANGED
|
@@ -63,6 +63,14 @@ export declare class RelayFileClient {
|
|
|
63
63
|
* sync-vs-async tokenProvider shapes.
|
|
64
64
|
*/
|
|
65
65
|
getToken(): Promise<string>;
|
|
66
|
+
/**
|
|
67
|
+
* Return the normalized API base URL this client was constructed with.
|
|
68
|
+
*
|
|
69
|
+
* onWrite/sync consumers use this so an explicit RelayFileClient remains the
|
|
70
|
+
* source of truth for both HTTP and WebSocket traffic instead of letting a
|
|
71
|
+
* process-level env override silently redirect only the WS side.
|
|
72
|
+
*/
|
|
73
|
+
getBaseUrl(): string;
|
|
66
74
|
listTree(workspaceId: string, options?: ListTreeOptions): Promise<TreeResponse>;
|
|
67
75
|
readFile(workspaceId: string, path: string, correlationId?: string, signal?: AbortSignal): Promise<FileReadResponse>;
|
|
68
76
|
readFile(input: ReadFileInput): Promise<FileReadResponse>;
|
package/dist/client.js
CHANGED
|
@@ -185,6 +185,16 @@ export class RelayFileClient {
|
|
|
185
185
|
async getToken() {
|
|
186
186
|
return resolveToken(this.tokenProvider);
|
|
187
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Return the normalized API base URL this client was constructed with.
|
|
190
|
+
*
|
|
191
|
+
* onWrite/sync consumers use this so an explicit RelayFileClient remains the
|
|
192
|
+
* source of truth for both HTTP and WebSocket traffic instead of letting a
|
|
193
|
+
* process-level env override silently redirect only the WS side.
|
|
194
|
+
*/
|
|
195
|
+
getBaseUrl() {
|
|
196
|
+
return this.baseUrl;
|
|
197
|
+
}
|
|
188
198
|
async listTree(workspaceId, options = {}) {
|
|
189
199
|
const query = buildQuery({
|
|
190
200
|
path: options.path ?? "/",
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export { RelayFileClient, DEFAULT_RELAYFILE_BASE_URL, type AccessTokenProvider, type ConnectWebSocketOptions, type RelayFileClientOptions, type RelayFileRetryOptions, type WebSocketConnection } from "./client.js";
|
|
2
2
|
export { RelayfileSetup, RELAYFILE_SDK_VERSION, WorkspaceHandle } from "./setup.js";
|
|
3
3
|
export { type RelayfileCloudLoginOptions, type RelayfileCloudTokenSet, type RelayfileCloudTokenSetupOptions } from "./cloud-login.js";
|
|
4
|
-
export { CloudAbortError, CloudApiError, CloudTimeoutError, IntegrationConnectionTimeoutError, MalformedCloudResponseError, MissingConnectionIdError, RelayfileSetupError, UnknownProviderError } from "./setup-errors.js";
|
|
5
|
-
export { WORKSPACE_INTEGRATION_PROVIDERS, type AgentWorkspaceInvite, type AgentWorkspaceInviteOptions, type AgentWorkspaceScopedInviteOptions, type ConnectIntegrationOptions, type ConnectIntegrationResult, type CreateWorkspaceOptions, type JoinWorkspaceOptions, type RelayfileSetupOptions, type RelayfileSetupRetryOptions, type WaitForConnectionOptions, type WorkspaceInfo, type WorkspaceIntegrationProvider, type WorkspaceMountEnv, type WorkspaceMountEnvOptions, type WorkspacePermissions } from "./setup-types.js";
|
|
4
|
+
export { CloudAbortError, CloudApiError, CloudTimeoutError, InvalidLocalDirError, InvalidMountModeError, InvalidRemotePathError, IntegrationConnectionTimeoutError, MalformedCloudResponseError, MissingConnectionIdError, MountModeUnavailableError, MountReadyTimeoutError, MountSessionInputError, ProviderNotConnectedError, ProviderNotReadyError, RelayfileSetupError, UnknownProviderError } from "./setup-errors.js";
|
|
5
|
+
export { type EnsureMountedWorkspaceInput, WORKSPACE_INTEGRATION_PROVIDERS, type AgentWorkspaceInvite, type AgentWorkspaceInviteOptions, type AgentWorkspaceScopedInviteOptions, type ConnectIntegrationOptions, type ConnectIntegrationResult, type CreateWorkspaceOptions, type JoinWorkspaceOptions, type MountLauncher, type MountLauncherEvent, type MountLauncherInstance, type MountLauncherStart, type MountMode, type MountSessionRequest, type MountSessionResponse, type MountSessionResult, type MountedWorkspaceHandle, type MountedWorkspaceStatus, type MountWorkspaceInput, type RelayfileSetupOptions, type RelayfileSetupRetryOptions, type WaitForConnectionOptions, type WorkspaceInfo, type WorkspaceIntegrationProvider, type WorkspaceMountEnv, type WorkspaceMountEnvOptions, type WorkspacePermissions } from "./setup-types.js";
|
|
6
|
+
export { createDefaultMountLauncher, defaultMountLauncher, readMountedWorkspaceStatus } from "./mount-launcher.js";
|
|
6
7
|
export { RelayFileSync, type RelayFileSyncOptions, type RelayFileSyncPong, type RelayFileSyncReconnectOptions, type RelayFileSyncSocket, type RelayFileSyncState, type RelayFileSyncTokenProvider } from "./sync.js";
|
|
7
8
|
export { onWrite, pathMatches, type OnWriteClient, type OnWriteHandler, type OnWriteHandlerError, type OnWriteOptions } from "./onWrite.js";
|
|
8
9
|
export { InvalidStateError, PayloadTooLargeError, QueueFullError, RelayFileApiError, RevisionConflictError } from "./errors.js";
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export { RelayFileClient, DEFAULT_RELAYFILE_BASE_URL } from "./client.js";
|
|
2
2
|
export { RelayfileSetup, RELAYFILE_SDK_VERSION, WorkspaceHandle } from "./setup.js";
|
|
3
|
-
export { CloudAbortError, CloudApiError, CloudTimeoutError, IntegrationConnectionTimeoutError, MalformedCloudResponseError, MissingConnectionIdError, RelayfileSetupError, UnknownProviderError } from "./setup-errors.js";
|
|
3
|
+
export { CloudAbortError, CloudApiError, CloudTimeoutError, InvalidLocalDirError, InvalidMountModeError, InvalidRemotePathError, IntegrationConnectionTimeoutError, MalformedCloudResponseError, MissingConnectionIdError, MountModeUnavailableError, MountReadyTimeoutError, MountSessionInputError, ProviderNotConnectedError, ProviderNotReadyError, RelayfileSetupError, UnknownProviderError } from "./setup-errors.js";
|
|
4
4
|
export { WORKSPACE_INTEGRATION_PROVIDERS } from "./setup-types.js";
|
|
5
|
+
export { createDefaultMountLauncher, defaultMountLauncher, readMountedWorkspaceStatus } from "./mount-launcher.js";
|
|
5
6
|
export { RelayFileSync } from "./sync.js";
|
|
6
7
|
export { onWrite, pathMatches } from "./onWrite.js";
|
|
7
8
|
export { InvalidStateError, PayloadTooLargeError, QueueFullError, RelayFileApiError, RevisionConflictError } from "./errors.js";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import type { MountLauncher, MountMode, MountedWorkspaceStatus } from "./setup-types.js";
|
|
3
|
+
interface DefaultMountLauncherOptions {
|
|
4
|
+
spawnImpl?: typeof spawn;
|
|
5
|
+
now?: () => number;
|
|
6
|
+
readyPollIntervalMs?: number;
|
|
7
|
+
}
|
|
8
|
+
interface ReadMountedWorkspaceStatusInput {
|
|
9
|
+
localDir: string;
|
|
10
|
+
workspaceId: string;
|
|
11
|
+
remotePath: string;
|
|
12
|
+
mode: MountMode;
|
|
13
|
+
relayfileBaseUrl: string;
|
|
14
|
+
relayfileToken: string;
|
|
15
|
+
expiresAt: string | null;
|
|
16
|
+
suggestedRefreshAt: string | null;
|
|
17
|
+
pid?: number;
|
|
18
|
+
}
|
|
19
|
+
export declare const defaultMountLauncher: MountLauncher;
|
|
20
|
+
export declare function createDefaultMountLauncher(options?: DefaultMountLauncherOptions): MountLauncher;
|
|
21
|
+
export declare function readMountedWorkspaceStatus(input: ReadMountedWorkspaceStatusInput): Promise<MountedWorkspaceStatus>;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { constants as fsConstants, createWriteStream } from "node:fs";
|
|
3
|
+
import { access, mkdir, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { RelayFileClient } from "./client.js";
|
|
8
|
+
import { CloudAbortError, MountModeUnavailableError, MountReadyTimeoutError, RelayfileSetupError } from "./setup-errors.js";
|
|
9
|
+
const DEFAULT_READY_POLL_INTERVAL_MS = 250;
|
|
10
|
+
const DEFAULT_STOP_TIMEOUT_MS = 10_000;
|
|
11
|
+
const LOG_ROTATION_MAX_BYTES = 10 * 1024 * 1024;
|
|
12
|
+
const LOG_ROTATION_FILES = 3;
|
|
13
|
+
const FUSE_UNAVAILABLE_SIGNATURE = "fuse mode is not available in this build";
|
|
14
|
+
export const defaultMountLauncher = createDefaultMountLauncher();
|
|
15
|
+
export function createDefaultMountLauncher(options = {}) {
|
|
16
|
+
return {
|
|
17
|
+
async start(input) {
|
|
18
|
+
return startRelayfileMount(input, options);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export async function readMountedWorkspaceStatus(input) {
|
|
23
|
+
const state = await readMountStateFile(input.localDir);
|
|
24
|
+
if (state && !isMountStateStale(state)) {
|
|
25
|
+
return {
|
|
26
|
+
ready: isMountStateReady(state),
|
|
27
|
+
mode: normalizeMountMode(state.mode) ?? input.mode,
|
|
28
|
+
pid: state.daemon?.pid ?? input.pid,
|
|
29
|
+
lastHeartbeatAt: normalizeIsoString(state.lastHeartbeatAt),
|
|
30
|
+
lastReconcileAt: normalizeIsoString(state.lastReconcileAt),
|
|
31
|
+
lastEventAt: normalizeIsoString(state.lastEventAt),
|
|
32
|
+
expiresAt: input.expiresAt,
|
|
33
|
+
suggestedRefreshAt: input.suggestedRefreshAt,
|
|
34
|
+
pendingWriteback: normalizeInteger(state.pendingWriteback),
|
|
35
|
+
pendingConflicts: normalizeInteger(state.pendingConflicts)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const ready = await probeMountedWorkspace(input);
|
|
39
|
+
return {
|
|
40
|
+
ready,
|
|
41
|
+
mode: input.mode,
|
|
42
|
+
pid: input.pid,
|
|
43
|
+
expiresAt: input.expiresAt,
|
|
44
|
+
suggestedRefreshAt: input.suggestedRefreshAt
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
async function startRelayfileMount(input, options) {
|
|
48
|
+
const localDir = path.resolve(input.env.RELAYFILE_LOCAL_DIR ?? process.cwd());
|
|
49
|
+
const relayDir = path.join(localDir, ".relay");
|
|
50
|
+
const logPath = path.join(relayDir, "mount.log");
|
|
51
|
+
const pidPath = path.join(relayDir, "mount.pid");
|
|
52
|
+
await mkdir(relayDir, { recursive: true });
|
|
53
|
+
await rotateMountLogIfNeeded(logPath);
|
|
54
|
+
const command = await resolveRelayfileMountCommand();
|
|
55
|
+
const args = input.background === false ? ["--once"] : [];
|
|
56
|
+
const child = (options.spawnImpl ?? spawn)(command, args, {
|
|
57
|
+
cwd: input.cwd ?? localDir,
|
|
58
|
+
env: {
|
|
59
|
+
...process.env,
|
|
60
|
+
...input.env
|
|
61
|
+
},
|
|
62
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
63
|
+
});
|
|
64
|
+
const outputBuffer = [];
|
|
65
|
+
const logStream = createWriteStream(logPath, { flags: "a" });
|
|
66
|
+
pipeChildOutput(child, logStream, outputBuffer, input);
|
|
67
|
+
if (typeof child.pid === "number" && child.pid > 0) {
|
|
68
|
+
await writeAtomicFile(pidPath, `${child.pid}\n`);
|
|
69
|
+
}
|
|
70
|
+
return new RelayfileMountProcessInstance({
|
|
71
|
+
child,
|
|
72
|
+
logStream,
|
|
73
|
+
pidPath,
|
|
74
|
+
outputBuffer,
|
|
75
|
+
input,
|
|
76
|
+
localDir,
|
|
77
|
+
now: options.now ?? Date.now,
|
|
78
|
+
readyPollIntervalMs: options.readyPollIntervalMs ?? DEFAULT_READY_POLL_INTERVAL_MS
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
class RelayfileMountProcessInstance {
|
|
82
|
+
pid;
|
|
83
|
+
ready;
|
|
84
|
+
child;
|
|
85
|
+
logStream;
|
|
86
|
+
pidPath;
|
|
87
|
+
outputBuffer;
|
|
88
|
+
input;
|
|
89
|
+
localDir;
|
|
90
|
+
now;
|
|
91
|
+
readyPollIntervalMs;
|
|
92
|
+
exited = false;
|
|
93
|
+
stopping;
|
|
94
|
+
readyResolved = false;
|
|
95
|
+
constructor(input) {
|
|
96
|
+
this.child = input.child;
|
|
97
|
+
this.logStream = input.logStream;
|
|
98
|
+
this.pidPath = input.pidPath;
|
|
99
|
+
this.outputBuffer = input.outputBuffer;
|
|
100
|
+
this.input = input.input;
|
|
101
|
+
this.localDir = input.localDir;
|
|
102
|
+
this.pid = input.child.pid ?? undefined;
|
|
103
|
+
this.now = input.now;
|
|
104
|
+
this.readyPollIntervalMs = input.readyPollIntervalMs;
|
|
105
|
+
this.child.once("exit", () => {
|
|
106
|
+
this.exited = true;
|
|
107
|
+
});
|
|
108
|
+
this.ready = this.waitForReady();
|
|
109
|
+
}
|
|
110
|
+
async status() {
|
|
111
|
+
return readMountedWorkspaceStatus({
|
|
112
|
+
localDir: this.localDir,
|
|
113
|
+
workspaceId: this.input.env.RELAYFILE_WORKSPACE ?? "",
|
|
114
|
+
remotePath: this.input.env.RELAYFILE_REMOTE_PATH ?? "/",
|
|
115
|
+
mode: normalizeMountMode(this.input.env.RELAYFILE_MOUNT_MODE) ?? "poll",
|
|
116
|
+
relayfileBaseUrl: this.input.env.RELAYFILE_BASE_URL ?? "",
|
|
117
|
+
relayfileToken: this.input.env.RELAYFILE_TOKEN ?? "",
|
|
118
|
+
expiresAt: null,
|
|
119
|
+
suggestedRefreshAt: null,
|
|
120
|
+
pid: this.pid
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
async stop() {
|
|
124
|
+
if (!this.stopping) {
|
|
125
|
+
this.stopping = this.performStop();
|
|
126
|
+
}
|
|
127
|
+
await this.stopping;
|
|
128
|
+
}
|
|
129
|
+
async waitForReady() {
|
|
130
|
+
const startedAt = this.now();
|
|
131
|
+
const timeoutAt = startedAt + this.input.readyTimeoutMs;
|
|
132
|
+
for (;;) {
|
|
133
|
+
if (this.input.signal?.aborted) {
|
|
134
|
+
await this.stop();
|
|
135
|
+
throw new CloudAbortError("mountWorkspace");
|
|
136
|
+
}
|
|
137
|
+
const status = await this.status();
|
|
138
|
+
if (status.ready) {
|
|
139
|
+
this.readyResolved = true;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (this.isFuseUnavailable()) {
|
|
143
|
+
throw new MountModeUnavailableError("fuse");
|
|
144
|
+
}
|
|
145
|
+
if (this.exited) {
|
|
146
|
+
throw this.buildEarlyExitError();
|
|
147
|
+
}
|
|
148
|
+
if (this.now() >= timeoutAt) {
|
|
149
|
+
const error = new MountReadyTimeoutError(this.localDir, this.input.readyTimeoutMs);
|
|
150
|
+
await this.stop();
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
await delay(this.readyPollIntervalMs);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
buildEarlyExitError() {
|
|
157
|
+
if (this.isFuseUnavailable()) {
|
|
158
|
+
return new MountModeUnavailableError("fuse");
|
|
159
|
+
}
|
|
160
|
+
return new RelayfileSetupError("relayfile-mount exited before the workspace became ready.", "mount_launch_failed");
|
|
161
|
+
}
|
|
162
|
+
isFuseUnavailable() {
|
|
163
|
+
return (normalizeMountMode(this.input.env.RELAYFILE_MOUNT_MODE) === "fuse" &&
|
|
164
|
+
this.outputBuffer.join("").includes(FUSE_UNAVAILABLE_SIGNATURE));
|
|
165
|
+
}
|
|
166
|
+
async performStop() {
|
|
167
|
+
if (!this.exited && typeof this.child.pid === "number") {
|
|
168
|
+
this.child.kill("SIGTERM");
|
|
169
|
+
await waitForExit(this.child, DEFAULT_STOP_TIMEOUT_MS);
|
|
170
|
+
}
|
|
171
|
+
if (!this.exited && typeof this.child.pid === "number") {
|
|
172
|
+
this.child.kill("SIGKILL");
|
|
173
|
+
await waitForExit(this.child, 1_000);
|
|
174
|
+
}
|
|
175
|
+
if (!this.readyResolved) {
|
|
176
|
+
this.exited = true;
|
|
177
|
+
}
|
|
178
|
+
this.logStream.end();
|
|
179
|
+
await unlinkIfExists(this.pidPath);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function pipeChildOutput(child, logStream, outputBuffer, input) {
|
|
183
|
+
const appendChunk = (streamName, chunk) => {
|
|
184
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
185
|
+
logStream.write(text);
|
|
186
|
+
outputBuffer.push(text);
|
|
187
|
+
if (outputBuffer.length > 32) {
|
|
188
|
+
outputBuffer.splice(0, outputBuffer.length - 32);
|
|
189
|
+
}
|
|
190
|
+
input.onEvent?.({
|
|
191
|
+
type: streamName,
|
|
192
|
+
text
|
|
193
|
+
});
|
|
194
|
+
};
|
|
195
|
+
child.stdout?.on("data", (chunk) => {
|
|
196
|
+
appendChunk("stdout", chunk);
|
|
197
|
+
});
|
|
198
|
+
child.stderr?.on("data", (chunk) => {
|
|
199
|
+
appendChunk("stderr", chunk);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
async function probeMountedWorkspace(input) {
|
|
203
|
+
const client = new RelayFileClient({
|
|
204
|
+
baseUrl: input.relayfileBaseUrl,
|
|
205
|
+
token: input.relayfileToken
|
|
206
|
+
});
|
|
207
|
+
try {
|
|
208
|
+
await client.listTree(input.workspaceId, {
|
|
209
|
+
path: input.remotePath,
|
|
210
|
+
depth: 1
|
|
211
|
+
});
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function readMountStateFile(localDir) {
|
|
219
|
+
const statePath = path.join(localDir, ".relay", "state.json");
|
|
220
|
+
try {
|
|
221
|
+
const payload = await readFile(statePath, "utf8");
|
|
222
|
+
const parsed = JSON.parse(payload);
|
|
223
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
return parsed;
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function isMountStateReady(state) {
|
|
233
|
+
if (!normalizeIsoString(state.lastReconcileAt)) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
const providers = Array.isArray(state.providers) ? state.providers : [];
|
|
237
|
+
return providers.every((provider) => {
|
|
238
|
+
const status = normalizeNonEmptyString(provider?.status);
|
|
239
|
+
return status === undefined || status === "ready" || status === "syncing" || status === "unknown";
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
function isMountStateStale(state) {
|
|
243
|
+
const lastReconcileAt = normalizeIsoString(state.lastReconcileAt);
|
|
244
|
+
const intervalMs = normalizeInteger(state.intervalMs);
|
|
245
|
+
if (!lastReconcileAt || !intervalMs || intervalMs <= 0) {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
const reconciledAt = Date.parse(lastReconcileAt);
|
|
249
|
+
return !Number.isNaN(reconciledAt) && Date.now() - reconciledAt > intervalMs * 2;
|
|
250
|
+
}
|
|
251
|
+
function normalizeMountMode(mode) {
|
|
252
|
+
return mode === "fuse" ? "fuse" : mode === "poll" ? "poll" : undefined;
|
|
253
|
+
}
|
|
254
|
+
function normalizeIsoString(value) {
|
|
255
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
return Number.isNaN(Date.parse(value)) ? undefined : value;
|
|
259
|
+
}
|
|
260
|
+
function normalizeInteger(value) {
|
|
261
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
262
|
+
? Math.trunc(value)
|
|
263
|
+
: undefined;
|
|
264
|
+
}
|
|
265
|
+
function normalizeNonEmptyString(value) {
|
|
266
|
+
return typeof value === "string" && value.trim() !== "" ? value : undefined;
|
|
267
|
+
}
|
|
268
|
+
async function resolveRelayfileMountCommand() {
|
|
269
|
+
const executableFromPath = await findExecutableInPath("relayfile-mount");
|
|
270
|
+
if (executableFromPath) {
|
|
271
|
+
return executableFromPath;
|
|
272
|
+
}
|
|
273
|
+
const candidates = [
|
|
274
|
+
process.env.RELAYFILE_MOUNT_BIN,
|
|
275
|
+
fileURLToPath(new URL("../../../../bin/relayfile-mount", import.meta.url))
|
|
276
|
+
];
|
|
277
|
+
for (const candidate of candidates) {
|
|
278
|
+
if (!candidate) {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (await isExecutable(candidate)) {
|
|
282
|
+
return candidate;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return "relayfile-mount";
|
|
286
|
+
}
|
|
287
|
+
async function findExecutableInPath(command) {
|
|
288
|
+
const pathValue = process.env.PATH ?? "";
|
|
289
|
+
const pathEntries = pathValue.split(path.delimiter).filter(Boolean);
|
|
290
|
+
for (const entry of pathEntries) {
|
|
291
|
+
const candidate = path.join(entry, command);
|
|
292
|
+
if (await isExecutable(candidate)) {
|
|
293
|
+
return candidate;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
async function isExecutable(candidate) {
|
|
299
|
+
try {
|
|
300
|
+
await access(candidate, fsConstants.X_OK);
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
async function rotateMountLogIfNeeded(logPath) {
|
|
308
|
+
try {
|
|
309
|
+
const info = await stat(logPath);
|
|
310
|
+
if (info.size < LOG_ROTATION_MAX_BYTES) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const oldest = `${logPath}.${LOG_ROTATION_FILES}`;
|
|
318
|
+
await unlinkIfExists(oldest);
|
|
319
|
+
for (let index = LOG_ROTATION_FILES - 1; index >= 1; index -= 1) {
|
|
320
|
+
const source = `${logPath}.${index}`;
|
|
321
|
+
const target = `${logPath}.${index + 1}`;
|
|
322
|
+
try {
|
|
323
|
+
await rename(source, target);
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
// ignore missing rotation slots
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
await rename(logPath, `${logPath}.1`);
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
// ignore missing active log
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
async function writeAtomicFile(targetPath, content) {
|
|
337
|
+
const tempPath = `${targetPath}.tmp-${process.pid}-${Date.now()}`;
|
|
338
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
339
|
+
await writeFile(tempPath, content, "utf8");
|
|
340
|
+
await rename(tempPath, targetPath);
|
|
341
|
+
}
|
|
342
|
+
async function waitForExit(child, timeoutMs) {
|
|
343
|
+
if (child.exitCode !== null) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
await new Promise((resolve) => {
|
|
347
|
+
const timer = setTimeout(() => {
|
|
348
|
+
child.removeListener("exit", onExit);
|
|
349
|
+
resolve();
|
|
350
|
+
}, timeoutMs);
|
|
351
|
+
const onExit = () => {
|
|
352
|
+
clearTimeout(timer);
|
|
353
|
+
child.removeListener("exit", onExit);
|
|
354
|
+
resolve();
|
|
355
|
+
};
|
|
356
|
+
child.once("exit", onExit);
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
async function unlinkIfExists(targetPath) {
|
|
360
|
+
try {
|
|
361
|
+
await unlink(targetPath);
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
// ignore
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
async function delay(delayMs) {
|
|
368
|
+
await new Promise((resolve) => {
|
|
369
|
+
setTimeout(resolve, delayMs);
|
|
370
|
+
});
|
|
371
|
+
}
|
package/dist/onWrite.js
CHANGED
|
@@ -38,6 +38,7 @@ export function onWrite(pattern, handler, options = {}) {
|
|
|
38
38
|
if (!workspaceId) {
|
|
39
39
|
throw new Error("onWrite requires options.workspaceId or RELAYFILE_WORKSPACE_ID.");
|
|
40
40
|
}
|
|
41
|
+
const baseUrl = resolveOnWriteBaseUrl(options, client);
|
|
41
42
|
const operations = new Set(options.operations ?? DEFAULT_OPERATIONS);
|
|
42
43
|
for (const operation of operations) {
|
|
43
44
|
if (operation !== "create" && operation !== "update" && operation !== "delete") {
|
|
@@ -65,7 +66,7 @@ export function onWrite(pattern, handler, options = {}) {
|
|
|
65
66
|
dispatcher.register(registration, {
|
|
66
67
|
workspaceId,
|
|
67
68
|
signal: options.signal,
|
|
68
|
-
baseUrl
|
|
69
|
+
baseUrl,
|
|
69
70
|
token: options.token,
|
|
70
71
|
webSocketFactory: options.webSocketFactory,
|
|
71
72
|
pingIntervalMs: options.pingIntervalMs,
|
|
@@ -146,7 +147,7 @@ class OnWriteDispatcher {
|
|
|
146
147
|
this.sync = RelayFileSync.connect({
|
|
147
148
|
client: this.client,
|
|
148
149
|
workspaceId: options.workspaceId,
|
|
149
|
-
baseUrl: options.baseUrl ??
|
|
150
|
+
baseUrl: options.baseUrl ?? DEFAULT_RELAYFILE_BASE_URL,
|
|
150
151
|
token,
|
|
151
152
|
reconnect: {
|
|
152
153
|
minDelayMs: DEFAULT_RECONNECT_MIN_DELAY_MS,
|
|
@@ -330,6 +331,18 @@ function getDefaultClient() {
|
|
|
330
331
|
}
|
|
331
332
|
return defaultClient;
|
|
332
333
|
}
|
|
334
|
+
function resolveOnWriteBaseUrl(options, client) {
|
|
335
|
+
if (options.baseUrl) {
|
|
336
|
+
return options.baseUrl;
|
|
337
|
+
}
|
|
338
|
+
if (!options.client) {
|
|
339
|
+
return readEnv("RELAYFILE_BASE_URL") ?? DEFAULT_RELAYFILE_BASE_URL;
|
|
340
|
+
}
|
|
341
|
+
if (client instanceof RelayFileClient) {
|
|
342
|
+
return client.getBaseUrl();
|
|
343
|
+
}
|
|
344
|
+
return DEFAULT_RELAYFILE_BASE_URL;
|
|
345
|
+
}
|
|
333
346
|
function readEnv(name) {
|
|
334
347
|
return globalThis.process?.env?.[name];
|
|
335
348
|
}
|
package/dist/setup-errors.d.ts
CHANGED
|
@@ -42,3 +42,41 @@ export declare class MissingConnectionIdError extends RelayfileSetupError {
|
|
|
42
42
|
readonly provider: WorkspaceIntegrationProvider;
|
|
43
43
|
constructor(provider: WorkspaceIntegrationProvider);
|
|
44
44
|
}
|
|
45
|
+
export declare class MountSessionInputError extends RelayfileSetupError {
|
|
46
|
+
constructor(message: string);
|
|
47
|
+
}
|
|
48
|
+
export declare class InvalidMountModeError extends RelayfileSetupError {
|
|
49
|
+
readonly mode: string;
|
|
50
|
+
constructor(mode: string);
|
|
51
|
+
}
|
|
52
|
+
export declare class InvalidLocalDirError extends RelayfileSetupError {
|
|
53
|
+
readonly localDir: string;
|
|
54
|
+
constructor(localDir: string, message?: string);
|
|
55
|
+
}
|
|
56
|
+
export declare class InvalidRemotePathError extends RelayfileSetupError {
|
|
57
|
+
readonly remotePath: string;
|
|
58
|
+
constructor(remotePath: string, message?: string);
|
|
59
|
+
}
|
|
60
|
+
export declare class MountModeUnavailableError extends RelayfileSetupError {
|
|
61
|
+
readonly mode: string;
|
|
62
|
+
constructor(mode: string, message?: string);
|
|
63
|
+
}
|
|
64
|
+
export declare class MountReadyTimeoutError extends RelayfileSetupError {
|
|
65
|
+
readonly localDir: string;
|
|
66
|
+
readonly timeoutMs: number;
|
|
67
|
+
constructor(localDir: string, timeoutMs: number);
|
|
68
|
+
}
|
|
69
|
+
export declare class ProviderNotConnectedError extends RelayfileSetupError {
|
|
70
|
+
readonly provider: WorkspaceIntegrationProvider;
|
|
71
|
+
constructor(provider: WorkspaceIntegrationProvider);
|
|
72
|
+
}
|
|
73
|
+
export declare class ProviderNotReadyError extends RelayfileSetupError {
|
|
74
|
+
readonly provider: WorkspaceIntegrationProvider;
|
|
75
|
+
readonly state?: string;
|
|
76
|
+
readonly initialSyncState?: string;
|
|
77
|
+
constructor(input: {
|
|
78
|
+
provider: WorkspaceIntegrationProvider;
|
|
79
|
+
state?: string;
|
|
80
|
+
initialSyncState?: string;
|
|
81
|
+
});
|
|
82
|
+
}
|
package/dist/setup-errors.js
CHANGED
|
@@ -69,6 +69,71 @@ export class MissingConnectionIdError extends RelayfileSetupError {
|
|
|
69
69
|
this.provider = provider;
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
+
export class MountSessionInputError extends RelayfileSetupError {
|
|
73
|
+
constructor(message) {
|
|
74
|
+
super(message, "mount_session_input_error");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export class InvalidMountModeError extends RelayfileSetupError {
|
|
78
|
+
mode;
|
|
79
|
+
constructor(mode) {
|
|
80
|
+
super(`Invalid mount mode "${mode}". Expected "poll" or "fuse".`, "invalid_mount_mode");
|
|
81
|
+
this.mode = mode;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export class InvalidLocalDirError extends RelayfileSetupError {
|
|
85
|
+
localDir;
|
|
86
|
+
constructor(localDir, message) {
|
|
87
|
+
super(message ?? `Invalid localDir "${localDir}" for mount session.`, "invalid_local_dir");
|
|
88
|
+
this.localDir = localDir;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export class InvalidRemotePathError extends RelayfileSetupError {
|
|
92
|
+
remotePath;
|
|
93
|
+
constructor(remotePath, message) {
|
|
94
|
+
super(message ?? `Invalid remotePath "${remotePath}" for mount session.`, "invalid_remote_path");
|
|
95
|
+
this.remotePath = remotePath;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export class MountModeUnavailableError extends RelayfileSetupError {
|
|
99
|
+
mode;
|
|
100
|
+
constructor(mode, message) {
|
|
101
|
+
super(message ?? `Mount mode "${mode}" is unavailable in this environment.`, "mount_mode_unavailable");
|
|
102
|
+
this.mode = mode;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export class MountReadyTimeoutError extends RelayfileSetupError {
|
|
106
|
+
localDir;
|
|
107
|
+
timeoutMs;
|
|
108
|
+
constructor(localDir, timeoutMs) {
|
|
109
|
+
super(`Timed out waiting for the mount at ${localDir} to become ready after ${timeoutMs}ms.`, "mount_ready_timeout");
|
|
110
|
+
this.localDir = localDir;
|
|
111
|
+
this.timeoutMs = timeoutMs;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
export class ProviderNotConnectedError extends RelayfileSetupError {
|
|
115
|
+
provider;
|
|
116
|
+
constructor(provider) {
|
|
117
|
+
super(`Provider "${provider}" is not connected for this workspace.`, "provider_not_connected");
|
|
118
|
+
this.provider = provider;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
export class ProviderNotReadyError extends RelayfileSetupError {
|
|
122
|
+
provider;
|
|
123
|
+
state;
|
|
124
|
+
initialSyncState;
|
|
125
|
+
constructor(input) {
|
|
126
|
+
const details = [input.state, input.initialSyncState]
|
|
127
|
+
.filter((value) => typeof value === "string" && value.trim() !== "")
|
|
128
|
+
.join(", ");
|
|
129
|
+
super(details
|
|
130
|
+
? `Provider "${input.provider}" is connected but not ready (${details}).`
|
|
131
|
+
: `Provider "${input.provider}" is connected but not ready.`, "provider_not_ready");
|
|
132
|
+
this.provider = input.provider;
|
|
133
|
+
this.state = input.state;
|
|
134
|
+
this.initialSyncState = input.initialSyncState;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
72
137
|
function readCloudErrorMessage(httpBody) {
|
|
73
138
|
if (!httpBody || typeof httpBody !== "object" || Array.isArray(httpBody)) {
|
|
74
139
|
return undefined;
|
package/dist/setup-types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { WorkspaceHandle } from "./setup.js";
|
|
1
2
|
import type { AccessTokenProvider } from "./client.js";
|
|
2
3
|
export declare const WORKSPACE_INTEGRATION_PROVIDERS: readonly ["github", "slack-sage", "slack-my-senior-dev", "slack-nightcto", "notion", "linear"];
|
|
3
4
|
export type WorkspaceIntegrationProvider = (typeof WORKSPACE_INTEGRATION_PROVIDERS)[number];
|
|
@@ -65,6 +66,108 @@ export interface WorkspaceMountEnvOptions {
|
|
|
65
66
|
relaycastBaseUrl?: string;
|
|
66
67
|
}
|
|
67
68
|
export type WorkspaceMountEnv = Record<string, string>;
|
|
69
|
+
export type MountMode = "poll" | "fuse";
|
|
70
|
+
export interface MountSessionRequest {
|
|
71
|
+
localDir: string;
|
|
72
|
+
remotePath?: string;
|
|
73
|
+
mode?: MountMode;
|
|
74
|
+
agentName?: string;
|
|
75
|
+
scopes?: string[];
|
|
76
|
+
provider?: WorkspaceIntegrationProvider;
|
|
77
|
+
}
|
|
78
|
+
export interface MountSessionResponse {
|
|
79
|
+
workspaceId: string;
|
|
80
|
+
relayfileBaseUrl: string;
|
|
81
|
+
relayfileToken: string;
|
|
82
|
+
wsUrl: string;
|
|
83
|
+
remotePath: string;
|
|
84
|
+
localDir: string;
|
|
85
|
+
mode: MountMode;
|
|
86
|
+
scopes: string[];
|
|
87
|
+
tokenIssuedAt: string | null;
|
|
88
|
+
expiresAt: string | null;
|
|
89
|
+
suggestedRefreshAt: string | null;
|
|
90
|
+
relaycastApiKey: string;
|
|
91
|
+
relaycastBaseUrl?: string;
|
|
92
|
+
}
|
|
93
|
+
export interface MountSessionResult {
|
|
94
|
+
workspaceId: string;
|
|
95
|
+
relayfileBaseUrl: string;
|
|
96
|
+
relayfileToken: string;
|
|
97
|
+
wsUrl: string;
|
|
98
|
+
remotePath: string;
|
|
99
|
+
localDir: string;
|
|
100
|
+
mode: MountMode;
|
|
101
|
+
scopes: string[];
|
|
102
|
+
tokenIssuedAt: string | null;
|
|
103
|
+
expiresAt: string | null;
|
|
104
|
+
suggestedRefreshAt: string | null;
|
|
105
|
+
relaycastApiKey: string;
|
|
106
|
+
relaycastBaseUrl?: string;
|
|
107
|
+
}
|
|
108
|
+
export interface MountedWorkspaceStatus {
|
|
109
|
+
ready: boolean;
|
|
110
|
+
mode: MountMode;
|
|
111
|
+
pid?: number;
|
|
112
|
+
lastHeartbeatAt?: string;
|
|
113
|
+
lastReconcileAt?: string;
|
|
114
|
+
lastEventAt?: string;
|
|
115
|
+
expiresAt: string | null;
|
|
116
|
+
suggestedRefreshAt: string | null;
|
|
117
|
+
pendingWriteback?: number;
|
|
118
|
+
pendingConflicts?: number;
|
|
119
|
+
}
|
|
120
|
+
export interface MountedWorkspaceHandle {
|
|
121
|
+
readonly workspaceId: string;
|
|
122
|
+
readonly localDir: string;
|
|
123
|
+
readonly remotePath: string;
|
|
124
|
+
readonly mode: MountMode;
|
|
125
|
+
readonly ready: boolean;
|
|
126
|
+
readonly expiresAt: string | null;
|
|
127
|
+
readonly suggestedRefreshAt: string | null;
|
|
128
|
+
env(): Record<string, string>;
|
|
129
|
+
status(): Promise<MountedWorkspaceStatus>;
|
|
130
|
+
stop(): Promise<void>;
|
|
131
|
+
}
|
|
132
|
+
export interface MountLauncherEvent {
|
|
133
|
+
type: string;
|
|
134
|
+
[key: string]: unknown;
|
|
135
|
+
}
|
|
136
|
+
export interface MountLauncherStart {
|
|
137
|
+
env: Record<string, string>;
|
|
138
|
+
cwd?: string;
|
|
139
|
+
signal?: AbortSignal;
|
|
140
|
+
readyTimeoutMs: number;
|
|
141
|
+
onEvent?: (event: MountLauncherEvent) => void;
|
|
142
|
+
background?: boolean;
|
|
143
|
+
}
|
|
144
|
+
export interface MountLauncherInstance {
|
|
145
|
+
pid?: number;
|
|
146
|
+
ready: Promise<void>;
|
|
147
|
+
status(): Promise<MountedWorkspaceStatus>;
|
|
148
|
+
stop(): Promise<void>;
|
|
149
|
+
}
|
|
150
|
+
export interface MountLauncher {
|
|
151
|
+
start(input: MountLauncherStart): Promise<MountLauncherInstance>;
|
|
152
|
+
}
|
|
153
|
+
export interface MountWorkspaceInput {
|
|
154
|
+
workspace?: WorkspaceHandle;
|
|
155
|
+
workspaceId?: string;
|
|
156
|
+
localDir: string;
|
|
157
|
+
remotePath?: string;
|
|
158
|
+
mode?: MountMode;
|
|
159
|
+
background?: boolean;
|
|
160
|
+
agentName?: string;
|
|
161
|
+
scopes?: string[];
|
|
162
|
+
signal?: AbortSignal;
|
|
163
|
+
launcher?: MountLauncher;
|
|
164
|
+
readyTimeoutMs?: number;
|
|
165
|
+
}
|
|
166
|
+
export interface EnsureMountedWorkspaceInput extends MountWorkspaceInput {
|
|
167
|
+
provider?: WorkspaceIntegrationProvider;
|
|
168
|
+
verifyProvider?: boolean;
|
|
169
|
+
providerReadyTimeoutMs?: number;
|
|
170
|
+
}
|
|
68
171
|
export interface AgentWorkspaceInviteOptions {
|
|
69
172
|
agentName?: string;
|
|
70
173
|
relaycastBaseUrl?: string;
|
package/dist/setup.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { RelayFileClient, type AccessTokenProvider } from "./client.js";
|
|
2
2
|
import { type RelayfileCloudLoginOptions, type RelayfileCloudTokenSet, type RelayfileCloudTokenSetupOptions } from "./cloud-login.js";
|
|
3
|
-
import { type AgentWorkspaceInvite, type AgentWorkspaceInviteOptions, type AgentWorkspaceScopedInviteOptions, type ConnectIntegrationOptions, type ConnectIntegrationResult, type CreateWorkspaceOptions, type JoinWorkspaceOptions, type RelayfileSetupOptions, type WaitForConnectionOptions, type WorkspaceMountEnv, type WorkspaceMountEnvOptions, type WorkspaceInfo, type WorkspaceIntegrationProvider, type WorkspacePermissions } from "./setup-types.js";
|
|
3
|
+
import { type EnsureMountedWorkspaceInput, type AgentWorkspaceInvite, type AgentWorkspaceInviteOptions, type AgentWorkspaceScopedInviteOptions, type ConnectIntegrationOptions, type ConnectIntegrationResult, type CreateWorkspaceOptions, type JoinWorkspaceOptions, type MountedWorkspaceHandle, type MountWorkspaceInput, type RelayfileSetupOptions, type WaitForConnectionOptions, type WorkspaceMountEnv, type WorkspaceMountEnvOptions, type WorkspaceInfo, type WorkspaceIntegrationProvider, type WorkspacePermissions } from "./setup-types.js";
|
|
4
4
|
export { RELAYFILE_SDK_VERSION } from "./version.js";
|
|
5
5
|
interface JoinWorkspaceResponse {
|
|
6
6
|
workspaceId?: string;
|
|
@@ -16,6 +16,11 @@ interface NormalizedJoinWorkspaceOptions {
|
|
|
16
16
|
scopes: string[];
|
|
17
17
|
permissions?: WorkspacePermissions;
|
|
18
18
|
}
|
|
19
|
+
interface ValidatedIntegrationStatusResponse {
|
|
20
|
+
ready: boolean;
|
|
21
|
+
state?: string;
|
|
22
|
+
initialSyncState?: string;
|
|
23
|
+
}
|
|
19
24
|
interface CloudRequestOptions {
|
|
20
25
|
operation: string;
|
|
21
26
|
method: string;
|
|
@@ -41,11 +46,15 @@ export declare class RelayfileSetup {
|
|
|
41
46
|
constructor(options?: RelayfileSetupOptions);
|
|
42
47
|
createWorkspace(options?: CreateWorkspaceOptions): Promise<WorkspaceHandle>;
|
|
43
48
|
joinWorkspace(workspaceId: string, options?: JoinWorkspaceOptions): Promise<WorkspaceHandle>;
|
|
49
|
+
mountWorkspace(input: MountWorkspaceInput): Promise<MountedWorkspaceHandle>;
|
|
50
|
+
ensureMountedWorkspace(input: EnsureMountedWorkspaceInput): Promise<MountedWorkspaceHandle>;
|
|
44
51
|
joinWorkspaceResponse(workspaceId: string, options: NormalizedJoinWorkspaceOptions, overrides?: {
|
|
45
52
|
tokenProvider?: AccessTokenProvider;
|
|
46
53
|
}): Promise<ValidatedJoinWorkspaceResponse>;
|
|
47
54
|
requestJson(options: CloudRequestOptions): Promise<unknown>;
|
|
48
55
|
getCloudApiUrl(): string;
|
|
56
|
+
private resolveWorkspaceForMount;
|
|
57
|
+
private createMountSession;
|
|
49
58
|
}
|
|
50
59
|
export declare class WorkspaceHandle {
|
|
51
60
|
readonly info: WorkspaceInfo;
|
|
@@ -66,6 +75,7 @@ export declare class WorkspaceHandle {
|
|
|
66
75
|
isConnected(provider: WorkspaceIntegrationProvider, connectionId: string): Promise<boolean>;
|
|
67
76
|
disconnectIntegration(provider: WorkspaceIntegrationProvider, _connectionId?: string): Promise<void>;
|
|
68
77
|
getToken(): string;
|
|
78
|
+
requestJson(options: Omit<CloudRequestOptions, "tokenProvider">): Promise<unknown>;
|
|
69
79
|
mountEnv(options?: WorkspaceMountEnvOptions): WorkspaceMountEnv;
|
|
70
80
|
/**
|
|
71
81
|
* Build an invite that hands a peer agent the calling workspace's existing
|
|
@@ -99,6 +109,9 @@ export declare class WorkspaceHandle {
|
|
|
99
109
|
private performRefreshToken;
|
|
100
110
|
private getOrRefreshToken;
|
|
101
111
|
private resolveConnectionId;
|
|
102
|
-
|
|
112
|
+
getConnectionStatus(provider: WorkspaceIntegrationProvider, connectionId: string, options?: {
|
|
113
|
+
signal?: AbortSignal;
|
|
114
|
+
timeoutMs?: number;
|
|
115
|
+
}): Promise<ValidatedIntegrationStatusResponse>;
|
|
103
116
|
private resolveRelaycastBaseUrl;
|
|
104
117
|
}
|
package/dist/setup.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import { RelayFileClient } from "./client.js";
|
|
2
3
|
import { createRelayfileCloudAccessTokenProvider, runRelayfileCloudLogin } from "./cloud-login.js";
|
|
3
|
-
import { CloudAbortError, CloudApiError, CloudTimeoutError, IntegrationConnectionTimeoutError, MalformedCloudResponseError, MissingConnectionIdError, UnknownProviderError } from "./setup-errors.js";
|
|
4
|
+
import { CloudAbortError, CloudApiError, CloudTimeoutError, InvalidLocalDirError, InvalidMountModeError, InvalidRemotePathError, IntegrationConnectionTimeoutError, MalformedCloudResponseError, MissingConnectionIdError, MountSessionInputError, ProviderNotConnectedError, ProviderNotReadyError, UnknownProviderError } from "./setup-errors.js";
|
|
4
5
|
import { WORKSPACE_INTEGRATION_PROVIDERS } from "./setup-types.js";
|
|
5
6
|
import { RELAYFILE_SDK_VERSION } from "./version.js";
|
|
7
|
+
import { defaultMountLauncher, readMountedWorkspaceStatus } from "./mount-launcher.js";
|
|
6
8
|
export { RELAYFILE_SDK_VERSION } from "./version.js";
|
|
7
9
|
const DEFAULT_CLOUD_API_URL = "https://agentrelay.com/cloud";
|
|
8
10
|
const DEFAULT_RELAYCAST_BASE_URL = "https://api.relaycast.dev";
|
|
@@ -14,6 +16,8 @@ const DEFAULT_AGENT_NAME = "sdk-agent";
|
|
|
14
16
|
const DEFAULT_SCOPES = ["fs:read", "fs:write"];
|
|
15
17
|
const DEFAULT_WAIT_INTERVAL_MS = 2_000;
|
|
16
18
|
const DEFAULT_WAIT_TIMEOUT_MS = 300_000;
|
|
19
|
+
const DEFAULT_MOUNT_READY_TIMEOUT_MS = 60_000;
|
|
20
|
+
const DEFAULT_MOUNT_AGENT_NAME = "relayfile-mount";
|
|
17
21
|
const TOKEN_REFRESH_AGE_MS = 55 * 60 * 1000;
|
|
18
22
|
export class RelayfileSetup {
|
|
19
23
|
cloudApiUrl;
|
|
@@ -99,6 +103,57 @@ export class RelayfileSetup {
|
|
|
99
103
|
joinOptions
|
|
100
104
|
});
|
|
101
105
|
}
|
|
106
|
+
async mountWorkspace(input) {
|
|
107
|
+
const normalized = normalizeMountWorkspaceInput(input);
|
|
108
|
+
const workspace = await this.resolveWorkspaceForMount(normalized);
|
|
109
|
+
const mountSession = await this.createMountSession(workspace, normalized);
|
|
110
|
+
const launcher = normalized.launcher;
|
|
111
|
+
const launcherInstance = await launcher.start({
|
|
112
|
+
env: buildMountLauncherEnv(mountSession),
|
|
113
|
+
signal: normalized.signal,
|
|
114
|
+
readyTimeoutMs: normalized.readyTimeoutMs,
|
|
115
|
+
background: normalized.background
|
|
116
|
+
});
|
|
117
|
+
try {
|
|
118
|
+
await waitForMountReady(launcherInstance.ready, normalized.signal);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
// If the abort signal fired before waitForMountReady got a chance to
|
|
122
|
+
// attach a Promise.race handler, ensureLauncher.ready will reject
|
|
123
|
+
// later. Attach a no-op rejection handler so Node 18+ does not surface
|
|
124
|
+
// an unhandledRejection (which terminates the process by default).
|
|
125
|
+
launcherInstance.ready.catch(() => { });
|
|
126
|
+
await safeStopLauncher(launcherInstance);
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
return new MountedWorkspaceHandleImpl({
|
|
130
|
+
mountSession,
|
|
131
|
+
launcherInstance: normalized.background ? launcherInstance : undefined,
|
|
132
|
+
probeOnly: !normalized.background
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
async ensureMountedWorkspace(input) {
|
|
136
|
+
const normalized = normalizeEnsureMountedWorkspaceInput(input);
|
|
137
|
+
const workspace = await this.resolveWorkspaceForMount(normalized);
|
|
138
|
+
if (normalized.verifyProvider) {
|
|
139
|
+
if (!normalized.provider) {
|
|
140
|
+
throw new MountSessionInputError("provider required when verifyProvider=true");
|
|
141
|
+
}
|
|
142
|
+
await verifyWorkspaceProviderReady(workspace, normalized.provider, normalized.providerReadyTimeoutMs, normalized.signal);
|
|
143
|
+
}
|
|
144
|
+
return this.mountWorkspace({
|
|
145
|
+
workspace,
|
|
146
|
+
localDir: normalized.localDir,
|
|
147
|
+
remotePath: normalized.remotePath,
|
|
148
|
+
mode: normalized.mode,
|
|
149
|
+
background: normalized.background,
|
|
150
|
+
agentName: normalized.agentName,
|
|
151
|
+
scopes: normalized.scopes,
|
|
152
|
+
signal: normalized.signal,
|
|
153
|
+
launcher: normalized.launcher,
|
|
154
|
+
readyTimeoutMs: normalized.readyTimeoutMs
|
|
155
|
+
});
|
|
156
|
+
}
|
|
102
157
|
async joinWorkspaceResponse(workspaceId, options, overrides = {}) {
|
|
103
158
|
return validateJoinWorkspaceResponse(await this.requestJson({
|
|
104
159
|
operation: "joinWorkspace",
|
|
@@ -163,6 +218,38 @@ export class RelayfileSetup {
|
|
|
163
218
|
getCloudApiUrl() {
|
|
164
219
|
return this.cloudApiUrl;
|
|
165
220
|
}
|
|
221
|
+
async resolveWorkspaceForMount(input) {
|
|
222
|
+
if (input.workspace) {
|
|
223
|
+
return input.workspace;
|
|
224
|
+
}
|
|
225
|
+
return this.joinWorkspace(input.workspaceId, {
|
|
226
|
+
agentName: input.agentName,
|
|
227
|
+
scopes: input.scopes
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
async createMountSession(workspace, input) {
|
|
231
|
+
const request = compactObject({
|
|
232
|
+
localDir: input.localDir,
|
|
233
|
+
remotePath: input.remotePath,
|
|
234
|
+
mode: input.mode,
|
|
235
|
+
agentName: normalizeNonEmptyString(input.agentName) ?? DEFAULT_MOUNT_AGENT_NAME,
|
|
236
|
+
scopes: input.scopes && input.scopes.length > 0
|
|
237
|
+
? [...input.scopes]
|
|
238
|
+
: undefined
|
|
239
|
+
});
|
|
240
|
+
try {
|
|
241
|
+
return validateMountSessionResponse(await workspace.requestJson({
|
|
242
|
+
operation: "mountWorkspace",
|
|
243
|
+
method: "POST",
|
|
244
|
+
path: `api/v1/workspaces/${encodeURIComponent(workspace.workspaceId)}/relayfile/mount-session`,
|
|
245
|
+
body: request,
|
|
246
|
+
signal: input.signal
|
|
247
|
+
}), input.localDir);
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
throw mapMountSessionError(error, request);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
166
253
|
}
|
|
167
254
|
export class WorkspaceHandle {
|
|
168
255
|
info;
|
|
@@ -259,10 +346,11 @@ export class WorkspaceHandle {
|
|
|
259
346
|
const remainingMs = timeoutMs - elapsedMs;
|
|
260
347
|
let ready;
|
|
261
348
|
try {
|
|
262
|
-
|
|
349
|
+
const status = await this.getConnectionStatus(provider, connectionId, {
|
|
263
350
|
signal: options.signal,
|
|
264
351
|
timeoutMs: remainingMs
|
|
265
352
|
});
|
|
353
|
+
ready = status.ready;
|
|
266
354
|
}
|
|
267
355
|
catch (error) {
|
|
268
356
|
if (error instanceof CloudTimeoutError) {
|
|
@@ -287,7 +375,8 @@ export class WorkspaceHandle {
|
|
|
287
375
|
}
|
|
288
376
|
async isConnected(provider, connectionId) {
|
|
289
377
|
assertProvider(provider);
|
|
290
|
-
|
|
378
|
+
const status = await this.getConnectionStatus(provider, this.resolveConnectionId(provider, connectionId));
|
|
379
|
+
return status.ready;
|
|
291
380
|
}
|
|
292
381
|
async disconnectIntegration(provider, _connectionId) {
|
|
293
382
|
assertProvider(provider);
|
|
@@ -302,6 +391,12 @@ export class WorkspaceHandle {
|
|
|
302
391
|
getToken() {
|
|
303
392
|
return this._token;
|
|
304
393
|
}
|
|
394
|
+
async requestJson(options) {
|
|
395
|
+
return this._setup.requestJson({
|
|
396
|
+
...options,
|
|
397
|
+
tokenProvider: async () => this.getOrRefreshToken()
|
|
398
|
+
});
|
|
399
|
+
}
|
|
305
400
|
mountEnv(options = {}) {
|
|
306
401
|
const relaycastBaseUrl = this.resolveRelaycastBaseUrl(options.relaycastBaseUrl);
|
|
307
402
|
return compactStringRecord({
|
|
@@ -426,15 +521,13 @@ export class WorkspaceHandle {
|
|
|
426
521
|
}
|
|
427
522
|
async getConnectionStatus(provider, connectionId, options = {}) {
|
|
428
523
|
const query = new URLSearchParams({ connectionId });
|
|
429
|
-
|
|
524
|
+
return validateIntegrationStatusResponse(await this.requestJson({
|
|
430
525
|
operation: "getIntegrationStatus",
|
|
431
526
|
method: "GET",
|
|
432
527
|
path: `api/v1/workspaces/${encodeURIComponent(this.workspaceId)}/integrations/${encodeURIComponent(provider)}/status?${query.toString()}`,
|
|
433
528
|
signal: options.signal,
|
|
434
|
-
timeoutMs: options.timeoutMs
|
|
435
|
-
tokenProvider: async () => this.getOrRefreshToken()
|
|
529
|
+
timeoutMs: options.timeoutMs
|
|
436
530
|
}));
|
|
437
|
-
return response.ready;
|
|
438
531
|
}
|
|
439
532
|
resolveRelaycastBaseUrl(override) {
|
|
440
533
|
return (normalizeNonEmptyString(override) ??
|
|
@@ -442,6 +535,63 @@ export class WorkspaceHandle {
|
|
|
442
535
|
DEFAULT_RELAYCAST_BASE_URL);
|
|
443
536
|
}
|
|
444
537
|
}
|
|
538
|
+
class MountedWorkspaceHandleImpl {
|
|
539
|
+
workspaceId;
|
|
540
|
+
localDir;
|
|
541
|
+
remotePath;
|
|
542
|
+
mode;
|
|
543
|
+
expiresAt;
|
|
544
|
+
suggestedRefreshAt;
|
|
545
|
+
mountSession;
|
|
546
|
+
launcherInstance;
|
|
547
|
+
probeOnly;
|
|
548
|
+
readySnapshot = true;
|
|
549
|
+
stopPromise;
|
|
550
|
+
constructor(input) {
|
|
551
|
+
this.mountSession = input.mountSession;
|
|
552
|
+
this.workspaceId = input.mountSession.workspaceId;
|
|
553
|
+
this.localDir = input.mountSession.localDir;
|
|
554
|
+
this.remotePath = input.mountSession.remotePath;
|
|
555
|
+
this.mode = input.mountSession.mode;
|
|
556
|
+
this.expiresAt = input.mountSession.expiresAt;
|
|
557
|
+
this.suggestedRefreshAt = input.mountSession.suggestedRefreshAt;
|
|
558
|
+
this.launcherInstance = input.launcherInstance;
|
|
559
|
+
this.probeOnly = input.probeOnly;
|
|
560
|
+
}
|
|
561
|
+
get ready() {
|
|
562
|
+
return this.readySnapshot;
|
|
563
|
+
}
|
|
564
|
+
env() {
|
|
565
|
+
return buildMountedWorkspaceEnv(this.mountSession);
|
|
566
|
+
}
|
|
567
|
+
async status() {
|
|
568
|
+
const status = await readMountedWorkspaceStatus({
|
|
569
|
+
localDir: this.localDir,
|
|
570
|
+
workspaceId: this.workspaceId,
|
|
571
|
+
remotePath: this.remotePath,
|
|
572
|
+
mode: this.mode,
|
|
573
|
+
relayfileBaseUrl: this.mountSession.relayfileBaseUrl,
|
|
574
|
+
relayfileToken: this.mountSession.relayfileToken,
|
|
575
|
+
expiresAt: this.expiresAt,
|
|
576
|
+
suggestedRefreshAt: this.suggestedRefreshAt,
|
|
577
|
+
pid: this.launcherInstance?.pid
|
|
578
|
+
});
|
|
579
|
+
this.readySnapshot = status.ready;
|
|
580
|
+
return status;
|
|
581
|
+
}
|
|
582
|
+
async stop() {
|
|
583
|
+
if (!this.stopPromise) {
|
|
584
|
+
this.stopPromise = this.performStop();
|
|
585
|
+
}
|
|
586
|
+
await this.stopPromise;
|
|
587
|
+
}
|
|
588
|
+
async performStop() {
|
|
589
|
+
if (this.probeOnly || !this.launcherInstance) {
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
await safeStopLauncher(this.launcherInstance);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
445
595
|
function assertProvider(provider) {
|
|
446
596
|
if (!WORKSPACE_INTEGRATION_PROVIDERS.includes(provider)) {
|
|
447
597
|
throw new UnknownProviderError(provider);
|
|
@@ -494,7 +644,26 @@ function validateConnectSessionResponse(payload) {
|
|
|
494
644
|
}
|
|
495
645
|
function validateIntegrationStatusResponse(payload) {
|
|
496
646
|
return {
|
|
497
|
-
ready: requireBooleanField(payload, "ready")
|
|
647
|
+
ready: requireBooleanField(payload, "ready"),
|
|
648
|
+
state: readOptionalStringField(payload, "state"),
|
|
649
|
+
initialSyncState: readOptionalNestedStringField(payload, ["initialSync", "state"])
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
function validateMountSessionResponse(payload, localDir) {
|
|
653
|
+
return {
|
|
654
|
+
workspaceId: requireStringField(payload, "workspaceId"),
|
|
655
|
+
relayfileBaseUrl: stripTrailingSlash(requireStringField(payload, "relayfileBaseUrl")),
|
|
656
|
+
relayfileToken: requireStringField(payload, "relayfileToken"),
|
|
657
|
+
wsUrl: requireStringField(payload, "wsUrl"),
|
|
658
|
+
remotePath: requireStringField(payload, "remotePath"),
|
|
659
|
+
localDir,
|
|
660
|
+
mode: requireMountModeField(payload, "mode"),
|
|
661
|
+
scopes: requireStringArrayField(payload, "scopes"),
|
|
662
|
+
tokenIssuedAt: readNullableStringField(payload, "tokenIssuedAt"),
|
|
663
|
+
expiresAt: readNullableStringField(payload, "expiresAt"),
|
|
664
|
+
suggestedRefreshAt: readNullableStringField(payload, "suggestedRefreshAt"),
|
|
665
|
+
relaycastApiKey: requireStringField(payload, "relaycastApiKey"),
|
|
666
|
+
relaycastBaseUrl: readOptionalStringField(payload, "relaycastBaseUrl")
|
|
498
667
|
};
|
|
499
668
|
}
|
|
500
669
|
function requireStringField(payload, field) {
|
|
@@ -514,6 +683,30 @@ function readOptionalStringField(payload, field) {
|
|
|
514
683
|
}
|
|
515
684
|
return value;
|
|
516
685
|
}
|
|
686
|
+
function readNullableStringField(payload, field) {
|
|
687
|
+
const value = readField(payload, field);
|
|
688
|
+
if (value === null || value === undefined) {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
692
|
+
throw new MalformedCloudResponseError(field, payload);
|
|
693
|
+
}
|
|
694
|
+
return value;
|
|
695
|
+
}
|
|
696
|
+
function readOptionalNestedStringField(payload, pathSegments) {
|
|
697
|
+
const parent = readField(payload, pathSegments[0]);
|
|
698
|
+
if (!parent || typeof parent !== "object" || Array.isArray(parent)) {
|
|
699
|
+
return undefined;
|
|
700
|
+
}
|
|
701
|
+
const value = parent[pathSegments[1]];
|
|
702
|
+
if (value === undefined) {
|
|
703
|
+
return undefined;
|
|
704
|
+
}
|
|
705
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
706
|
+
throw new MalformedCloudResponseError(pathSegments.join("."), payload);
|
|
707
|
+
}
|
|
708
|
+
return value;
|
|
709
|
+
}
|
|
517
710
|
function requireBooleanField(payload, field) {
|
|
518
711
|
const value = readField(payload, field);
|
|
519
712
|
if (typeof value !== "boolean") {
|
|
@@ -521,6 +714,20 @@ function requireBooleanField(payload, field) {
|
|
|
521
714
|
}
|
|
522
715
|
return value;
|
|
523
716
|
}
|
|
717
|
+
function requireMountModeField(payload, field) {
|
|
718
|
+
const value = requireStringField(payload, field);
|
|
719
|
+
if (value !== "poll" && value !== "fuse") {
|
|
720
|
+
throw new MalformedCloudResponseError(field, payload);
|
|
721
|
+
}
|
|
722
|
+
return value;
|
|
723
|
+
}
|
|
724
|
+
function requireStringArrayField(payload, field) {
|
|
725
|
+
const value = readField(payload, field);
|
|
726
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
|
|
727
|
+
throw new MalformedCloudResponseError(field, payload);
|
|
728
|
+
}
|
|
729
|
+
return [...value];
|
|
730
|
+
}
|
|
524
731
|
function readField(payload, field) {
|
|
525
732
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
526
733
|
return undefined;
|
|
@@ -530,6 +737,59 @@ function readField(payload, field) {
|
|
|
530
737
|
function normalizeConnectionId(connectionId) {
|
|
531
738
|
return normalizeNonEmptyString(connectionId);
|
|
532
739
|
}
|
|
740
|
+
function normalizeMountWorkspaceInput(input) {
|
|
741
|
+
if (!!input.workspace === !!input.workspaceId) {
|
|
742
|
+
throw new MountSessionInputError("Exactly one of workspace or workspaceId must be provided.");
|
|
743
|
+
}
|
|
744
|
+
const localDir = normalizeNonEmptyString(input.localDir);
|
|
745
|
+
if (!localDir) {
|
|
746
|
+
throw new InvalidLocalDirError(String(input.localDir ?? ""));
|
|
747
|
+
}
|
|
748
|
+
return {
|
|
749
|
+
workspace: input.workspace,
|
|
750
|
+
workspaceId: normalizeNonEmptyString(input.workspaceId),
|
|
751
|
+
localDir: path.resolve(localDir),
|
|
752
|
+
remotePath: normalizeMountRemotePath(input.remotePath),
|
|
753
|
+
mode: normalizeMountModeInput(input.mode),
|
|
754
|
+
background: input.background !== false,
|
|
755
|
+
agentName: normalizeNonEmptyString(input.agentName),
|
|
756
|
+
scopes: input.scopes && input.scopes.length > 0 ? [...input.scopes] : undefined,
|
|
757
|
+
signal: input.signal,
|
|
758
|
+
launcher: input.launcher ?? defaultMountLauncher,
|
|
759
|
+
readyTimeoutMs: Math.max(1, Math.floor(input.readyTimeoutMs ?? DEFAULT_MOUNT_READY_TIMEOUT_MS))
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
function normalizeEnsureMountedWorkspaceInput(input) {
|
|
763
|
+
const normalized = normalizeMountWorkspaceInput(input);
|
|
764
|
+
return {
|
|
765
|
+
...normalized,
|
|
766
|
+
provider: input.provider,
|
|
767
|
+
verifyProvider: input.verifyProvider !== false,
|
|
768
|
+
providerReadyTimeoutMs: Math.max(0, Math.floor(input.providerReadyTimeoutMs ?? 0))
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
function normalizeMountModeInput(mode) {
|
|
772
|
+
const normalized = normalizeNonEmptyString(mode) ?? "poll";
|
|
773
|
+
if (normalized !== "poll" && normalized !== "fuse") {
|
|
774
|
+
throw new InvalidMountModeError(normalized);
|
|
775
|
+
}
|
|
776
|
+
return normalized;
|
|
777
|
+
}
|
|
778
|
+
function normalizeMountRemotePath(remotePath) {
|
|
779
|
+
const normalized = normalizeNonEmptyString(remotePath) ?? "/";
|
|
780
|
+
if (normalized.includes("\u0000")) {
|
|
781
|
+
throw new InvalidRemotePathError(normalized);
|
|
782
|
+
}
|
|
783
|
+
const segments = normalized.replace(/\\/g, "/").split("/");
|
|
784
|
+
if (segments.some((segment) => segment === "..")) {
|
|
785
|
+
throw new InvalidRemotePathError(normalized);
|
|
786
|
+
}
|
|
787
|
+
const resolved = path.posix.normalize(normalized.startsWith("/") ? normalized : `/${normalized}`);
|
|
788
|
+
if (!resolved.startsWith("/")) {
|
|
789
|
+
throw new InvalidRemotePathError(normalized);
|
|
790
|
+
}
|
|
791
|
+
return resolved === "/" ? "/" : resolved.replace(/\/+$/, "");
|
|
792
|
+
}
|
|
533
793
|
function normalizeNonEmptyString(value) {
|
|
534
794
|
const normalized = value?.trim();
|
|
535
795
|
return normalized ? normalized : undefined;
|
|
@@ -552,6 +812,31 @@ function compactStringRecord(value) {
|
|
|
552
812
|
return entryValue !== undefined;
|
|
553
813
|
}));
|
|
554
814
|
}
|
|
815
|
+
function buildMountedWorkspaceEnv(mountSession) {
|
|
816
|
+
const relaycastBaseUrl = normalizeNonEmptyString(mountSession.relaycastBaseUrl) ??
|
|
817
|
+
DEFAULT_RELAYCAST_BASE_URL;
|
|
818
|
+
return compactStringRecord({
|
|
819
|
+
RELAYFILE_BASE_URL: mountSession.relayfileBaseUrl,
|
|
820
|
+
RELAYFILE_TOKEN: mountSession.relayfileToken,
|
|
821
|
+
RELAYFILE_WORKSPACE: mountSession.workspaceId,
|
|
822
|
+
RELAYFILE_REMOTE_PATH: mountSession.remotePath,
|
|
823
|
+
RELAYFILE_LOCAL_DIR: mountSession.localDir,
|
|
824
|
+
RELAYFILE_MOUNT_MODE: mountSession.mode,
|
|
825
|
+
RELAYCAST_API_KEY: mountSession.relaycastApiKey,
|
|
826
|
+
RELAY_API_KEY: mountSession.relaycastApiKey,
|
|
827
|
+
RELAYCAST_BASE_URL: relaycastBaseUrl,
|
|
828
|
+
RELAY_BASE_URL: relaycastBaseUrl
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
function buildMountLauncherEnv(mountSession) {
|
|
832
|
+
return compactStringRecord({
|
|
833
|
+
...buildMountedWorkspaceEnv(mountSession),
|
|
834
|
+
RELAYFILE_MOUNT_SCOPES: mountSession.scopes.join(" ")
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
function stripTrailingSlash(value) {
|
|
838
|
+
return value.replace(/\/+$/, "");
|
|
839
|
+
}
|
|
555
840
|
function buildCloudUrl(baseUrl, path) {
|
|
556
841
|
const base = new URL(baseUrl);
|
|
557
842
|
if (!base.pathname.endsWith("/")) {
|
|
@@ -672,3 +957,100 @@ function throwIfAborted(signal, operation) {
|
|
|
672
957
|
throw new CloudAbortError(operation);
|
|
673
958
|
}
|
|
674
959
|
}
|
|
960
|
+
async function waitForMountReady(ready, signal) {
|
|
961
|
+
if (!signal) {
|
|
962
|
+
await ready;
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
if (signal.aborted) {
|
|
966
|
+
throw new CloudAbortError("mountWorkspace");
|
|
967
|
+
}
|
|
968
|
+
let onAbort;
|
|
969
|
+
try {
|
|
970
|
+
await Promise.race([
|
|
971
|
+
ready,
|
|
972
|
+
new Promise((_, reject) => {
|
|
973
|
+
onAbort = () => {
|
|
974
|
+
signal.removeEventListener("abort", onAbort);
|
|
975
|
+
reject(new CloudAbortError("mountWorkspace"));
|
|
976
|
+
};
|
|
977
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
978
|
+
})
|
|
979
|
+
]);
|
|
980
|
+
}
|
|
981
|
+
finally {
|
|
982
|
+
if (onAbort) {
|
|
983
|
+
signal.removeEventListener("abort", onAbort);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
async function safeStopLauncher(launcherInstance) {
|
|
988
|
+
try {
|
|
989
|
+
await launcherInstance.stop();
|
|
990
|
+
}
|
|
991
|
+
catch {
|
|
992
|
+
// stop is best-effort during cleanup
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
async function verifyWorkspaceProviderReady(workspace, provider, providerReadyTimeoutMs, signal) {
|
|
996
|
+
const startedAt = Date.now();
|
|
997
|
+
const connectionId = workspace.workspaceId;
|
|
998
|
+
for (;;) {
|
|
999
|
+
throwIfAborted(signal, "ensureMountedWorkspace");
|
|
1000
|
+
const elapsedMs = Date.now() - startedAt;
|
|
1001
|
+
const remainingMs = providerReadyTimeoutMs <= 0
|
|
1002
|
+
? undefined
|
|
1003
|
+
: Math.max(1, providerReadyTimeoutMs - elapsedMs);
|
|
1004
|
+
const status = await workspace.getConnectionStatus(provider, connectionId, {
|
|
1005
|
+
signal,
|
|
1006
|
+
timeoutMs: remainingMs
|
|
1007
|
+
});
|
|
1008
|
+
if (status.ready) {
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
// Integration status uses state="not_connected" to signal a provider that
|
|
1012
|
+
// has never been connected. Other non-ready states still indicate a
|
|
1013
|
+
// connected provider and map to ProviderNotReadyError.
|
|
1014
|
+
if (status.state === "not_connected") {
|
|
1015
|
+
throw new ProviderNotConnectedError(provider);
|
|
1016
|
+
}
|
|
1017
|
+
if (providerReadyTimeoutMs <= 0 || elapsedMs >= providerReadyTimeoutMs) {
|
|
1018
|
+
throw new ProviderNotReadyError({
|
|
1019
|
+
provider,
|
|
1020
|
+
state: status.state,
|
|
1021
|
+
initialSyncState: status.initialSyncState
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
const sleepMs = Math.min(DEFAULT_WAIT_INTERVAL_MS, Math.max(1, providerReadyTimeoutMs - elapsedMs));
|
|
1025
|
+
await sleep(sleepMs, signal, "ensureMountedWorkspace");
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
function mapMountSessionError(error, request) {
|
|
1029
|
+
if (!(error instanceof CloudApiError) || error.httpStatus !== 400) {
|
|
1030
|
+
return error;
|
|
1031
|
+
}
|
|
1032
|
+
const code = readCloudErrorCode(error.httpBody);
|
|
1033
|
+
switch (code) {
|
|
1034
|
+
case "invalid_mode":
|
|
1035
|
+
return new InvalidMountModeError(request.mode ?? "poll");
|
|
1036
|
+
case "invalid_local_dir":
|
|
1037
|
+
return new InvalidLocalDirError(request.localDir);
|
|
1038
|
+
case "invalid_remote_path":
|
|
1039
|
+
return new InvalidRemotePathError(request.remotePath ?? "/");
|
|
1040
|
+
default:
|
|
1041
|
+
return error;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
function readCloudErrorCode(httpBody) {
|
|
1045
|
+
if (!httpBody || typeof httpBody !== "object" || Array.isArray(httpBody)) {
|
|
1046
|
+
return undefined;
|
|
1047
|
+
}
|
|
1048
|
+
const payload = httpBody;
|
|
1049
|
+
for (const field of ["code", "error"]) {
|
|
1050
|
+
const value = payload[field];
|
|
1051
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
1052
|
+
return value;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
return undefined;
|
|
1056
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@relayfile/sdk",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
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",
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
"typecheck": "tsc --noEmit",
|
|
14
14
|
"test": "vitest run",
|
|
15
15
|
"test:e2e": "node scripts/setup-e2e.mjs",
|
|
16
|
+
"post-auth-mount:e2e": "node scripts/post-auth-mount-session-e2e.mjs",
|
|
17
|
+
"e2e:post-auth-mount-session": "node scripts/post-auth-mount-session-e2e.mjs",
|
|
16
18
|
"agent-workspace:e2e": "node scripts/agent-workspace-golden-path-e2e.mjs",
|
|
17
19
|
"test:e2e:golden-path": "node scripts/agent-workspace-golden-path-e2e.mjs",
|
|
18
20
|
"demo:agent-workspace": "npm run build && node scripts/agent-workspace-demo.mjs",
|
|
@@ -20,7 +22,7 @@
|
|
|
20
22
|
"prepublishOnly": "npm run build"
|
|
21
23
|
},
|
|
22
24
|
"dependencies": {
|
|
23
|
-
"@relayfile/core": "0.7.
|
|
25
|
+
"@relayfile/core": "0.7.2"
|
|
24
26
|
},
|
|
25
27
|
"devDependencies": {
|
|
26
28
|
"typescript": "^5.7.3",
|