@buda-ai/connector 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -12
- package/dist/cli.js +2559 -1132
- package/package.json +1 -1
- package/dist/chunk-MCKGQKYU.js +0 -15
- package/dist/lib-UX6C35VI.js +0 -1307
package/dist/cli.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import "./chunk-MCKGQKYU.js";
|
|
3
2
|
|
|
4
3
|
// src/config.ts
|
|
5
|
-
import { createHash as createHash2, randomBytes, randomUUID } from "crypto";
|
|
4
|
+
import { createHash as createHash2, randomBytes, randomUUID as randomUUID2 } from "crypto";
|
|
6
5
|
import { chmod, mkdir, readFile as readFile2, rename, unlink as unlink2, writeFile } from "fs/promises";
|
|
7
6
|
import { homedir, hostname, platform as platform2 } from "os";
|
|
8
7
|
import path from "path";
|
|
9
8
|
|
|
10
9
|
// src/exclusive-file-lock.ts
|
|
10
|
+
import { randomUUID } from "crypto";
|
|
11
11
|
import { open, readFile, stat, unlink } from "fs/promises";
|
|
12
12
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
13
13
|
var DEFAULT_STALE_MS = 12e4;
|
|
@@ -34,8 +34,11 @@ async function acquireExclusiveFileLock(targetPath, options = {}) {
|
|
|
34
34
|
await wait(pollMs);
|
|
35
35
|
continue;
|
|
36
36
|
}
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
const ownerToken = randomUUID();
|
|
38
|
+
await handle.writeFile(
|
|
39
|
+
`${JSON.stringify({ pid: process.pid, ownerToken, createdAt: Date.now() })}
|
|
40
|
+
`
|
|
41
|
+
);
|
|
39
42
|
await handle.sync();
|
|
40
43
|
let released = false;
|
|
41
44
|
return {
|
|
@@ -43,7 +46,8 @@ async function acquireExclusiveFileLock(targetPath, options = {}) {
|
|
|
43
46
|
if (released) return;
|
|
44
47
|
released = true;
|
|
45
48
|
await handle.close();
|
|
46
|
-
await
|
|
49
|
+
const currentOwner = await readFile(lockPath, "utf8").then((raw) => JSON.parse(raw).ownerToken).catch(() => void 0);
|
|
50
|
+
if (currentOwner === ownerToken) await unlink(lockPath).catch(() => void 0);
|
|
47
51
|
}
|
|
48
52
|
};
|
|
49
53
|
}
|
|
@@ -155,6 +159,17 @@ function getConnectorProfileKey(serverUrl, token) {
|
|
|
155
159
|
const discriminator = token?.trim() ? getConnectorTokenFingerprint(token) : "legacy-no-token";
|
|
156
160
|
return `${getServerOriginKey(serverUrl)}--${discriminator}`;
|
|
157
161
|
}
|
|
162
|
+
function getConnectorIdentityKey(deviceId, serverUrl, spaceId) {
|
|
163
|
+
const normalizedSpaceId = spaceId.trim();
|
|
164
|
+
if (!deviceId.trim() || !normalizedSpaceId) {
|
|
165
|
+
throw new Error("Connector identity requires deviceId and spaceId");
|
|
166
|
+
}
|
|
167
|
+
const digest = createHash2("sha256").update(`${deviceId.trim()}
|
|
168
|
+
${normalizeServerOrigin(serverUrl)}
|
|
169
|
+
${normalizedSpaceId}`, "utf8").digest("hex").slice(0, 24);
|
|
170
|
+
const spaceKey = normalizedSpaceId.replace(/[^a-z0-9_-]+/gi, "-").toLowerCase();
|
|
171
|
+
return `${getServerOriginKey(serverUrl)}--${spaceKey}--${digest}`;
|
|
172
|
+
}
|
|
158
173
|
function getConnectorPaths(serverUrl, configRoot = path.join(homedir(), ".buda")) {
|
|
159
174
|
const scopedLegacyDir = path.join(configRoot, "servers", getServerOriginKey(serverUrl));
|
|
160
175
|
return {
|
|
@@ -176,6 +191,17 @@ function getConnectorDaemonLockTarget(serverUrl, connectorId, configRoot = path.
|
|
|
176
191
|
"connector-daemon"
|
|
177
192
|
);
|
|
178
193
|
}
|
|
194
|
+
function getConnectorBootstrapLockTarget(launchConfig, configRoot = path.join(homedir(), ".buda")) {
|
|
195
|
+
const serverUrl = getServerUrl(launchConfig);
|
|
196
|
+
const scope = launchConfig?.spaceId?.trim() ? `space-${launchConfig.spaceId.trim()}` : getConnectorToken(launchConfig) ? `token-${getConnectorTokenFingerprint(getConnectorToken(launchConfig)).slice(0, 24)}` : "oauth-unresolved";
|
|
197
|
+
return path.join(
|
|
198
|
+
configRoot,
|
|
199
|
+
"daemons",
|
|
200
|
+
getServerOriginKey(serverUrl),
|
|
201
|
+
scope.replace(/[^a-z0-9_-]+/gi, "-").toLowerCase(),
|
|
202
|
+
"bootstrap"
|
|
203
|
+
);
|
|
204
|
+
}
|
|
179
205
|
function getServerUrl(launchConfig) {
|
|
180
206
|
return (launchConfig?.serverUrl || process.env.BUDA_CONNECTOR_SERVER_URL || process.env.BUDA_SERVER_URL || process.env.NEXT_PUBLIC_APP_URL || DEFAULT_SERVER_URL).replace(/\/$/, "");
|
|
181
207
|
}
|
|
@@ -208,15 +234,16 @@ function getBunnyDaemonPort(launchConfig) {
|
|
|
208
234
|
function getConnectorToken(launchConfig) {
|
|
209
235
|
return launchConfig?.token?.trim() || process.env.BUDA_CONNECTOR_TOKEN?.trim() || void 0;
|
|
210
236
|
}
|
|
211
|
-
function buildDeviceConfig(launchConfig, deviceId = `dvc_${
|
|
237
|
+
function buildDeviceConfig(launchConfig, deviceId = `dvc_${randomUUID2()}`) {
|
|
212
238
|
const host = hostname();
|
|
213
239
|
const token = getConnectorToken(launchConfig);
|
|
214
240
|
return {
|
|
215
|
-
connectorId: `cnr_${
|
|
241
|
+
connectorId: `cnr_${randomUUID2()}`,
|
|
216
242
|
deviceId,
|
|
217
243
|
deviceName: host,
|
|
218
244
|
hostLabel: `${host} (${platform2()})`,
|
|
219
245
|
serverUrl: getServerUrl(launchConfig),
|
|
246
|
+
...launchConfig?.spaceId?.trim() ? { spaceId: launchConfig.spaceId.trim() } : {},
|
|
220
247
|
workdirRoot: getWorkdirRoot(launchConfig),
|
|
221
248
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
222
249
|
deviceToken: randomBytes(32).toString("base64url"),
|
|
@@ -228,12 +255,18 @@ function isDeviceConfig(value) {
|
|
|
228
255
|
const config = value;
|
|
229
256
|
return typeof config.connectorId === "string" && typeof config.deviceId === "string" && typeof config.serverUrl === "string";
|
|
230
257
|
}
|
|
258
|
+
function isCanonicalDeviceConfig(value) {
|
|
259
|
+
return isDeviceConfig(value) && typeof value.spaceId === "string" && value.spaceId.trim().length > 0;
|
|
260
|
+
}
|
|
231
261
|
async function readJsonFile(configPath) {
|
|
232
262
|
try {
|
|
233
263
|
const raw = await readFile2(configPath, "utf8");
|
|
234
264
|
return JSON.parse(raw);
|
|
235
|
-
} catch {
|
|
236
|
-
|
|
265
|
+
} catch (error) {
|
|
266
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
throw new Error(`Invalid Connector config ${configPath}`, { cause: error });
|
|
237
270
|
}
|
|
238
271
|
}
|
|
239
272
|
function migrateLegacyProfiles(values) {
|
|
@@ -250,36 +283,68 @@ function migrateLegacyProfiles(values) {
|
|
|
250
283
|
async function loadDeviceConfigStore(configPath) {
|
|
251
284
|
const parsed = await readJsonFile(configPath);
|
|
252
285
|
const candidate = parsed;
|
|
286
|
+
if (candidate?.version === 4 && candidate.identities && typeof candidate.identities === "object" && candidate.aliases && typeof candidate.aliases === "object") {
|
|
287
|
+
const identities = candidate.identities;
|
|
288
|
+
for (const [identityKey, value] of Object.entries(identities)) {
|
|
289
|
+
if (!isCanonicalDeviceConfig(value)) {
|
|
290
|
+
throw new Error(`Invalid canonical Connector identity ${identityKey}`);
|
|
291
|
+
}
|
|
292
|
+
const expected = getConnectorIdentityKey(value.deviceId, value.serverUrl, value.spaceId);
|
|
293
|
+
if (identityKey !== expected) {
|
|
294
|
+
throw new Error(`Connector identity key does not match ${value.deviceId}/${value.spaceId}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
store: {
|
|
299
|
+
version: 4,
|
|
300
|
+
...typeof candidate.systemDeviceId === "string" ? { systemDeviceId: candidate.systemDeviceId } : {},
|
|
301
|
+
identities,
|
|
302
|
+
aliases: candidate.aliases
|
|
303
|
+
},
|
|
304
|
+
legacyProfiles: {},
|
|
305
|
+
needsWrite: false
|
|
306
|
+
};
|
|
307
|
+
}
|
|
253
308
|
if (candidate?.version === 3 && candidate.profiles && typeof candidate.profiles === "object") {
|
|
254
309
|
return {
|
|
255
310
|
store: {
|
|
256
|
-
version:
|
|
311
|
+
version: 4,
|
|
257
312
|
...typeof candidate.systemDeviceId === "string" ? { systemDeviceId: candidate.systemDeviceId } : {},
|
|
258
|
-
|
|
313
|
+
identities: {},
|
|
314
|
+
aliases: {}
|
|
259
315
|
},
|
|
316
|
+
legacyProfiles: candidate.profiles,
|
|
260
317
|
needsWrite: false
|
|
261
318
|
};
|
|
262
319
|
}
|
|
263
320
|
if (candidate && (candidate.version === 1 || candidate.version === 2) && candidate.servers && typeof candidate.servers === "object") {
|
|
264
321
|
return {
|
|
265
322
|
store: {
|
|
266
|
-
version:
|
|
323
|
+
version: 4,
|
|
267
324
|
...typeof candidate.systemDeviceId === "string" ? { systemDeviceId: candidate.systemDeviceId } : {},
|
|
268
|
-
|
|
325
|
+
identities: {},
|
|
326
|
+
aliases: {}
|
|
269
327
|
},
|
|
270
|
-
|
|
328
|
+
legacyProfiles: migrateLegacyProfiles(Object.values(candidate.servers)),
|
|
329
|
+
needsWrite: false
|
|
271
330
|
};
|
|
272
331
|
}
|
|
273
332
|
if (isDeviceConfig(parsed)) {
|
|
274
333
|
return {
|
|
275
334
|
store: {
|
|
276
|
-
version:
|
|
277
|
-
|
|
335
|
+
version: 4,
|
|
336
|
+
identities: {},
|
|
337
|
+
aliases: {}
|
|
278
338
|
},
|
|
279
|
-
|
|
339
|
+
legacyProfiles: migrateLegacyProfiles([parsed]),
|
|
340
|
+
needsWrite: false
|
|
280
341
|
};
|
|
281
342
|
}
|
|
282
|
-
return {
|
|
343
|
+
return {
|
|
344
|
+
store: { version: 4, identities: {}, aliases: {} },
|
|
345
|
+
legacyProfiles: {},
|
|
346
|
+
needsWrite: false
|
|
347
|
+
};
|
|
283
348
|
}
|
|
284
349
|
function normalizeStoredWorkdirRoot(config, paths) {
|
|
285
350
|
const configuredRoot = config.workdirRoot?.trim();
|
|
@@ -297,7 +362,7 @@ function normalizeStoredWorkdirRoot(config, paths) {
|
|
|
297
362
|
}
|
|
298
363
|
async function writeDeviceConfigStore(configPath, store) {
|
|
299
364
|
await mkdir(path.dirname(configPath), { recursive: true });
|
|
300
|
-
const temporaryPath = `${configPath}.${process.pid}.${
|
|
365
|
+
const temporaryPath = `${configPath}.${process.pid}.${randomUUID2()}.tmp`;
|
|
301
366
|
try {
|
|
302
367
|
await writeFile(temporaryPath, `${JSON.stringify(store, null, 2)}
|
|
303
368
|
`, { mode: 384 });
|
|
@@ -307,36 +372,70 @@ async function writeDeviceConfigStore(configPath, store) {
|
|
|
307
372
|
await unlink2(temporaryPath).catch(() => void 0);
|
|
308
373
|
}
|
|
309
374
|
}
|
|
375
|
+
function findStoredDeviceConfig(store, legacyProfiles, launchConfig, serverUrl, paths) {
|
|
376
|
+
const aliases = [
|
|
377
|
+
...launchConfig?.spaceId?.trim() ? [`${getServerOriginKey(serverUrl)}--space--${launchConfig.spaceId.trim()}`] : [],
|
|
378
|
+
...getConnectorToken(launchConfig) ? [getConnectorProfileKey(serverUrl, getConnectorToken(launchConfig))] : []
|
|
379
|
+
];
|
|
380
|
+
const identityKey = aliases.map((alias) => store.aliases[alias]).find(Boolean);
|
|
381
|
+
let storedConfig = identityKey ? store.identities[identityKey] : void 0;
|
|
382
|
+
if (!storedConfig && getConnectorToken(launchConfig)) {
|
|
383
|
+
const legacyKey = getConnectorProfileKey(serverUrl, getConnectorToken(launchConfig));
|
|
384
|
+
storedConfig = legacyProfiles[legacyKey];
|
|
385
|
+
}
|
|
386
|
+
if (!isDeviceConfig(storedConfig)) return null;
|
|
387
|
+
return normalizeStoredWorkdirRoot(storedConfig, paths).config;
|
|
388
|
+
}
|
|
389
|
+
function putCanonicalDeviceConfig(store, config) {
|
|
390
|
+
if (!config.spaceId?.trim()) {
|
|
391
|
+
throw new Error("Cannot persist Connector identity before the server resolves spaceId");
|
|
392
|
+
}
|
|
393
|
+
const spaceId = config.spaceId.trim();
|
|
394
|
+
const identityKey = getConnectorIdentityKey(config.deviceId, config.serverUrl, spaceId);
|
|
395
|
+
store.identities[identityKey] = { ...config, spaceId };
|
|
396
|
+
store.aliases[`${getServerOriginKey(config.serverUrl)}--space--${spaceId}`] = identityKey;
|
|
397
|
+
if (config.token?.trim()) {
|
|
398
|
+
store.aliases[getConnectorProfileKey(config.serverUrl, config.token)] = identityKey;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
310
401
|
async function readDeviceConfig(launchConfig, configRoot) {
|
|
311
402
|
const serverUrl = getServerUrl(launchConfig);
|
|
312
403
|
const paths = getConnectorPaths(serverUrl, configRoot);
|
|
313
|
-
const launchToken = getConnectorToken(launchConfig);
|
|
314
404
|
await mkdir(paths.configDir, { recursive: true });
|
|
315
405
|
return withExclusiveFileLock(paths.configPath, async () => {
|
|
316
|
-
const { store,
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
406
|
+
const { store, legacyProfiles } = await loadDeviceConfigStore(paths.configPath);
|
|
407
|
+
return findStoredDeviceConfig(store, legacyProfiles, launchConfig, serverUrl, paths);
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
async function listDeviceConfigs(configRoot) {
|
|
411
|
+
const paths = getConnectorPaths(DEFAULT_SERVER_URL, configRoot);
|
|
412
|
+
await mkdir(paths.configDir, { recursive: true });
|
|
413
|
+
return withExclusiveFileLock(paths.configPath, async () => {
|
|
414
|
+
const { legacyProfiles, store } = await loadDeviceConfigStore(paths.configPath);
|
|
415
|
+
const identities = [...Object.values(store.identities), ...Object.values(legacyProfiles)].filter(isCanonicalDeviceConfig).map(
|
|
416
|
+
(config) => normalizeStoredWorkdirRoot(config, getConnectorPaths(config.serverUrl, configRoot)).config
|
|
417
|
+
);
|
|
418
|
+
const unique = /* @__PURE__ */ new Map();
|
|
419
|
+
for (const config of identities) {
|
|
420
|
+
unique.set(`${normalizeServerOrigin(config.serverUrl)}
|
|
421
|
+
${config.connectorId}`, config);
|
|
329
422
|
}
|
|
330
|
-
return
|
|
423
|
+
return [...unique.values()].sort(
|
|
424
|
+
(left, right) => left.createdAt.localeCompare(right.createdAt) || left.connectorId.localeCompare(right.connectorId)
|
|
425
|
+
);
|
|
331
426
|
});
|
|
332
427
|
}
|
|
333
428
|
async function writeDeviceConfig(config, configRoot) {
|
|
429
|
+
if (!config.spaceId?.trim()) {
|
|
430
|
+
throw new Error("Cannot persist Connector identity before the server resolves spaceId");
|
|
431
|
+
}
|
|
432
|
+
const spaceId = config.spaceId.trim();
|
|
334
433
|
const paths = getConnectorPaths(config.serverUrl, configRoot);
|
|
335
434
|
await mkdir(config.workdirRoot, { recursive: true });
|
|
336
435
|
await mkdir(paths.configDir, { recursive: true });
|
|
337
436
|
await withExclusiveFileLock(paths.configPath, async () => {
|
|
338
437
|
const { store } = await loadDeviceConfigStore(paths.configPath);
|
|
339
|
-
store
|
|
438
|
+
putCanonicalDeviceConfig(store, { ...config, spaceId });
|
|
340
439
|
await writeDeviceConfigStore(paths.configPath, store);
|
|
341
440
|
});
|
|
342
441
|
}
|
|
@@ -344,13 +443,12 @@ async function getOrCreateSystemDeviceId(configRoot, resolver = resolveSystemDev
|
|
|
344
443
|
const paths = getConnectorPaths(DEFAULT_SERVER_URL, configRoot);
|
|
345
444
|
await mkdir(paths.configDir, { recursive: true });
|
|
346
445
|
return withExclusiveFileLock(paths.configPath, async () => {
|
|
347
|
-
const { store,
|
|
446
|
+
const { store, legacyProfiles } = await loadDeviceConfigStore(paths.configPath);
|
|
348
447
|
if (store.systemDeviceId) {
|
|
349
|
-
if (needsWrite) await writeDeviceConfigStore(paths.configPath, store);
|
|
350
448
|
return store.systemDeviceId;
|
|
351
449
|
}
|
|
352
|
-
const existingDeviceId = Object.values(store.
|
|
353
|
-
store.systemDeviceId = resolver() || existingDeviceId || `dvc_${
|
|
450
|
+
const existingDeviceId = Object.values(store.identities).find(isDeviceConfig)?.deviceId ?? Object.values(legacyProfiles).find(isDeviceConfig)?.deviceId;
|
|
451
|
+
store.systemDeviceId = resolver() || existingDeviceId || `dvc_${randomUUID2()}`;
|
|
354
452
|
await writeDeviceConfigStore(paths.configPath, store);
|
|
355
453
|
return store.systemDeviceId;
|
|
356
454
|
});
|
|
@@ -362,33 +460,28 @@ async function createDeviceConfig(launchConfig, configRoot, resolver) {
|
|
|
362
460
|
async function ensureDeviceConfig(launchConfig, configRoot, resolver = resolveSystemDeviceId) {
|
|
363
461
|
const serverUrl = getServerUrl(launchConfig);
|
|
364
462
|
const paths = getConnectorPaths(serverUrl, configRoot);
|
|
365
|
-
const token = getConnectorToken(launchConfig);
|
|
366
|
-
const profileKey = getConnectorProfileKey(serverUrl, token);
|
|
367
463
|
await mkdir(paths.configDir, { recursive: true });
|
|
368
464
|
const config = await withExclusiveFileLock(paths.configPath, async () => {
|
|
369
|
-
const { store,
|
|
370
|
-
const
|
|
371
|
-
if (
|
|
372
|
-
const
|
|
373
|
-
if (needsWrite || normalized.changed) {
|
|
374
|
-
store.profiles[profileKey] = normalized.config;
|
|
375
|
-
await writeDeviceConfigStore(paths.configPath, store);
|
|
376
|
-
}
|
|
377
|
-
const existing = normalized.config;
|
|
378
|
-
const overrideToken = getConnectorToken(launchConfig);
|
|
379
|
-
return {
|
|
465
|
+
const { store, legacyProfiles } = await loadDeviceConfigStore(paths.configPath);
|
|
466
|
+
const existing = findStoredDeviceConfig(store, legacyProfiles, launchConfig, serverUrl, paths);
|
|
467
|
+
if (existing) {
|
|
468
|
+
const resolved = {
|
|
380
469
|
...existing,
|
|
381
470
|
serverUrl,
|
|
382
471
|
workdirRoot: launchConfig?.workdirRoot || process.env.BUDA_CONNECTOR_WORKDIR || existing.workdirRoot || getWorkdirRoot(launchConfig),
|
|
383
|
-
...
|
|
472
|
+
...getConnectorToken(launchConfig) ? { token: getConnectorToken(launchConfig) } : {}
|
|
384
473
|
};
|
|
474
|
+
if (isCanonicalDeviceConfig(resolved)) {
|
|
475
|
+
putCanonicalDeviceConfig(store, resolved);
|
|
476
|
+
await writeDeviceConfigStore(paths.configPath, store);
|
|
477
|
+
}
|
|
478
|
+
return resolved;
|
|
385
479
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
store.profiles[profileKey] = created;
|
|
480
|
+
const existingDeviceId = store.systemDeviceId ?? Object.values(store.identities).find(isDeviceConfig)?.deviceId ?? Object.values(legacyProfiles).find(isDeviceConfig)?.deviceId;
|
|
481
|
+
const deviceId = existingDeviceId || resolver() || `dvc_${randomUUID2()}`;
|
|
482
|
+
const created = buildDeviceConfig(launchConfig, deviceId);
|
|
483
|
+
store.systemDeviceId = deviceId;
|
|
484
|
+
if (isCanonicalDeviceConfig(created)) putCanonicalDeviceConfig(store, created);
|
|
392
485
|
await writeDeviceConfigStore(paths.configPath, store);
|
|
393
486
|
return created;
|
|
394
487
|
});
|
|
@@ -401,6 +494,7 @@ function parseLaunchConfig(raw) {
|
|
|
401
494
|
const bunnyDaemonPort = typeof parsed.bunnyDaemonPort === "number" && Number.isInteger(parsed.bunnyDaemonPort) ? parsed.bunnyDaemonPort : void 0;
|
|
402
495
|
return {
|
|
403
496
|
serverUrl: typeof parsed.serverUrl === "string" ? parsed.serverUrl : void 0,
|
|
497
|
+
spaceId: typeof parsed.spaceId === "string" ? parsed.spaceId : void 0,
|
|
404
498
|
workdirRoot: typeof parsed.workdirRoot === "string" ? parsed.workdirRoot : void 0,
|
|
405
499
|
oauthToken: typeof parsed.oauthToken === "string" ? parsed.oauthToken : void 0,
|
|
406
500
|
token: typeof parsed.token === "string" ? parsed.token : void 0,
|
|
@@ -413,9 +507,327 @@ function parseLaunchConfig(raw) {
|
|
|
413
507
|
import { closeSync, openSync } from "fs";
|
|
414
508
|
import { ReadStream, WriteStream } from "tty";
|
|
415
509
|
import { format, stripVTControlCharacters } from "util";
|
|
510
|
+
|
|
511
|
+
// src/daemon-control.ts
|
|
512
|
+
import { randomBytes as randomBytes2, randomUUID as randomUUID3 } from "crypto";
|
|
513
|
+
import { chmod as chmod2, mkdir as mkdir2, readdir, readFile as readFile3, rename as rename2, unlink as unlink3, writeFile as writeFile2 } from "fs/promises";
|
|
514
|
+
import { createConnection, createServer } from "net";
|
|
515
|
+
import path2 from "path";
|
|
516
|
+
|
|
517
|
+
// package.json
|
|
518
|
+
var package_default = {
|
|
519
|
+
name: "@buda-ai/connector",
|
|
520
|
+
version: "0.1.8",
|
|
521
|
+
private: false,
|
|
522
|
+
type: "module",
|
|
523
|
+
publishConfig: {
|
|
524
|
+
access: "public"
|
|
525
|
+
},
|
|
526
|
+
files: [
|
|
527
|
+
"dist",
|
|
528
|
+
"agent-templates"
|
|
529
|
+
],
|
|
530
|
+
bin: {
|
|
531
|
+
"buda-connector": "./dist/cli.js",
|
|
532
|
+
connector: "./dist/cli.js"
|
|
533
|
+
},
|
|
534
|
+
scripts: {
|
|
535
|
+
dev: "tsx src/cli.ts dev",
|
|
536
|
+
daemon: "tsx src/cli.ts daemon",
|
|
537
|
+
new: "tsx src/cli.ts new",
|
|
538
|
+
start: "node dist/cli.js daemon",
|
|
539
|
+
build: "tsup",
|
|
540
|
+
test: "vitest run",
|
|
541
|
+
typecheck: "tsc --noEmit",
|
|
542
|
+
lint: "biome format . --write && biome check .",
|
|
543
|
+
"lint:err": "biome format . --write && biome check . --write --diagnostic-level=error"
|
|
544
|
+
},
|
|
545
|
+
dependencies: {
|
|
546
|
+
"@bunny-agent/daemon": "0.9.51",
|
|
547
|
+
"@clack/prompts": "^1.0.0",
|
|
548
|
+
"playwright-core": "^1.52.0",
|
|
549
|
+
ws: "^8.21.0"
|
|
550
|
+
},
|
|
551
|
+
optionalDependencies: {
|
|
552
|
+
"node-pty": "^1.1.0"
|
|
553
|
+
},
|
|
554
|
+
devDependencies: {
|
|
555
|
+
"@types/node": "^24.13.2",
|
|
556
|
+
"@types/ws": "^8.18.1",
|
|
557
|
+
relaylib: "workspace:*",
|
|
558
|
+
tsup: "^8.5.0",
|
|
559
|
+
tsx: "^4.20.5",
|
|
560
|
+
typescript: "^5.9.3",
|
|
561
|
+
vitest: "^2.1.8"
|
|
562
|
+
},
|
|
563
|
+
engines: {
|
|
564
|
+
node: ">=24.18.0"
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
// src/daemon-control.ts
|
|
569
|
+
var CONNECTOR_CONTROL_PROTOCOL_VERSION = 1;
|
|
570
|
+
var CONNECTOR_RUNTIME_VERSION = package_default.version;
|
|
571
|
+
var DEFAULT_CONTROL_REQUEST_TIMEOUT_MS = 3e3;
|
|
572
|
+
var getDaemonControlMetadataPath = (config, configRoot) => `${getConnectorDaemonLockTarget(config.serverUrl, config.connectorId, configRoot)}.control.json`;
|
|
573
|
+
var writeMetadata = async (metadataPath, metadata) => {
|
|
574
|
+
await mkdir2(path2.dirname(metadataPath), { recursive: true, mode: 448 });
|
|
575
|
+
const temporaryPath = `${metadataPath}.${process.pid}.${randomUUID3()}.tmp`;
|
|
576
|
+
try {
|
|
577
|
+
await writeFile2(temporaryPath, `${JSON.stringify(metadata)}
|
|
578
|
+
`, { mode: 384 });
|
|
579
|
+
await rename2(temporaryPath, metadataPath);
|
|
580
|
+
await chmod2(metadataPath, 384);
|
|
581
|
+
} finally {
|
|
582
|
+
await unlink3(temporaryPath).catch(() => void 0);
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
var publicStatus = (metadata) => ({
|
|
586
|
+
connectorId: metadata.connectorId,
|
|
587
|
+
idle: metadata.idle,
|
|
588
|
+
instanceId: metadata.instanceId,
|
|
589
|
+
...metadata.lastHeartbeatAt ? { lastHeartbeatAt: metadata.lastHeartbeatAt } : {},
|
|
590
|
+
mode: metadata.mode,
|
|
591
|
+
pid: metadata.pid,
|
|
592
|
+
protocolVersion: metadata.protocolVersion,
|
|
593
|
+
relayConnected: metadata.relayConnected,
|
|
594
|
+
serverUrl: metadata.serverUrl,
|
|
595
|
+
...metadata.spaceId ? { spaceId: metadata.spaceId } : {},
|
|
596
|
+
startedAt: metadata.startedAt,
|
|
597
|
+
version: metadata.version
|
|
598
|
+
});
|
|
599
|
+
var parseRequest = (raw) => {
|
|
600
|
+
try {
|
|
601
|
+
const value = JSON.parse(raw);
|
|
602
|
+
if (typeof value.secret !== "string" || !["hold", "persist", "release", "status", "stop", "stop-if-idle"].includes(
|
|
603
|
+
value.command ?? ""
|
|
604
|
+
)) {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
return value;
|
|
608
|
+
} catch {
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
async function startDaemonControlServer(options) {
|
|
613
|
+
const metadataPath = getDaemonControlMetadataPath(options.config, options.configRoot);
|
|
614
|
+
let stopping = false;
|
|
615
|
+
let desktopHolds = 0;
|
|
616
|
+
let metadata;
|
|
617
|
+
let startupGrace;
|
|
618
|
+
const sockets = /* @__PURE__ */ new Set();
|
|
619
|
+
const requestStop = () => {
|
|
620
|
+
if (stopping) return;
|
|
621
|
+
stopping = true;
|
|
622
|
+
void Promise.resolve(options.onStop());
|
|
623
|
+
};
|
|
624
|
+
const server = createServer((socket) => {
|
|
625
|
+
sockets.add(socket);
|
|
626
|
+
socket.setEncoding("utf8");
|
|
627
|
+
let buffer = "";
|
|
628
|
+
let holding = false;
|
|
629
|
+
socket.on("data", (chunk) => {
|
|
630
|
+
buffer += chunk;
|
|
631
|
+
const newline = buffer.indexOf("\n");
|
|
632
|
+
if (newline < 0) return;
|
|
633
|
+
const request = parseRequest(buffer.slice(0, newline));
|
|
634
|
+
buffer = buffer.slice(newline + 1);
|
|
635
|
+
if (!request || request.secret !== metadata.secret) {
|
|
636
|
+
socket.end(
|
|
637
|
+
`${JSON.stringify({ ok: false, error: "unauthorized" })}
|
|
638
|
+
`
|
|
639
|
+
);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
metadata.idle = options.isIdle?.() ?? metadata.idle;
|
|
643
|
+
metadata.lastHeartbeatAt = options.getLastHeartbeatAt?.() ?? metadata.lastHeartbeatAt;
|
|
644
|
+
metadata.relayConnected = options.isRelayConnected?.() ?? metadata.relayConnected;
|
|
645
|
+
if (request.command === "persist") {
|
|
646
|
+
if (startupGrace) clearTimeout(startupGrace);
|
|
647
|
+
metadata.mode = "persistent";
|
|
648
|
+
void writeMetadata(metadataPath, metadata);
|
|
649
|
+
} else if (request.command === "hold") {
|
|
650
|
+
if (startupGrace) clearTimeout(startupGrace);
|
|
651
|
+
holding = true;
|
|
652
|
+
desktopHolds += 1;
|
|
653
|
+
} else if (request.command === "release") {
|
|
654
|
+
if (metadata.mode === "transient" && desktopHolds === 0) requestStop();
|
|
655
|
+
} else if (request.command === "stop") {
|
|
656
|
+
requestStop();
|
|
657
|
+
} else if (request.command === "stop-if-idle" && metadata.idle) {
|
|
658
|
+
requestStop();
|
|
659
|
+
}
|
|
660
|
+
socket.write(
|
|
661
|
+
`${JSON.stringify({ ok: true, status: publicStatus(metadata) })}
|
|
662
|
+
`
|
|
663
|
+
);
|
|
664
|
+
if (request.command !== "hold") socket.end();
|
|
665
|
+
});
|
|
666
|
+
socket.on("close", () => {
|
|
667
|
+
sockets.delete(socket);
|
|
668
|
+
if (!holding) return;
|
|
669
|
+
desktopHolds = Math.max(0, desktopHolds - 1);
|
|
670
|
+
if (metadata.mode === "transient" && desktopHolds === 0) requestStop();
|
|
671
|
+
});
|
|
672
|
+
});
|
|
673
|
+
await new Promise((resolve, reject) => {
|
|
674
|
+
server.once("error", reject);
|
|
675
|
+
server.listen(0, "127.0.0.1", () => {
|
|
676
|
+
server.off("error", reject);
|
|
677
|
+
resolve();
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
const address = server.address();
|
|
681
|
+
if (!address || typeof address === "string")
|
|
682
|
+
throw new Error("Failed to bind daemon control port");
|
|
683
|
+
metadata = {
|
|
684
|
+
connectorId: options.config.connectorId,
|
|
685
|
+
idle: options.isIdle?.() ?? true,
|
|
686
|
+
instanceId: randomUUID3(),
|
|
687
|
+
mode: options.mode,
|
|
688
|
+
pid: process.pid,
|
|
689
|
+
port: address.port,
|
|
690
|
+
protocolVersion: CONNECTOR_CONTROL_PROTOCOL_VERSION,
|
|
691
|
+
relayConnected: options.isRelayConnected?.() ?? false,
|
|
692
|
+
secret: randomBytes2(32).toString("base64url"),
|
|
693
|
+
serverUrl: options.config.serverUrl,
|
|
694
|
+
...options.config.spaceId ? { spaceId: options.config.spaceId } : {},
|
|
695
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
696
|
+
version: options.version ?? CONNECTOR_RUNTIME_VERSION
|
|
697
|
+
};
|
|
698
|
+
await writeMetadata(metadataPath, metadata);
|
|
699
|
+
if (metadata.mode === "transient") {
|
|
700
|
+
startupGrace = setTimeout(requestStop, options.transientGraceMs ?? 3e4);
|
|
701
|
+
}
|
|
702
|
+
return {
|
|
703
|
+
close: async () => {
|
|
704
|
+
if (startupGrace) clearTimeout(startupGrace);
|
|
705
|
+
for (const socket of sockets) socket.destroy();
|
|
706
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
707
|
+
const activeInstanceId = await readFile3(metadataPath, "utf8").then((raw) => JSON.parse(raw).instanceId).catch(() => void 0);
|
|
708
|
+
if (activeInstanceId === metadata.instanceId) {
|
|
709
|
+
await unlink3(metadataPath).catch(() => void 0);
|
|
710
|
+
}
|
|
711
|
+
},
|
|
712
|
+
setIdle: (idle) => {
|
|
713
|
+
metadata.idle = idle;
|
|
714
|
+
},
|
|
715
|
+
status: () => publicStatus(metadata)
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
var parseMetadata = (raw, expectedConnectorId) => {
|
|
719
|
+
const metadata = JSON.parse(raw);
|
|
720
|
+
if (metadata.protocolVersion !== CONNECTOR_CONTROL_PROTOCOL_VERSION || typeof metadata.port !== "number" || typeof metadata.secret !== "string" || typeof metadata.connectorId !== "string" || typeof metadata.serverUrl !== "string" || expectedConnectorId && metadata.connectorId !== expectedConnectorId) {
|
|
721
|
+
throw new Error("Invalid Connector daemon control metadata");
|
|
722
|
+
}
|
|
723
|
+
return metadata;
|
|
724
|
+
};
|
|
725
|
+
var readMetadata = async (config, configRoot) => {
|
|
726
|
+
const metadataPath = getDaemonControlMetadataPath(config, configRoot);
|
|
727
|
+
return parseMetadata(await readFile3(metadataPath, "utf8"), config.connectorId);
|
|
728
|
+
};
|
|
729
|
+
async function listPersistedDaemonStatuses(configRoot) {
|
|
730
|
+
const daemonRoot = path2.join(
|
|
731
|
+
getConnectorPaths(DEFAULT_SERVER_URL, configRoot).configDir,
|
|
732
|
+
"daemons"
|
|
733
|
+
);
|
|
734
|
+
const serverDirectories = await readdir(daemonRoot, { withFileTypes: true }).catch(() => []);
|
|
735
|
+
const statuses = await Promise.all(
|
|
736
|
+
serverDirectories.filter((entry) => entry.isDirectory()).map(async (serverEntry) => {
|
|
737
|
+
const serverDirectory = path2.join(daemonRoot, serverEntry.name);
|
|
738
|
+
const connectorEntries = await readdir(serverDirectory, { withFileTypes: true }).catch(
|
|
739
|
+
() => []
|
|
740
|
+
);
|
|
741
|
+
return Promise.all(
|
|
742
|
+
connectorEntries.filter((entry) => entry.isDirectory()).map(
|
|
743
|
+
(entry) => readFile3(
|
|
744
|
+
path2.join(serverDirectory, entry.name, "connector-daemon.control.json"),
|
|
745
|
+
"utf8"
|
|
746
|
+
).then((raw) => publicStatus(parseMetadata(raw))).catch(() => null)
|
|
747
|
+
)
|
|
748
|
+
);
|
|
749
|
+
})
|
|
750
|
+
);
|
|
751
|
+
const unique = /* @__PURE__ */ new Map();
|
|
752
|
+
for (const status of statuses.flat()) {
|
|
753
|
+
if (!status) continue;
|
|
754
|
+
unique.set(`${status.serverUrl}
|
|
755
|
+
${status.connectorId}`, status);
|
|
756
|
+
}
|
|
757
|
+
return [...unique.values()];
|
|
758
|
+
}
|
|
759
|
+
var connectControl = async (metadata, command, timeoutMs = DEFAULT_CONTROL_REQUEST_TIMEOUT_MS) => new Promise((resolve, reject) => {
|
|
760
|
+
const socket = createConnection({ host: "127.0.0.1", port: metadata.port });
|
|
761
|
+
let buffer = "";
|
|
762
|
+
let settled = false;
|
|
763
|
+
const cleanup = () => {
|
|
764
|
+
socket.setTimeout(0);
|
|
765
|
+
socket.off("data", onData);
|
|
766
|
+
socket.off("error", fail);
|
|
767
|
+
socket.off("timeout", onTimeout);
|
|
768
|
+
};
|
|
769
|
+
const fail = (error) => {
|
|
770
|
+
if (settled) return;
|
|
771
|
+
settled = true;
|
|
772
|
+
cleanup();
|
|
773
|
+
socket.destroy();
|
|
774
|
+
reject(error);
|
|
775
|
+
};
|
|
776
|
+
const onTimeout = () => {
|
|
777
|
+
fail(new Error(`Connector daemon control timed out after ${timeoutMs}ms`));
|
|
778
|
+
};
|
|
779
|
+
const onData = (chunk) => {
|
|
780
|
+
buffer += chunk;
|
|
781
|
+
const newline = buffer.indexOf("\n");
|
|
782
|
+
if (newline < 0) return;
|
|
783
|
+
try {
|
|
784
|
+
const response = JSON.parse(buffer.slice(0, newline));
|
|
785
|
+
if (!response.ok) return fail(new Error(response.error ?? "Daemon control request failed"));
|
|
786
|
+
settled = true;
|
|
787
|
+
cleanup();
|
|
788
|
+
resolve({ response, socket });
|
|
789
|
+
} catch (error) {
|
|
790
|
+
fail(error instanceof Error ? error : new Error(String(error)));
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
socket.once("error", fail);
|
|
794
|
+
socket.once("timeout", onTimeout);
|
|
795
|
+
socket.setTimeout(timeoutMs);
|
|
796
|
+
socket.once("connect", () => {
|
|
797
|
+
socket.write(
|
|
798
|
+
`${JSON.stringify({ command, secret: metadata.secret })}
|
|
799
|
+
`
|
|
800
|
+
);
|
|
801
|
+
});
|
|
802
|
+
socket.setEncoding("utf8");
|
|
803
|
+
socket.on("data", onData);
|
|
804
|
+
});
|
|
805
|
+
async function readPersistedDaemonStatus(config, configRoot) {
|
|
806
|
+
return publicStatus(await readMetadata(config, configRoot));
|
|
807
|
+
}
|
|
808
|
+
async function requestDaemonControl(config, command, configRoot, options = {}) {
|
|
809
|
+
const metadata = await readMetadata(config, configRoot);
|
|
810
|
+
const { response, socket } = await connectControl(metadata, command, options.timeoutMs);
|
|
811
|
+
socket.end();
|
|
812
|
+
if (!response.status) throw new Error("Daemon control response did not include status");
|
|
813
|
+
return response.status;
|
|
814
|
+
}
|
|
815
|
+
async function holdDaemonControl(config, configRoot) {
|
|
816
|
+
const metadata = await readMetadata(config, configRoot);
|
|
817
|
+
const { response, socket } = await connectControl(metadata, "hold");
|
|
818
|
+
if (!response.status) {
|
|
819
|
+
socket.destroy();
|
|
820
|
+
throw new Error("Daemon hold response did not include status");
|
|
821
|
+
}
|
|
822
|
+
const closed = new Promise((resolve) => socket.once("close", () => resolve()));
|
|
823
|
+
return { close: () => socket.end(), closed, status: response.status };
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// src/console-dashboard.ts
|
|
416
827
|
var MAX_LINES_PER_PANE = 500;
|
|
417
828
|
var MIN_DASHBOARD_WIDTH = 60;
|
|
418
829
|
var MIN_DASHBOARD_HEIGHT = 18;
|
|
830
|
+
var DASHBOARD_TITLE = `Buda Connector v${CONNECTOR_RUNTIME_VERSION}`;
|
|
419
831
|
var colors = {
|
|
420
832
|
cyan: "\x1B[36m",
|
|
421
833
|
dim: "\x1B[2m",
|
|
@@ -475,9 +887,12 @@ var formatDuration = (durationMs) => {
|
|
|
475
887
|
return [hours, minutes, seconds].map((part) => String(part).padStart(2, "0")).join(":");
|
|
476
888
|
};
|
|
477
889
|
var createDashboardState = (startedAt = Date.now()) => ({
|
|
890
|
+
activeSection: "activity",
|
|
478
891
|
activePane: "activity",
|
|
479
892
|
activity: [],
|
|
480
893
|
agentDebug: [],
|
|
894
|
+
connectorLogs: {},
|
|
895
|
+
connectors: [],
|
|
481
896
|
heartbeat: "Waiting",
|
|
482
897
|
relay: "Starting",
|
|
483
898
|
agent: "Starting",
|
|
@@ -538,37 +953,189 @@ var scrollDashboardPane = (state, pane, delta) => {
|
|
|
538
953
|
const nextScroll = delta === "follow" ? 0 : delta === "oldest" ? maxScroll : Math.max(0, Math.min(maxScroll, state.scroll[pane] + delta));
|
|
539
954
|
return { ...state, scroll: { ...state.scroll, [pane]: nextScroll } };
|
|
540
955
|
};
|
|
541
|
-
var
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
const
|
|
545
|
-
|
|
956
|
+
var selectedConnector = (state) => state.connectors.find((connector) => connector.connectorId === state.selectedConnectorId);
|
|
957
|
+
var applySelectedConnector = (state, resetScroll = true) => {
|
|
958
|
+
const connector = selectedConnector(state);
|
|
959
|
+
const logs = connector ? state.connectorLogs[connector.connectorId] : void 0;
|
|
960
|
+
return {
|
|
961
|
+
...state,
|
|
962
|
+
activity: logs?.activity ?? [],
|
|
963
|
+
agent: connector?.agent ?? "Stopped",
|
|
964
|
+
agentDebug: logs?.agentDebug ?? [],
|
|
965
|
+
heartbeat: connector?.heartbeat ?? "Stopped",
|
|
966
|
+
relay: connector?.relay ?? "Disconnected",
|
|
967
|
+
server: connector?.serverUrl ?? "Unknown",
|
|
968
|
+
scroll: resetScroll ? { activity: 0, agentDebug: 0 } : state.scroll
|
|
969
|
+
};
|
|
970
|
+
};
|
|
971
|
+
var updateDashboardConnectors = (state, connectors, preferredConnectorId) => {
|
|
972
|
+
const selectedStillExists = connectors.some(
|
|
973
|
+
(connector) => connector.connectorId === state.selectedConnectorId
|
|
974
|
+
);
|
|
975
|
+
const selectedConnectorId = selectedStillExists ? state.selectedConnectorId : connectors.find((connector) => connector.connectorId === preferredConnectorId)?.connectorId ?? connectors[0]?.connectorId;
|
|
976
|
+
return applySelectedConnector(
|
|
977
|
+
{
|
|
978
|
+
...state,
|
|
979
|
+
activeSection: connectors.length > 0 && state.connectors.length === 0 ? "connectors" : state.activeSection,
|
|
980
|
+
connectors,
|
|
981
|
+
selectedConnectorId
|
|
982
|
+
},
|
|
983
|
+
selectedConnectorId !== state.selectedConnectorId
|
|
984
|
+
);
|
|
985
|
+
};
|
|
986
|
+
var selectConnectorByOffset = (state, offset) => {
|
|
987
|
+
if (state.connectors.length === 0) return state;
|
|
988
|
+
const currentIndex = Math.max(
|
|
989
|
+
0,
|
|
990
|
+
state.connectors.findIndex((connector) => connector.connectorId === state.selectedConnectorId)
|
|
991
|
+
);
|
|
992
|
+
const nextIndex = Math.max(0, Math.min(state.connectors.length - 1, currentIndex + offset));
|
|
993
|
+
return applySelectedConnector({
|
|
994
|
+
...state,
|
|
995
|
+
selectedConnectorId: state.connectors[nextIndex]?.connectorId
|
|
996
|
+
});
|
|
997
|
+
};
|
|
998
|
+
var promptOptionCount = (prompt) => prompt.kind === "exit-choice" ? 3 : 2;
|
|
999
|
+
var isUpKey = (key) => ["\x1B[A", "\x1BOA", "k", "K"].includes(key);
|
|
1000
|
+
var isDownKey = (key) => ["\x1B[B", "\x1BOB", "j", "J"].includes(key);
|
|
1001
|
+
var getConnectorSectionHeight = (state, height) => state.connectors.length ? Math.min(
|
|
1002
|
+
Math.max(4, state.connectors.length + 2),
|
|
1003
|
+
Math.max(4, Math.min(Math.floor(height / 3), height - 13))
|
|
1004
|
+
) : 0;
|
|
1005
|
+
var parseDashboardMouseWheel = (input, state, size) => {
|
|
1006
|
+
const event = input.split("\x1B[<").at(-1);
|
|
1007
|
+
const match = event?.match(/^(64|65);\d+;(\d+)[Mm]/);
|
|
546
1008
|
if (!match) return void 0;
|
|
547
|
-
const button = Number(match[1]);
|
|
548
1009
|
const row = Number(match[2]);
|
|
549
|
-
const
|
|
1010
|
+
const connectorHeight = getConnectorSectionHeight(state, size.height);
|
|
1011
|
+
const availablePaneRows = size.height - 7 - connectorHeight;
|
|
550
1012
|
const activityHeight = Math.floor(availablePaneRows / 2);
|
|
551
|
-
const
|
|
552
|
-
const
|
|
553
|
-
const pane = row >=
|
|
1013
|
+
const activityStartRow = 7 + connectorHeight;
|
|
1014
|
+
const debugStartRow = activityStartRow + activityHeight;
|
|
1015
|
+
const pane = row >= activityStartRow && row < debugStartRow ? "activity" : row >= debugStartRow && row < size.height ? "agentDebug" : void 0;
|
|
554
1016
|
if (!pane) return void 0;
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
1017
|
+
return { delta: match[1] === "64" ? 3 : -3, pane };
|
|
1018
|
+
};
|
|
1019
|
+
var reduceDashboardInput = (state, key, managerEnabled) => {
|
|
1020
|
+
if (state.prompt) {
|
|
1021
|
+
if (key === "\x1B") return { state: { ...state, prompt: void 0 } };
|
|
1022
|
+
if (isUpKey(key) || isDownKey(key)) {
|
|
1023
|
+
const delta = isUpKey(key) ? -1 : 1;
|
|
1024
|
+
const optionCount = promptOptionCount(state.prompt);
|
|
1025
|
+
const option = (state.prompt.option + delta + optionCount) % optionCount;
|
|
1026
|
+
return { state: { ...state, prompt: { ...state.prompt, option } } };
|
|
1027
|
+
}
|
|
1028
|
+
if (key !== "\r" && key !== "\n") return { state };
|
|
1029
|
+
if (state.prompt.kind === "stop-selected") {
|
|
1030
|
+
const connector = selectedConnector(state);
|
|
1031
|
+
return state.prompt.option === 1 && connector ? {
|
|
1032
|
+
command: {
|
|
1033
|
+
connectorId: connector.connectorId,
|
|
1034
|
+
serverUrl: connector.serverUrl,
|
|
1035
|
+
type: "stop-selected"
|
|
1036
|
+
},
|
|
1037
|
+
state: { ...state, prompt: void 0 }
|
|
1038
|
+
} : { state: { ...state, prompt: void 0 } };
|
|
1039
|
+
}
|
|
1040
|
+
if (state.prompt.kind === "stop-all-confirm") {
|
|
1041
|
+
return state.prompt.option === 1 ? { command: { type: "stop-all" }, state: { ...state, prompt: void 0 } } : { state: { ...state, prompt: void 0 } };
|
|
1042
|
+
}
|
|
1043
|
+
if (state.prompt.option === 0) {
|
|
1044
|
+
return { state: { ...state, prompt: { kind: "stop-all-confirm", option: 0 } } };
|
|
1045
|
+
}
|
|
1046
|
+
if (state.prompt.option === 1) {
|
|
1047
|
+
return { command: { type: "exit-dashboard" }, state: { ...state, prompt: void 0 } };
|
|
1048
|
+
}
|
|
1049
|
+
return { state: { ...state, prompt: void 0 } };
|
|
1050
|
+
}
|
|
1051
|
+
if (managerEnabled && key === "") {
|
|
1052
|
+
return { state: { ...state, prompt: { kind: "exit-choice", option: 1 } } };
|
|
1053
|
+
}
|
|
1054
|
+
if (managerEnabled && key === "\x1B") {
|
|
1055
|
+
return { command: { type: "exit-dashboard" }, state };
|
|
1056
|
+
}
|
|
1057
|
+
if (managerEnabled && (key === "s" || key === "S") && ["Online", "Running"].includes(selectedConnector(state)?.status ?? "")) {
|
|
1058
|
+
return { state: { ...state, prompt: { kind: "stop-selected", option: 0 } } };
|
|
1059
|
+
}
|
|
1060
|
+
if (key === " ") {
|
|
1061
|
+
const sections = managerEnabled ? ["connectors", "activity", "agentDebug"] : ["activity", "agentDebug"];
|
|
1062
|
+
const index = sections.indexOf(state.activeSection);
|
|
1063
|
+
const activeSection = sections[(index + 1) % sections.length] ?? sections[0];
|
|
1064
|
+
return {
|
|
1065
|
+
state: {
|
|
1066
|
+
...state,
|
|
1067
|
+
activePane: activeSection === "connectors" ? state.activePane : activeSection,
|
|
1068
|
+
activeSection
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
if (state.activeSection === "connectors" && (isUpKey(key) || isDownKey(key))) {
|
|
1073
|
+
return { state: selectConnectorByOffset(state, isUpKey(key) ? -1 : 1) };
|
|
1074
|
+
}
|
|
1075
|
+
const pane = state.activePane;
|
|
1076
|
+
if (key === "f" || key === "F" || key === "\x1B[F") {
|
|
1077
|
+
return { state: scrollDashboardPane(state, pane, "follow") };
|
|
1078
|
+
}
|
|
1079
|
+
if (key === "\x1B[H") return { state: scrollDashboardPane(state, pane, "oldest") };
|
|
1080
|
+
if (isUpKey(key)) return { state: scrollDashboardPane(state, pane, 1) };
|
|
1081
|
+
if (isDownKey(key)) return { state: scrollDashboardPane(state, pane, -1) };
|
|
1082
|
+
if (key === "\x1B[5~") return { state: scrollDashboardPane(state, pane, 8) };
|
|
1083
|
+
if (key === "\x1B[6~") return { state: scrollDashboardPane(state, pane, -8) };
|
|
1084
|
+
return { state };
|
|
559
1085
|
};
|
|
560
|
-
var getTerminalCopyGesture = (termProgram = process.env.TERM_PROGRAM) => termProgram === "Apple_Terminal" ? "Fn+drag copy" : "Shift+drag copy";
|
|
561
1086
|
var renderStatus = (state, width, now) => {
|
|
562
1087
|
const innerWidth = width - 2;
|
|
563
1088
|
const status = (value) => value;
|
|
1089
|
+
const connector = selectedConnector(state);
|
|
564
1090
|
return [
|
|
565
1091
|
`+${"-".repeat(innerWidth)}+`,
|
|
566
|
-
`|${colors.cyan}${truncate(
|
|
1092
|
+
`|${colors.cyan}${truncate(` ${DASHBOARD_TITLE}`, innerWidth)}${colors.reset}|`,
|
|
1093
|
+
`|${truncate(` Connector ${connector?.label ?? connector?.connectorId ?? "None"} Space ${connector?.spaceId ?? "Unknown"}`, innerWidth)}|`,
|
|
567
1094
|
`|${truncate(` Relay ${status(state.relay)} Agent ${status(state.agent)} Heartbeat ${status(state.heartbeat)}`, innerWidth)}|`,
|
|
568
1095
|
`|${truncate(` Server ${state.server} Uptime ${formatDuration(now - state.startedAt)}`, innerWidth)}|`,
|
|
569
1096
|
`+${"-".repeat(innerWidth)}+`
|
|
570
1097
|
];
|
|
571
1098
|
};
|
|
1099
|
+
var renderConnectors = (state, height, width) => {
|
|
1100
|
+
const innerWidth = width - 2;
|
|
1101
|
+
const contentHeight = Math.max(1, height - 2);
|
|
1102
|
+
const selectedIndex = Math.max(
|
|
1103
|
+
0,
|
|
1104
|
+
state.connectors.findIndex((connector) => connector.connectorId === state.selectedConnectorId)
|
|
1105
|
+
);
|
|
1106
|
+
const start = Math.max(
|
|
1107
|
+
0,
|
|
1108
|
+
Math.min(
|
|
1109
|
+
selectedIndex - Math.floor(contentHeight / 2),
|
|
1110
|
+
state.connectors.length - contentHeight
|
|
1111
|
+
)
|
|
1112
|
+
);
|
|
1113
|
+
const visible = state.connectors.slice(start, start + contentHeight);
|
|
1114
|
+
const range = state.connectors.length ? `${start + 1}-${start + visible.length}/${state.connectors.length}` : "0/0";
|
|
1115
|
+
const title = ` Connectors ${state.activeSection === "connectors" ? "[active]" : ""}`;
|
|
1116
|
+
const headerFill = Math.max(1, innerWidth - title.length - range.length);
|
|
1117
|
+
const output = [
|
|
1118
|
+
`${state.activeSection === "connectors" ? colors.reverse : ""}+${title}${"-".repeat(headerFill)}${range}+${colors.reset}`
|
|
1119
|
+
];
|
|
1120
|
+
for (let index = 0; index < contentHeight; index += 1) {
|
|
1121
|
+
const connector = visible[index];
|
|
1122
|
+
if (!connector) {
|
|
1123
|
+
output.push(`|${" ".repeat(innerWidth)}|`);
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
1126
|
+
const marker = connector.connectorId === state.selectedConnectorId ? ">" : " ";
|
|
1127
|
+
const statusText = connector.status.padEnd(8);
|
|
1128
|
+
const availableFieldWidth = Math.max(24, innerWidth - 18);
|
|
1129
|
+
const connectorWidth = Math.max(10, Math.floor(availableFieldWidth * 0.4));
|
|
1130
|
+
const spaceWidth = Math.max(8, Math.floor(availableFieldWidth * 0.25));
|
|
1131
|
+
const serverWidth = Math.max(8, availableFieldWidth - connectorWidth - spaceWidth);
|
|
1132
|
+
const content = `${marker} ${statusText} ${truncate(connector.label ?? connector.connectorId, connectorWidth)} | ${truncate(connector.spaceId, spaceWidth)} | ${truncate(connector.serverUrl, serverWidth)}`;
|
|
1133
|
+
const color = connector.status === "Stopped" ? colors.dim : connector.status === "Online" ? colors.cyan : connector.status === "Connecting" || connector.status === "Stopping" ? colors.yellow : connector.status === "Error" || connector.status === "Unresponsive" ? colors.red : "";
|
|
1134
|
+
output.push(`|${color}${truncate(content, innerWidth)}${colors.reset}|`);
|
|
1135
|
+
}
|
|
1136
|
+
output.push(`+${"-".repeat(innerWidth)}+`);
|
|
1137
|
+
return output;
|
|
1138
|
+
};
|
|
572
1139
|
var renderPane = (title, lines, height, width, active, scroll) => {
|
|
573
1140
|
const innerWidth = width - 2;
|
|
574
1141
|
const contentHeight = Math.max(1, height - 2);
|
|
@@ -594,29 +1161,67 @@ var renderPane = (title, lines, height, width, active, scroll) => {
|
|
|
594
1161
|
output.push(`+${"-".repeat(innerWidth)}+`);
|
|
595
1162
|
return output;
|
|
596
1163
|
};
|
|
597
|
-
var renderFooter = (width) =>
|
|
1164
|
+
var renderFooter = (state, width) => {
|
|
1165
|
+
const managerKeys = state.connectors.length ? "Up/Down select | S stop | Esc exit | " : "";
|
|
1166
|
+
return `${colors.dim}${truncate(` ${managerKeys}Tab switch pane | F follow`, width)}${colors.reset}`;
|
|
1167
|
+
};
|
|
1168
|
+
var renderPrompt = (state, size) => {
|
|
1169
|
+
const width = Math.max(10, size.width);
|
|
1170
|
+
const height = Math.max(4, size.height);
|
|
1171
|
+
const connector = selectedConnector(state);
|
|
1172
|
+
const prompt = state.prompt;
|
|
1173
|
+
if (!prompt) return "";
|
|
1174
|
+
const content = prompt.kind === "exit-choice" ? {
|
|
1175
|
+
options: ["Stop all local Connectors and exit", "Exit Dashboard only", "Cancel"],
|
|
1176
|
+
title: "Exit Buda Connector Dashboard?",
|
|
1177
|
+
warning: "Dashboard exit does not stop background Connector processes."
|
|
1178
|
+
} : prompt.kind === "stop-all-confirm" ? {
|
|
1179
|
+
options: ["Cancel", "Stop all local Connectors and exit"],
|
|
1180
|
+
title: "Stop every local Connector?",
|
|
1181
|
+
warning: "Running agent tasks will be interrupted."
|
|
1182
|
+
} : {
|
|
1183
|
+
options: ["Cancel", "Stop selected Connector"],
|
|
1184
|
+
title: `Stop ${connector?.connectorId ?? "selected Connector"}?`,
|
|
1185
|
+
warning: "The Dashboard will remain open."
|
|
1186
|
+
};
|
|
1187
|
+
const lines = [
|
|
1188
|
+
content.title,
|
|
1189
|
+
"",
|
|
1190
|
+
content.warning,
|
|
1191
|
+
"",
|
|
1192
|
+
...content.options.map((option, index) => `${index === prompt.option ? ">" : " "} ${option}`),
|
|
1193
|
+
"",
|
|
1194
|
+
"Up/Down choose | Enter confirm | Esc cancel"
|
|
1195
|
+
];
|
|
1196
|
+
const visible = lines.slice(0, height);
|
|
1197
|
+
while (visible.length < height) visible.push("");
|
|
1198
|
+
return visible.map((line) => truncate(line, width)).join("\n");
|
|
1199
|
+
};
|
|
598
1200
|
var renderDashboard = (state, size, now = Date.now()) => {
|
|
599
1201
|
const width = Math.max(10, size.width);
|
|
600
1202
|
const height = Math.max(4, size.height);
|
|
601
1203
|
if (width < MIN_DASHBOARD_WIDTH || height < MIN_DASHBOARD_HEIGHT) {
|
|
602
1204
|
return [
|
|
603
|
-
truncate(
|
|
1205
|
+
truncate(DASHBOARD_TITLE, width),
|
|
604
1206
|
truncate("Terminal too small for dashboard. Resize or restart with --plain.", width),
|
|
605
1207
|
truncate(`Relay ${state.relay} | Agent ${state.agent}`, width),
|
|
606
1208
|
truncate(`Heartbeat ${state.heartbeat}`, width)
|
|
607
1209
|
].slice(0, height).join("\n");
|
|
608
1210
|
}
|
|
609
|
-
|
|
1211
|
+
if (state.prompt) return renderPrompt(state, size);
|
|
1212
|
+
const connectorHeight = getConnectorSectionHeight(state, height);
|
|
1213
|
+
const availablePaneRows = height - 7 - connectorHeight;
|
|
610
1214
|
const activityHeight = Math.floor(availablePaneRows / 2);
|
|
611
1215
|
const debugHeight = availablePaneRows - activityHeight;
|
|
612
1216
|
return [
|
|
613
1217
|
...renderStatus(state, width, now),
|
|
1218
|
+
...connectorHeight ? renderConnectors(state, connectorHeight, width) : [],
|
|
614
1219
|
...renderPane(
|
|
615
1220
|
"Activity",
|
|
616
1221
|
state.activity,
|
|
617
1222
|
activityHeight,
|
|
618
1223
|
width,
|
|
619
|
-
state.
|
|
1224
|
+
state.activeSection === "activity",
|
|
620
1225
|
state.scroll.activity
|
|
621
1226
|
),
|
|
622
1227
|
...renderPane(
|
|
@@ -624,15 +1229,16 @@ var renderDashboard = (state, size, now = Date.now()) => {
|
|
|
624
1229
|
state.agentDebug,
|
|
625
1230
|
debugHeight,
|
|
626
1231
|
width,
|
|
627
|
-
state.
|
|
1232
|
+
state.activeSection === "agentDebug",
|
|
628
1233
|
state.scroll.agentDebug
|
|
629
1234
|
),
|
|
630
|
-
renderFooter(width)
|
|
1235
|
+
renderFooter(state, width)
|
|
631
1236
|
].join("\n");
|
|
632
1237
|
};
|
|
633
1238
|
var ConnectorConsoleDashboard = class {
|
|
634
|
-
constructor(streams) {
|
|
1239
|
+
constructor(streams, manager) {
|
|
635
1240
|
this.streams = streams;
|
|
1241
|
+
this.manager = manager;
|
|
636
1242
|
}
|
|
637
1243
|
state = createDashboardState();
|
|
638
1244
|
stopped = false;
|
|
@@ -658,6 +1264,38 @@ var ConnectorConsoleDashboard = class {
|
|
|
658
1264
|
this.state = { ...this.state, ...update };
|
|
659
1265
|
this.scheduleRender();
|
|
660
1266
|
}
|
|
1267
|
+
updateConnectors(connectors, preferredConnectorId) {
|
|
1268
|
+
this.state = updateDashboardConnectors(this.state, connectors, preferredConnectorId);
|
|
1269
|
+
this.scheduleRender();
|
|
1270
|
+
}
|
|
1271
|
+
appendConnectorLog(connectorId, level, raw) {
|
|
1272
|
+
const current = this.state.connectorLogs[connectorId] ?? { activity: [], agentDebug: [] };
|
|
1273
|
+
let connectorState = { ...this.state, ...current };
|
|
1274
|
+
const formatted = raw;
|
|
1275
|
+
for (const rawLine of formatted.split("\n")) {
|
|
1276
|
+
const { pane, message } = classifyDashboardLine(rawLine);
|
|
1277
|
+
if (!message) continue;
|
|
1278
|
+
connectorState = appendDashboardLine(
|
|
1279
|
+
connectorState,
|
|
1280
|
+
pane,
|
|
1281
|
+
classifyDashboardLevel(level, formatted, rawLine),
|
|
1282
|
+
message
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
const connectorLogs = {
|
|
1286
|
+
...this.state.connectorLogs,
|
|
1287
|
+
[connectorId]: {
|
|
1288
|
+
activity: connectorState.activity,
|
|
1289
|
+
agentDebug: connectorState.agentDebug
|
|
1290
|
+
}
|
|
1291
|
+
};
|
|
1292
|
+
this.state = {
|
|
1293
|
+
...this.state,
|
|
1294
|
+
connectorLogs,
|
|
1295
|
+
...connectorId === this.state.selectedConnectorId ? { activity: connectorState.activity, agentDebug: connectorState.agentDebug } : {}
|
|
1296
|
+
};
|
|
1297
|
+
this.scheduleRender();
|
|
1298
|
+
}
|
|
661
1299
|
stop = () => {
|
|
662
1300
|
if (this.stopped) return;
|
|
663
1301
|
this.stopped = true;
|
|
@@ -689,43 +1327,59 @@ var ConnectorConsoleDashboard = class {
|
|
|
689
1327
|
}
|
|
690
1328
|
handleInput = (chunk) => {
|
|
691
1329
|
const key = chunk.toString();
|
|
692
|
-
if (key === "") {
|
|
1330
|
+
if (key === "" && !this.manager) {
|
|
693
1331
|
this.stop();
|
|
694
1332
|
process.kill(process.pid, "SIGINT");
|
|
695
1333
|
return;
|
|
696
1334
|
}
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
};
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
} else if (key === "f" || key === "F" || key === "\x1B[F") {
|
|
713
|
-
this.state = scrollDashboardPane(this.state, this.state.activePane, "follow");
|
|
714
|
-
} else if (key === "\x1B[H") {
|
|
715
|
-
this.state = scrollDashboardPane(this.state, this.state.activePane, "oldest");
|
|
716
|
-
} else if (key === "\x1B[A") {
|
|
717
|
-
this.state = scrollDashboardPane(this.state, this.state.activePane, 1);
|
|
718
|
-
} else if (key === "\x1B[B") {
|
|
719
|
-
this.state = scrollDashboardPane(this.state, this.state.activePane, -1);
|
|
720
|
-
} else if (key === "\x1B[5~") {
|
|
721
|
-
this.state = scrollDashboardPane(this.state, this.state.activePane, 8);
|
|
722
|
-
} else if (key === "\x1B[6~") {
|
|
723
|
-
this.state = scrollDashboardPane(this.state, this.state.activePane, -8);
|
|
724
|
-
} else {
|
|
1335
|
+
const mouseWheel = parseDashboardMouseWheel(key, this.state, {
|
|
1336
|
+
height: this.streams.output.rows || 24,
|
|
1337
|
+
width: this.streams.output.columns || 80
|
|
1338
|
+
});
|
|
1339
|
+
if (mouseWheel) {
|
|
1340
|
+
this.state = scrollDashboardPane(
|
|
1341
|
+
{
|
|
1342
|
+
...this.state,
|
|
1343
|
+
activePane: mouseWheel.pane,
|
|
1344
|
+
activeSection: mouseWheel.pane
|
|
1345
|
+
},
|
|
1346
|
+
mouseWheel.pane,
|
|
1347
|
+
mouseWheel.delta
|
|
1348
|
+
);
|
|
1349
|
+
this.scheduleRender();
|
|
725
1350
|
return;
|
|
726
1351
|
}
|
|
1352
|
+
const result = reduceDashboardInput(this.state, key, Boolean(this.manager));
|
|
1353
|
+
this.state = result.state;
|
|
727
1354
|
this.scheduleRender();
|
|
1355
|
+
if (result.command) void this.runCommand(result.command);
|
|
728
1356
|
};
|
|
1357
|
+
async runCommand(command) {
|
|
1358
|
+
if (!this.manager) return;
|
|
1359
|
+
if (command.type === "exit-dashboard") {
|
|
1360
|
+
this.stop();
|
|
1361
|
+
this.manager.onExit();
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
try {
|
|
1365
|
+
if (command.type === "stop-selected") {
|
|
1366
|
+
this.state = {
|
|
1367
|
+
...this.state,
|
|
1368
|
+
connectors: this.state.connectors.map(
|
|
1369
|
+
(connector) => connector.connectorId === command.connectorId ? { ...connector, status: "Stopping" } : connector
|
|
1370
|
+
)
|
|
1371
|
+
};
|
|
1372
|
+
this.scheduleRender();
|
|
1373
|
+
await this.manager.onStopConnector(command.connectorId, command.serverUrl);
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
await this.manager.onStopAll();
|
|
1377
|
+
this.stop();
|
|
1378
|
+
this.manager.onExit();
|
|
1379
|
+
} catch (error) {
|
|
1380
|
+
this.capture("error", [error instanceof Error ? error.message : String(error)]);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
729
1383
|
scheduleRender = () => {
|
|
730
1384
|
if (this.stopped || this.renderQueued) return;
|
|
731
1385
|
this.renderQueued = true;
|
|
@@ -744,30 +1398,49 @@ var startConnectorConsoleDashboard = (options = {}) => {
|
|
|
744
1398
|
const streams = options.streams ?? resolveDashboardStreams();
|
|
745
1399
|
if (!streams?.input.isTTY || !streams.output.isTTY) return void 0;
|
|
746
1400
|
if (activeDashboard) return activeDashboard;
|
|
747
|
-
activeDashboard = new ConnectorConsoleDashboard(streams);
|
|
1401
|
+
activeDashboard = new ConnectorConsoleDashboard(streams, options.manager);
|
|
748
1402
|
activeDashboard.start();
|
|
749
1403
|
return activeDashboard;
|
|
750
1404
|
};
|
|
751
1405
|
var updateConnectorDashboardStatus = (update) => {
|
|
752
1406
|
activeDashboard?.updateStatus(update);
|
|
753
1407
|
};
|
|
1408
|
+
var updateConnectorDashboardConnectors = (connectors, preferredConnectorId) => {
|
|
1409
|
+
activeDashboard?.updateConnectors(connectors, preferredConnectorId);
|
|
1410
|
+
};
|
|
1411
|
+
var appendConnectorDashboardLog = (connectorId, level, raw) => {
|
|
1412
|
+
activeDashboard?.appendConnectorLog(connectorId, level, raw);
|
|
1413
|
+
};
|
|
754
1414
|
|
|
755
|
-
// src/
|
|
756
|
-
import {
|
|
1415
|
+
// src/daemon-client.ts
|
|
1416
|
+
import { spawn } from "child_process";
|
|
1417
|
+
import { mkdir as mkdir6, readFile as readFile6 } from "fs/promises";
|
|
1418
|
+
import path7 from "path";
|
|
757
1419
|
|
|
758
|
-
// src/
|
|
759
|
-
import {
|
|
760
|
-
import {
|
|
1420
|
+
// src/cloud-client.ts
|
|
1421
|
+
import { createHash as createHash4 } from "crypto";
|
|
1422
|
+
import { platform as platform3, release } from "os";
|
|
1423
|
+
|
|
1424
|
+
// src/browser.ts
|
|
1425
|
+
import { accessSync, constants, statSync } from "fs";
|
|
1426
|
+
import { mkdir as mkdir4 } from "fs/promises";
|
|
1427
|
+
import { platform as getPlatform, homedir as homedir2 } from "os";
|
|
1428
|
+
import path4 from "path";
|
|
761
1429
|
|
|
762
1430
|
// src/logger.ts
|
|
763
|
-
import { appendFile, mkdir as
|
|
764
|
-
import
|
|
1431
|
+
import { appendFile, mkdir as mkdir3, readdir as readdir2, rename as rename3, stat as stat2, unlink as unlink4 } from "fs/promises";
|
|
1432
|
+
import path3 from "path";
|
|
1433
|
+
import { format as format2 } from "util";
|
|
765
1434
|
var CONNECTOR_LOG_MAX_BYTES = 2 * 1024 * 1024;
|
|
766
1435
|
var CONNECTOR_LOG_BACKUP_COUNT = 2;
|
|
767
1436
|
var CONNECTOR_LOG_RETENTION_DAYS = 7;
|
|
768
1437
|
var pendingLogs = [];
|
|
769
1438
|
var fileWriteQueue = Promise.resolve();
|
|
770
1439
|
var lastPrunedDate = "";
|
|
1440
|
+
var activeLogContext;
|
|
1441
|
+
var setConnectorLogContext = (context) => {
|
|
1442
|
+
activeLogContext = { ...activeLogContext, ...context };
|
|
1443
|
+
};
|
|
771
1444
|
var ignoreMissingFile = (error) => {
|
|
772
1445
|
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
|
|
773
1446
|
return;
|
|
@@ -781,17 +1454,17 @@ var formatConnectorLogDate = (date) => {
|
|
|
781
1454
|
return `${year}-${month}-${day}`;
|
|
782
1455
|
};
|
|
783
1456
|
var getDatedConnectorLogPath = (baseLogPath, date = /* @__PURE__ */ new Date()) => {
|
|
784
|
-
const parsed =
|
|
785
|
-
return
|
|
1457
|
+
const parsed = path3.parse(baseLogPath);
|
|
1458
|
+
return path3.join(parsed.dir, `${parsed.name}-${formatConnectorLogDate(date)}${parsed.ext}`);
|
|
786
1459
|
};
|
|
787
1460
|
async function pruneConnectorLogs(baseLogPath, now = /* @__PURE__ */ new Date(), retentionDays = CONNECTOR_LOG_RETENTION_DAYS) {
|
|
788
|
-
const parsed =
|
|
1461
|
+
const parsed = path3.parse(baseLogPath);
|
|
789
1462
|
const prefix = `${parsed.name}-`;
|
|
790
1463
|
const cutoff = new Date(now);
|
|
791
1464
|
cutoff.setHours(0, 0, 0, 0);
|
|
792
1465
|
cutoff.setDate(cutoff.getDate() - Math.max(0, retentionDays - 1));
|
|
793
1466
|
const cutoffDate = formatConnectorLogDate(cutoff);
|
|
794
|
-
const files = await
|
|
1467
|
+
const files = await readdir2(parsed.dir).catch(() => []);
|
|
795
1468
|
const expired = files.filter((fileName) => {
|
|
796
1469
|
if (!fileName.startsWith(prefix)) return false;
|
|
797
1470
|
const date = fileName.slice(prefix.length, prefix.length + 10);
|
|
@@ -801,7 +1474,7 @@ async function pruneConnectorLogs(baseLogPath, now = /* @__PURE__ */ new Date(),
|
|
|
801
1474
|
const isLogSegment = suffix === parsed.ext || /^\d+$/.test(segment);
|
|
802
1475
|
return isDatedLog && isLogSegment && date < cutoffDate;
|
|
803
1476
|
});
|
|
804
|
-
await Promise.all(expired.map((fileName) =>
|
|
1477
|
+
await Promise.all(expired.map((fileName) => unlink4(path3.join(parsed.dir, fileName))));
|
|
805
1478
|
return expired.length;
|
|
806
1479
|
}
|
|
807
1480
|
async function rotateConnectorLogIfNeeded(logPath, maxBytes = CONNECTOR_LOG_MAX_BYTES, backupCount = CONNECTOR_LOG_BACKUP_COUNT) {
|
|
@@ -810,11 +1483,11 @@ async function rotateConnectorLogIfNeeded(logPath, maxBytes = CONNECTOR_LOG_MAX_
|
|
|
810
1483
|
return 0;
|
|
811
1484
|
});
|
|
812
1485
|
if (currentSize < maxBytes) return false;
|
|
813
|
-
await
|
|
1486
|
+
await unlink4(`${logPath}.${backupCount}`).catch(ignoreMissingFile);
|
|
814
1487
|
for (let index = backupCount - 1; index >= 1; index -= 1) {
|
|
815
|
-
await
|
|
1488
|
+
await rename3(`${logPath}.${index}`, `${logPath}.${index + 1}`).catch(ignoreMissingFile);
|
|
816
1489
|
}
|
|
817
|
-
await
|
|
1490
|
+
await rename3(logPath, `${logPath}.1`).catch(ignoreMissingFile);
|
|
818
1491
|
return true;
|
|
819
1492
|
}
|
|
820
1493
|
var appendConnectorLog = (baseLogPath, line, now = /* @__PURE__ */ new Date()) => {
|
|
@@ -832,19 +1505,62 @@ var appendConnectorLog = (baseLogPath, line, now = /* @__PURE__ */ new Date()) =
|
|
|
832
1505
|
fileWriteQueue = queuedWrite.catch(() => void 0);
|
|
833
1506
|
return queuedWrite;
|
|
834
1507
|
};
|
|
1508
|
+
var persistLocalEntry = async (entry) => {
|
|
1509
|
+
const paths = getConnectorPaths(
|
|
1510
|
+
activeLogContext?.serverUrl ?? getServerUrl(),
|
|
1511
|
+
activeLogContext?.configRoot
|
|
1512
|
+
);
|
|
1513
|
+
await mkdir3(path3.dirname(paths.logPath), { recursive: true }).catch(() => void 0);
|
|
1514
|
+
await appendConnectorLog(paths.logPath, `${JSON.stringify(entry)}
|
|
1515
|
+
`).catch(() => void 0);
|
|
1516
|
+
};
|
|
1517
|
+
var installConnectorConsoleCapture = () => {
|
|
1518
|
+
const writes = [];
|
|
1519
|
+
const original = {
|
|
1520
|
+
error: console.error,
|
|
1521
|
+
log: console.log,
|
|
1522
|
+
warn: console.warn
|
|
1523
|
+
};
|
|
1524
|
+
const capture = (level, output) => (...args) => {
|
|
1525
|
+
const message = format2(...args);
|
|
1526
|
+
if (!message.startsWith("[buda-connector]")) {
|
|
1527
|
+
writes.push(
|
|
1528
|
+
persistLocalEntry({
|
|
1529
|
+
level,
|
|
1530
|
+
message,
|
|
1531
|
+
metadata: {
|
|
1532
|
+
source: "daemon-console",
|
|
1533
|
+
...activeLogContext?.connectorId ? { connectorId: activeLogContext.connectorId } : {}
|
|
1534
|
+
},
|
|
1535
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1536
|
+
})
|
|
1537
|
+
);
|
|
1538
|
+
}
|
|
1539
|
+
output(...args);
|
|
1540
|
+
};
|
|
1541
|
+
console.log = capture("info", original.log);
|
|
1542
|
+
console.warn = capture("warn", original.warn);
|
|
1543
|
+
console.error = capture("error", original.error);
|
|
1544
|
+
return async () => {
|
|
1545
|
+
console.log = original.log;
|
|
1546
|
+
console.warn = original.warn;
|
|
1547
|
+
console.error = original.error;
|
|
1548
|
+
await Promise.all(writes);
|
|
1549
|
+
};
|
|
1550
|
+
};
|
|
835
1551
|
async function writeLog(level, message, metadata) {
|
|
836
1552
|
const entry = {
|
|
837
1553
|
level,
|
|
838
1554
|
message,
|
|
839
|
-
metadata
|
|
1555
|
+
metadata: {
|
|
1556
|
+
...metadata ?? {},
|
|
1557
|
+
...activeLogContext?.connectorId ? { connectorId: activeLogContext.connectorId } : {}
|
|
1558
|
+
},
|
|
840
1559
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
841
1560
|
};
|
|
842
1561
|
pendingLogs.push(entry);
|
|
843
1562
|
if (pendingLogs.length > 100) pendingLogs.splice(0, pendingLogs.length - 100);
|
|
844
|
-
|
|
845
|
-
await mkdir2(path2.dirname(paths.logPath), { recursive: true }).catch(() => void 0);
|
|
846
|
-
await appendConnectorLog(paths.logPath, `${JSON.stringify(entry)}
|
|
847
|
-
`).catch(() => void 0);
|
|
1563
|
+
await persistLocalEntry(entry);
|
|
848
1564
|
const line = `[buda-connector] ${message}`;
|
|
849
1565
|
if (level === "error") console.error(line);
|
|
850
1566
|
else if (level === "warn") console.warn(line);
|
|
@@ -857,767 +1573,376 @@ function restorePendingLogs(logs) {
|
|
|
857
1573
|
pendingLogs.unshift(...logs);
|
|
858
1574
|
}
|
|
859
1575
|
|
|
860
|
-
// src/
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
var TERMINAL_IDLE_TTL_MS = 10 * 6e4;
|
|
865
|
-
var TERMINAL_HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
866
|
-
var LOCAL_TERMINAL_GRANT_TTL_MS = 3e4;
|
|
867
|
-
var LOCAL_TERMINAL_CLOSE_SPAWN_FAILED = 4401;
|
|
868
|
-
var MAX_BUFFER_CHUNKS = 1e3;
|
|
869
|
-
var MAX_TERMINAL_FRAME_BYTES = 1024 * 1024;
|
|
870
|
-
var normalizeTerminalCloseCode = (code, fallback = 1011) => code >= 1e3 && code <= 1014 && ![1004, 1005, 1006].includes(code) || code >= 3e3 && code <= 4999 ? code : fallback;
|
|
871
|
-
var truncateTerminalCloseReason = (reason, maxBytes = 120) => {
|
|
872
|
-
let result = reason;
|
|
873
|
-
while (Buffer.byteLength(result, "utf8") > maxBytes) result = result.slice(0, -1);
|
|
874
|
-
return result;
|
|
875
|
-
};
|
|
1576
|
+
// src/browser.ts
|
|
1577
|
+
var DEFAULT_VIEWPORT = { width: 1280, height: 800 };
|
|
1578
|
+
var MAX_SCREENCAST_DIMENSION = 2560;
|
|
1579
|
+
var SESSION_ID_PATTERN = /^[A-Za-z0-9_.:-]{1,320}$/;
|
|
876
1580
|
var sessions = /* @__PURE__ */ new Map();
|
|
877
|
-
var
|
|
878
|
-
var
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
1581
|
+
var sessionPromises = /* @__PURE__ */ new Map();
|
|
1582
|
+
var getBrowserMode = () => {
|
|
1583
|
+
const configured = process.env.BUDA_CONNECTOR_BROWSER_MODE?.trim().toLowerCase();
|
|
1584
|
+
return configured === "auto" || configured === "headed" || configured === "headless" ? configured : "headless";
|
|
1585
|
+
};
|
|
1586
|
+
var getSystemBrowserCandidates = (platform4, homeDirectory, env) => {
|
|
1587
|
+
if (platform4 === "darwin") {
|
|
1588
|
+
return [
|
|
1589
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
1590
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
1591
|
+
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
1592
|
+
`${homeDirectory}/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`,
|
|
1593
|
+
`${homeDirectory}/Applications/Chromium.app/Contents/MacOS/Chromium`,
|
|
1594
|
+
`${homeDirectory}/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge`
|
|
1595
|
+
];
|
|
884
1596
|
}
|
|
885
|
-
if (
|
|
886
|
-
|
|
1597
|
+
if (platform4 === "win32") {
|
|
1598
|
+
const localAppData = env.LOCALAPPDATA;
|
|
1599
|
+
const programFiles = env.PROGRAMFILES;
|
|
1600
|
+
const programFilesX86 = env["PROGRAMFILES(X86)"];
|
|
1601
|
+
return [
|
|
1602
|
+
localAppData ? `${localAppData}\\Google\\Chrome\\Application\\chrome.exe` : "",
|
|
1603
|
+
localAppData ? `${localAppData}\\Chromium\\Application\\chrome.exe` : "",
|
|
1604
|
+
programFiles ? `${programFiles}\\Google\\Chrome\\Application\\chrome.exe` : "",
|
|
1605
|
+
programFilesX86 ? `${programFilesX86}\\Google\\Chrome\\Application\\chrome.exe` : "",
|
|
1606
|
+
programFiles ? `${programFiles}\\Microsoft\\Edge\\Application\\msedge.exe` : "",
|
|
1607
|
+
programFilesX86 ? `${programFilesX86}\\Microsoft\\Edge\\Application\\msedge.exe` : ""
|
|
1608
|
+
];
|
|
887
1609
|
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1610
|
+
return [
|
|
1611
|
+
"/usr/bin/google-chrome",
|
|
1612
|
+
"/usr/bin/google-chrome-stable",
|
|
1613
|
+
"/usr/bin/chromium",
|
|
1614
|
+
"/usr/bin/chromium-browser",
|
|
1615
|
+
"/snap/bin/chromium",
|
|
1616
|
+
"/usr/bin/microsoft-edge",
|
|
1617
|
+
"/usr/bin/microsoft-edge-stable"
|
|
1618
|
+
];
|
|
1619
|
+
};
|
|
1620
|
+
var resolveBrowserAvailability = (options = {}) => {
|
|
1621
|
+
const env = options.env ?? process.env;
|
|
1622
|
+
const platform4 = options.platform ?? getPlatform();
|
|
1623
|
+
const homeDirectory = options.homeDirectory ?? homedir2();
|
|
1624
|
+
const pathIsExecutable = options.pathIsExecutable ?? ((candidate) => {
|
|
1625
|
+
try {
|
|
1626
|
+
if (!statSync(candidate).isFile()) return false;
|
|
1627
|
+
accessSync(candidate, constants.X_OK);
|
|
1628
|
+
return true;
|
|
1629
|
+
} catch {
|
|
1630
|
+
return false;
|
|
1631
|
+
}
|
|
1632
|
+
});
|
|
1633
|
+
const configuredPath = env.BUDA_CONNECTOR_BROWSER_PATH?.trim();
|
|
1634
|
+
if (configuredPath) {
|
|
1635
|
+
if (pathIsExecutable(configuredPath)) {
|
|
1636
|
+
return { available: true, executablePath: configuredPath };
|
|
1637
|
+
}
|
|
1638
|
+
return {
|
|
1639
|
+
available: false,
|
|
1640
|
+
error: `BUDA_CONNECTOR_BROWSER_PATH is missing or not executable: ${configuredPath}. Set it to a Chrome, Chromium, or Edge executable.`
|
|
1641
|
+
};
|
|
891
1642
|
}
|
|
892
|
-
|
|
893
|
-
|
|
1643
|
+
const executablePath = getSystemBrowserCandidates(platform4, homeDirectory, env).find(
|
|
1644
|
+
(candidate) => candidate && pathIsExecutable(candidate)
|
|
1645
|
+
);
|
|
1646
|
+
if (executablePath) {
|
|
1647
|
+
return { available: true, executablePath };
|
|
894
1648
|
}
|
|
895
1649
|
return {
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
token: body.token,
|
|
899
|
-
cols: parseDimension(body.cols, 80, 10, 500),
|
|
900
|
-
rows: parseDimension(body.rows, 24, 5, 200)
|
|
1650
|
+
available: false,
|
|
1651
|
+
error: "No supported system browser was found. Install Google Chrome, Chromium, or Microsoft Edge, or set BUDA_CONNECTOR_BROWSER_PATH to its executable. The connector browser capability will not be advertised."
|
|
901
1652
|
};
|
|
902
1653
|
};
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
if (session.clients.size > 0 || sessions.get(sessionKey) !== session) return;
|
|
912
|
-
sessions.delete(sessionKey);
|
|
1654
|
+
async function launchPlaywright(viewport = DEFAULT_VIEWPORT) {
|
|
1655
|
+
const availability = resolveBrowserAvailability();
|
|
1656
|
+
if (!availability.available) throw new Error(availability.error);
|
|
1657
|
+
const { chromium } = await import("playwright-core");
|
|
1658
|
+
const mode = getBrowserMode();
|
|
1659
|
+
const executablePath = availability.executablePath;
|
|
1660
|
+
let browser = null;
|
|
1661
|
+
if (mode !== "headless") {
|
|
913
1662
|
try {
|
|
914
|
-
|
|
915
|
-
|
|
1663
|
+
await writeLog("info", `launching headed system browser ${executablePath}`);
|
|
1664
|
+
browser = await chromium.launch({
|
|
1665
|
+
executablePath,
|
|
1666
|
+
headless: false,
|
|
1667
|
+
args: ["--no-sandbox", "--disable-setuid-sandbox"]
|
|
1668
|
+
});
|
|
1669
|
+
} catch (error) {
|
|
1670
|
+
if (mode === "headed") throw error;
|
|
1671
|
+
await writeLog(
|
|
1672
|
+
"warn",
|
|
1673
|
+
`headed browser launch failed, falling back to headless: ${error instanceof Error ? error.message : String(error)}`
|
|
1674
|
+
);
|
|
916
1675
|
}
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
};
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
1676
|
+
}
|
|
1677
|
+
if (!browser) {
|
|
1678
|
+
await writeLog("info", `launching headless system browser ${executablePath}`);
|
|
1679
|
+
browser = await chromium.launch({
|
|
1680
|
+
executablePath,
|
|
1681
|
+
headless: true,
|
|
1682
|
+
args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu"]
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
const context = await browser.newContext({ viewport, deviceScaleFactor: 1 });
|
|
1686
|
+
const page = await context.newPage();
|
|
1687
|
+
const cdpSession = await context.newCDPSession(page);
|
|
1688
|
+
return { browser, page, cdpSession };
|
|
1689
|
+
}
|
|
1690
|
+
var normalizeSessionId = (value) => {
|
|
1691
|
+
if (typeof value !== "string") return null;
|
|
1692
|
+
const normalized = value.trim();
|
|
1693
|
+
return SESSION_ID_PATTERN.test(normalized) ? normalized : null;
|
|
924
1694
|
};
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1695
|
+
async function ensureSession(sessionId, initialViewport = DEFAULT_VIEWPORT) {
|
|
1696
|
+
const activeSession = sessions.get(sessionId);
|
|
1697
|
+
if (activeSession) return activeSession;
|
|
1698
|
+
let sessionPromise = sessionPromises.get(sessionId);
|
|
1699
|
+
if (!sessionPromise) {
|
|
1700
|
+
sessionPromise = (async () => {
|
|
1701
|
+
await writeLog("info", `launching local browser session ${sessionId}`);
|
|
1702
|
+
const { browser, page, cdpSession } = await launchPlaywright(initialViewport);
|
|
1703
|
+
const nextSession = {
|
|
1704
|
+
page,
|
|
1705
|
+
browser,
|
|
1706
|
+
cdpSession,
|
|
1707
|
+
screencastClients: /* @__PURE__ */ new Set(),
|
|
1708
|
+
isScreencasting: false,
|
|
1709
|
+
screencastListenerAttached: false,
|
|
1710
|
+
resizeQueue: Promise.resolve(),
|
|
1711
|
+
inputQueue: Promise.resolve(),
|
|
1712
|
+
navigationQueue: Promise.resolve(),
|
|
1713
|
+
clientIds: /* @__PURE__ */ new Set()
|
|
1714
|
+
};
|
|
1715
|
+
sessions.set(sessionId, nextSession);
|
|
1716
|
+
browser.on("disconnected", () => {
|
|
1717
|
+
if (sessions.get(sessionId) === nextSession) sessions.delete(sessionId);
|
|
1718
|
+
});
|
|
1719
|
+
return nextSession;
|
|
1720
|
+
})();
|
|
1721
|
+
sessionPromises.set(sessionId, sessionPromise);
|
|
938
1722
|
}
|
|
939
|
-
const shell = process.platform === "win32" ? "powershell.exe" : process.env.SHELL || "/bin/bash";
|
|
940
|
-
const shellArgs = process.platform === "win32" ? [] : ["-l"];
|
|
941
|
-
let pty;
|
|
942
1723
|
try {
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1724
|
+
return await sessionPromise;
|
|
1725
|
+
} finally {
|
|
1726
|
+
if (sessionPromises.get(sessionId) === sessionPromise) sessionPromises.delete(sessionId);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
async function startScreencast(s) {
|
|
1730
|
+
if (s.isScreencasting) return;
|
|
1731
|
+
if (!s.screencastListenerAttached) {
|
|
1732
|
+
s.screencastListenerAttached = true;
|
|
1733
|
+
s.cdpSession.on("Page.screencastFrame", (params) => {
|
|
1734
|
+
const metadata = params.metadata;
|
|
1735
|
+
const viewport = s.page.viewportSize() ?? DEFAULT_VIEWPORT;
|
|
1736
|
+
const frame = {
|
|
1737
|
+
data: params.data,
|
|
1738
|
+
viewportWidth: metadata.deviceWidth ?? viewport.width,
|
|
1739
|
+
viewportHeight: metadata.deviceHeight ?? viewport.height
|
|
1740
|
+
};
|
|
1741
|
+
for (const listener of s.screencastClients) listener(frame);
|
|
1742
|
+
void s.cdpSession.send("Page.screencastFrameAck", { sessionId: params.sessionId }).catch(() => {
|
|
1743
|
+
});
|
|
949
1744
|
});
|
|
950
|
-
} catch (error) {
|
|
951
|
-
const message = serializeErrorMessage(error);
|
|
952
|
-
void writeLog(
|
|
953
|
-
"error",
|
|
954
|
-
`local terminal failed to spawn shell=${shell} cwd=${grant.cwd}: ${message}`
|
|
955
|
-
);
|
|
956
|
-
throw new LocalTerminalUnavailableError(
|
|
957
|
-
"Local terminal is unavailable because node-pty could not start a shell. The connector remains available for other capabilities."
|
|
958
|
-
);
|
|
959
1745
|
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
idleTimer: null
|
|
967
|
-
};
|
|
968
|
-
sessions.set(grant.sessionKey, session);
|
|
969
|
-
pty.onData((data) => {
|
|
970
|
-
session.buffer.push(data);
|
|
971
|
-
if (session.buffer.length > MAX_BUFFER_CHUNKS) session.buffer.shift();
|
|
972
|
-
for (const socket of session.clients) {
|
|
973
|
-
if (socket.readyState === WebSocket.OPEN) socket.send(data);
|
|
974
|
-
}
|
|
1746
|
+
await s.cdpSession.send("Page.startScreencast", {
|
|
1747
|
+
format: "jpeg",
|
|
1748
|
+
quality: 90,
|
|
1749
|
+
maxWidth: MAX_SCREENCAST_DIMENSION,
|
|
1750
|
+
maxHeight: MAX_SCREENCAST_DIMENSION,
|
|
1751
|
+
everyNthFrame: 2
|
|
975
1752
|
});
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
socket.close(1e3, "Terminal exited");
|
|
982
|
-
}
|
|
983
|
-
session.clients.clear();
|
|
1753
|
+
s.isScreencasting = true;
|
|
1754
|
+
}
|
|
1755
|
+
async function stopScreencast(s) {
|
|
1756
|
+
if (!s.isScreencasting) return;
|
|
1757
|
+
await s.cdpSession.send("Page.stopScreencast").catch(() => {
|
|
984
1758
|
});
|
|
985
|
-
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
const
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
pendingMessages.push(data);
|
|
994
|
-
return;
|
|
995
|
-
}
|
|
996
|
-
try {
|
|
997
|
-
const message = JSON.parse(data);
|
|
998
|
-
if (message.type === "bridge-start") {
|
|
999
|
-
if (!started) {
|
|
1000
|
-
started = true;
|
|
1001
|
-
sendJson(socket, { type: "status", message: "connected" });
|
|
1002
|
-
for (const chunk of session.buffer) socket.send(chunk);
|
|
1003
|
-
}
|
|
1004
|
-
return;
|
|
1005
|
-
}
|
|
1006
|
-
if (message.type === "input" && typeof message.data === "string") {
|
|
1007
|
-
session.pty.write(message.data);
|
|
1008
|
-
return;
|
|
1009
|
-
}
|
|
1010
|
-
if (message.type === "resize") {
|
|
1011
|
-
const cols = parseDimension(message.cols, session.cols, 10, 500);
|
|
1012
|
-
const rows = parseDimension(message.rows, session.rows, 5, 200);
|
|
1013
|
-
if (cols !== session.cols || rows !== session.rows) {
|
|
1014
|
-
session.cols = cols;
|
|
1015
|
-
session.rows = rows;
|
|
1016
|
-
session.pty.resize(cols, rows);
|
|
1017
|
-
}
|
|
1018
|
-
return;
|
|
1019
|
-
}
|
|
1020
|
-
} catch {
|
|
1021
|
-
}
|
|
1022
|
-
session.pty.write(data);
|
|
1023
|
-
};
|
|
1024
|
-
socket.on("message", (raw) => handleMessage(raw.toString()));
|
|
1025
|
-
session = sessions.get(grant.sessionKey);
|
|
1026
|
-
if (!session) session = await createTerminalSession(grant, nodePtyLoader);
|
|
1027
|
-
else clearIdleTimer(session);
|
|
1028
|
-
session.clients.add(socket);
|
|
1029
|
-
for (const data of pendingMessages.splice(0)) handleMessage(data);
|
|
1030
|
-
const detach = () => {
|
|
1031
|
-
if (!session) return;
|
|
1032
|
-
session.clients.delete(socket);
|
|
1033
|
-
if (session.clients.size === 0) scheduleIdleCleanup(grant.sessionKey, session);
|
|
1034
|
-
};
|
|
1035
|
-
socket.once("close", detach);
|
|
1036
|
-
socket.once("error", detach);
|
|
1037
|
-
};
|
|
1038
|
-
var installConnectorTerminalEndpoint = (server, options = {}) => {
|
|
1039
|
-
if (installedServers.has(server)) return;
|
|
1040
|
-
installedServers.add(server);
|
|
1041
|
-
const terminalServer = new WebSocketServer({ noServer: true });
|
|
1042
|
-
server.on("upgrade", (request, socket, head) => {
|
|
1043
|
-
const url = new URL(request.url ?? "/", "http://connector.local");
|
|
1044
|
-
if (url.pathname !== "/api/terminal") return;
|
|
1045
|
-
const tokenHeader = request.headers["x-buda-local-terminal-token"];
|
|
1046
|
-
const token = Array.isArray(tokenHeader) ? tokenHeader[0] : tokenHeader;
|
|
1047
|
-
const grant = token ? localGrants.get(token) : void 0;
|
|
1048
|
-
if (!token || !grant || grant.expiresAt <= Date.now()) {
|
|
1049
|
-
if (token) localGrants.delete(token);
|
|
1050
|
-
socket.destroy();
|
|
1051
|
-
return;
|
|
1052
|
-
}
|
|
1053
|
-
localGrants.delete(token);
|
|
1054
|
-
terminalServer.handleUpgrade(request, socket, head, (client) => {
|
|
1055
|
-
void attachLocalTerminal(
|
|
1056
|
-
client,
|
|
1057
|
-
grant,
|
|
1058
|
-
options.nodePtyLoader ?? (() => import("./lib-UX6C35VI.js"))
|
|
1059
|
-
).catch((error) => {
|
|
1060
|
-
const message = error instanceof LocalTerminalUnavailableError ? error.message : "Local terminal is unavailable. The connector remains available for other capabilities.";
|
|
1061
|
-
sendJson(client, { type: "error", fatal: true, message });
|
|
1062
|
-
client.close(LOCAL_TERMINAL_CLOSE_SPAWN_FAILED, "Local terminal unavailable");
|
|
1063
|
-
});
|
|
1064
|
-
});
|
|
1759
|
+
s.isScreencasting = false;
|
|
1760
|
+
}
|
|
1761
|
+
async function closeSession(sessionId) {
|
|
1762
|
+
const activeSession = sessions.get(sessionId) ?? await sessionPromises.get(sessionId)?.catch(() => null);
|
|
1763
|
+
if (!activeSession) return;
|
|
1764
|
+
if (sessions.get(sessionId) === activeSession) sessions.delete(sessionId);
|
|
1765
|
+
await stopScreencast(activeSession);
|
|
1766
|
+
await activeSession.cdpSession.detach().catch(() => {
|
|
1065
1767
|
});
|
|
1066
|
-
|
|
1067
|
-
for (const client of terminalServer.clients) client.terminate();
|
|
1068
|
-
terminalServer.close();
|
|
1069
|
-
localGrants.clear();
|
|
1070
|
-
for (const session of sessions.values()) {
|
|
1071
|
-
clearIdleTimer(session);
|
|
1072
|
-
try {
|
|
1073
|
-
session.pty.kill();
|
|
1074
|
-
} catch {
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
sessions.clear();
|
|
1768
|
+
await activeSession.browser.close().catch(() => {
|
|
1078
1769
|
});
|
|
1079
|
-
};
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1770
|
+
await writeLog("info", `browser session stopped ${sessionId}`);
|
|
1771
|
+
}
|
|
1772
|
+
async function closeAllBrowserSessions() {
|
|
1773
|
+
const sessionIds = /* @__PURE__ */ new Set([...sessions.keys(), ...sessionPromises.keys()]);
|
|
1774
|
+
await Promise.all([...sessionIds].map((sessionId) => closeSession(sessionId)));
|
|
1775
|
+
}
|
|
1776
|
+
async function resizeBrowser(s, viewport) {
|
|
1777
|
+
const resize = s.resizeQueue.catch(() => {
|
|
1778
|
+
}).then(async () => {
|
|
1779
|
+
const current = s.page.viewportSize();
|
|
1780
|
+
if (current?.width === viewport.width && current.height === viewport.height) return;
|
|
1781
|
+
const wasScreencasting = s.isScreencasting;
|
|
1782
|
+
if (wasScreencasting) await stopScreencast(s);
|
|
1783
|
+
await s.page.setViewportSize(viewport);
|
|
1784
|
+
if (wasScreencasting) await startScreencast(s);
|
|
1785
|
+
await writeLog("info", `browser resized to ${viewport.width}x${viewport.height}`);
|
|
1092
1786
|
});
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1787
|
+
s.resizeQueue = resize;
|
|
1788
|
+
await resize;
|
|
1789
|
+
}
|
|
1790
|
+
async function navigateBrowser(s, url, options) {
|
|
1791
|
+
const navigation = s.navigationQueue.catch(() => {
|
|
1792
|
+
}).then(async () => {
|
|
1793
|
+
if (s.page.url() === url) return null;
|
|
1794
|
+
return s.page.goto(url, options);
|
|
1795
|
+
});
|
|
1796
|
+
s.navigationQueue = navigation.then(() => {
|
|
1797
|
+
});
|
|
1798
|
+
return navigation;
|
|
1799
|
+
}
|
|
1800
|
+
var normalizeViewportDimension = (value, min, max, fallback) => {
|
|
1801
|
+
const dimension = typeof value === "number" && Number.isFinite(value) ? Math.round(value) : fallback;
|
|
1802
|
+
return Math.min(max, Math.max(min, dimension));
|
|
1101
1803
|
};
|
|
1102
|
-
var
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
const onError = (error) => {
|
|
1108
|
-
socket.removeListener("open", onOpen);
|
|
1109
|
-
reject(error);
|
|
1110
|
-
};
|
|
1111
|
-
socket.once("open", onOpen);
|
|
1112
|
-
socket.once("error", onError);
|
|
1113
|
-
});
|
|
1114
|
-
var proxyConnectorTerminalTunnel = async (input) => {
|
|
1115
|
-
const cloud = new WebSocket(
|
|
1116
|
-
getConnectorTerminalUrl(input.serverUrl, input.connectorId, input.sessionId),
|
|
1117
|
-
{
|
|
1118
|
-
headers: { "X-Buda-Terminal-Token": input.cloudToken },
|
|
1119
|
-
handshakeTimeout: TERMINAL_HANDSHAKE_TIMEOUT_MS
|
|
1120
|
-
}
|
|
1121
|
-
);
|
|
1122
|
-
const local = new WebSocket(toLocalTerminalUrl(input.localBaseUrl), {
|
|
1123
|
-
headers: { "X-Buda-Local-Terminal-Token": input.localToken },
|
|
1124
|
-
handshakeTimeout: TERMINAL_HANDSHAKE_TIMEOUT_MS
|
|
1804
|
+
var normalizeTimeout = (value, fallback, max) => typeof value === "number" && Number.isFinite(value) ? Math.min(max, Math.max(1e3, Math.round(value))) : fallback;
|
|
1805
|
+
function jsonResponse(status, body) {
|
|
1806
|
+
return new Response(JSON.stringify(body), {
|
|
1807
|
+
status,
|
|
1808
|
+
headers: { "content-type": "application/json" }
|
|
1125
1809
|
});
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
return false;
|
|
1143
|
-
}
|
|
1144
|
-
pendingBytes += bytes;
|
|
1145
|
-
frames.push([data, isBinary]);
|
|
1146
|
-
return true;
|
|
1147
|
-
};
|
|
1148
|
-
cloud.on("message", (data, isBinary) => {
|
|
1149
|
-
if (!bridgeAccepted) {
|
|
1150
|
-
if (!isBinary && data.toString() === '{"type":"bridge-accepted"}') {
|
|
1151
|
-
bridgeAccepted = true;
|
|
1152
|
-
local.send(JSON.stringify({ type: "bridge-start" }));
|
|
1153
|
-
for (const [frame, binary] of pendingCloudFrames) {
|
|
1154
|
-
if (local.readyState === WebSocket.OPEN) local.send(frame, { binary });
|
|
1155
|
-
}
|
|
1156
|
-
for (const [frame, binary] of pendingLocalFrames) {
|
|
1157
|
-
if (cloud.readyState === WebSocket.OPEN) cloud.send(frame, { binary });
|
|
1158
|
-
}
|
|
1159
|
-
pendingCloudFrames.length = 0;
|
|
1160
|
-
pendingLocalFrames.length = 0;
|
|
1161
|
-
pendingBytes = 0;
|
|
1810
|
+
}
|
|
1811
|
+
var getRequestSessionId = (context) => normalizeSessionId(context.sessionId);
|
|
1812
|
+
async function handleBrowserInput(sessionIdValue, body) {
|
|
1813
|
+
const sessionId = normalizeSessionId(sessionIdValue);
|
|
1814
|
+
if (!sessionId) throw new Error("A valid browser session id is required.");
|
|
1815
|
+
const ev = body ?? {};
|
|
1816
|
+
const s = await ensureSession(sessionId);
|
|
1817
|
+
const input = s.inputQueue.catch(() => {
|
|
1818
|
+
}).then(async () => {
|
|
1819
|
+
switch (ev.type) {
|
|
1820
|
+
case "mouseMove":
|
|
1821
|
+
await s.cdpSession.send("Input.dispatchMouseEvent", {
|
|
1822
|
+
type: "mouseMoved",
|
|
1823
|
+
x: ev.x,
|
|
1824
|
+
y: ev.y
|
|
1825
|
+
});
|
|
1162
1826
|
return;
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1827
|
+
case "mouseDown":
|
|
1828
|
+
await s.cdpSession.send("Input.dispatchMouseEvent", {
|
|
1829
|
+
type: "mousePressed",
|
|
1830
|
+
x: ev.x,
|
|
1831
|
+
y: ev.y,
|
|
1832
|
+
button: "left",
|
|
1833
|
+
clickCount: 1
|
|
1834
|
+
});
|
|
1835
|
+
return;
|
|
1836
|
+
case "mouseUp":
|
|
1837
|
+
await s.cdpSession.send("Input.dispatchMouseEvent", {
|
|
1838
|
+
type: "mouseReleased",
|
|
1839
|
+
x: ev.x,
|
|
1840
|
+
y: ev.y,
|
|
1841
|
+
button: "left",
|
|
1842
|
+
clickCount: 1
|
|
1843
|
+
});
|
|
1844
|
+
return;
|
|
1845
|
+
case "keyDown":
|
|
1846
|
+
await s.cdpSession.send("Input.dispatchKeyEvent", {
|
|
1847
|
+
type: "keyDown",
|
|
1848
|
+
key: ev.key,
|
|
1849
|
+
text: ev.text ?? ""
|
|
1850
|
+
});
|
|
1851
|
+
return;
|
|
1852
|
+
case "keyUp":
|
|
1853
|
+
await s.cdpSession.send("Input.dispatchKeyEvent", {
|
|
1854
|
+
type: "keyUp",
|
|
1855
|
+
key: ev.key
|
|
1856
|
+
});
|
|
1857
|
+
return;
|
|
1858
|
+
case "scroll":
|
|
1859
|
+
await s.cdpSession.send("Input.dispatchMouseEvent", {
|
|
1860
|
+
type: "mouseWheel",
|
|
1861
|
+
x: ev.x,
|
|
1862
|
+
y: ev.y,
|
|
1863
|
+
deltaX: 0,
|
|
1864
|
+
deltaY: ev.deltaY
|
|
1865
|
+
});
|
|
1866
|
+
return;
|
|
1867
|
+
default:
|
|
1868
|
+
throw new Error(`Unknown browser input event: ${String(ev.type)}`);
|
|
1166
1869
|
}
|
|
1167
|
-
if (local.readyState === WebSocket.OPEN) local.send(data, { binary: isBinary });
|
|
1168
1870
|
});
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
bufferFrame(pendingLocalFrames, data, isBinary);
|
|
1172
|
-
return;
|
|
1173
|
-
}
|
|
1174
|
-
if (cloud.readyState === WebSocket.OPEN) cloud.send(data, { binary: isBinary });
|
|
1175
|
-
});
|
|
1176
|
-
cloud.once("close", (code, reason) => {
|
|
1177
|
-
if (!bridgeAccepted) {
|
|
1178
|
-
void writeLog(
|
|
1179
|
-
"warn",
|
|
1180
|
-
`terminal cloud tunnel closed before readiness session=${input.sessionId} code=${code} reason=${truncateTerminalCloseReason(reason.toString())}`
|
|
1181
|
-
);
|
|
1182
|
-
}
|
|
1183
|
-
if (local.readyState === WebSocket.OPEN) {
|
|
1184
|
-
local.close(normalizeTerminalCloseCode(code), truncateTerminalCloseReason(reason.toString()));
|
|
1185
|
-
}
|
|
1186
|
-
});
|
|
1187
|
-
local.once("close", (code, reason) => {
|
|
1188
|
-
if (cloud.readyState === WebSocket.OPEN) {
|
|
1189
|
-
cloud.close(normalizeTerminalCloseCode(code), truncateTerminalCloseReason(reason.toString()));
|
|
1190
|
-
}
|
|
1191
|
-
});
|
|
1192
|
-
cloud.once("error", () => local.terminate());
|
|
1193
|
-
local.once("error", () => cloud.terminate());
|
|
1194
|
-
cloud.send(JSON.stringify({ type: "bridge-ready" }));
|
|
1195
|
-
await writeLog(
|
|
1196
|
-
"info",
|
|
1197
|
-
`terminal proxy connected session=${input.sessionId} workspace=${input.workspaceKey}`
|
|
1198
|
-
);
|
|
1199
|
-
};
|
|
1200
|
-
|
|
1201
|
-
// src/bunny-daemon.ts
|
|
1202
|
-
async function startEmbeddedBunnyDaemon(config, launchConfig) {
|
|
1203
|
-
const host = getBunnyDaemonHost(launchConfig);
|
|
1204
|
-
const port = getBunnyDaemonPort(launchConfig);
|
|
1205
|
-
const root = config.workdirRoot;
|
|
1206
|
-
await mkdir3(root, { recursive: true });
|
|
1207
|
-
const server = createDaemon({ host, port, root });
|
|
1208
|
-
installConnectorTerminalEndpoint(server);
|
|
1209
|
-
await new Promise((resolve, reject) => {
|
|
1210
|
-
const onError = (error) => {
|
|
1211
|
-
server.off("listening", onListening);
|
|
1212
|
-
reject(error);
|
|
1213
|
-
};
|
|
1214
|
-
const onListening = () => {
|
|
1215
|
-
server.off("error", onError);
|
|
1216
|
-
resolve();
|
|
1217
|
-
};
|
|
1218
|
-
server.once("error", onError);
|
|
1219
|
-
server.once("listening", onListening);
|
|
1220
|
-
server.listen(port, host);
|
|
1221
|
-
});
|
|
1222
|
-
const address = server.address();
|
|
1223
|
-
const actualPort = typeof address === "object" && address ? address.port : port;
|
|
1224
|
-
const baseUrl = `http://${host}:${actualPort}`;
|
|
1225
|
-
await writeLog("info", `bunny daemon listening ${baseUrl}`, {
|
|
1226
|
-
root
|
|
1227
|
-
});
|
|
1228
|
-
return {
|
|
1229
|
-
server,
|
|
1230
|
-
host,
|
|
1231
|
-
port: actualPort,
|
|
1232
|
-
baseUrl,
|
|
1233
|
-
root
|
|
1234
|
-
};
|
|
1235
|
-
}
|
|
1236
|
-
async function stopEmbeddedBunnyDaemon(daemon) {
|
|
1237
|
-
await new Promise((resolve) => {
|
|
1238
|
-
daemon.server.close(() => resolve());
|
|
1239
|
-
});
|
|
1240
|
-
await writeLog("info", `bunny daemon stopped ${daemon.baseUrl}`);
|
|
1871
|
+
s.inputQueue = input;
|
|
1872
|
+
await input;
|
|
1241
1873
|
}
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
var sessions2 = /* @__PURE__ */ new Map();
|
|
1256
|
-
var sessionPromises = /* @__PURE__ */ new Map();
|
|
1257
|
-
var getBrowserMode = () => {
|
|
1258
|
-
const configured = process.env.BUDA_CONNECTOR_BROWSER_MODE?.trim().toLowerCase();
|
|
1259
|
-
return configured === "auto" || configured === "headed" || configured === "headless" ? configured : "headless";
|
|
1260
|
-
};
|
|
1261
|
-
var getSystemBrowserCandidates = (platform4, homeDirectory, env) => {
|
|
1262
|
-
if (platform4 === "darwin") {
|
|
1263
|
-
return [
|
|
1264
|
-
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
1265
|
-
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
1266
|
-
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
1267
|
-
`${homeDirectory}/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`,
|
|
1268
|
-
`${homeDirectory}/Applications/Chromium.app/Contents/MacOS/Chromium`,
|
|
1269
|
-
`${homeDirectory}/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge`
|
|
1270
|
-
];
|
|
1271
|
-
}
|
|
1272
|
-
if (platform4 === "win32") {
|
|
1273
|
-
const localAppData = env.LOCALAPPDATA;
|
|
1274
|
-
const programFiles = env.PROGRAMFILES;
|
|
1275
|
-
const programFilesX86 = env["PROGRAMFILES(X86)"];
|
|
1276
|
-
return [
|
|
1277
|
-
localAppData ? `${localAppData}\\Google\\Chrome\\Application\\chrome.exe` : "",
|
|
1278
|
-
localAppData ? `${localAppData}\\Chromium\\Application\\chrome.exe` : "",
|
|
1279
|
-
programFiles ? `${programFiles}\\Google\\Chrome\\Application\\chrome.exe` : "",
|
|
1280
|
-
programFilesX86 ? `${programFilesX86}\\Google\\Chrome\\Application\\chrome.exe` : "",
|
|
1281
|
-
programFiles ? `${programFiles}\\Microsoft\\Edge\\Application\\msedge.exe` : "",
|
|
1282
|
-
programFilesX86 ? `${programFilesX86}\\Microsoft\\Edge\\Application\\msedge.exe` : ""
|
|
1283
|
-
];
|
|
1874
|
+
var getPageResult = async (s) => ({
|
|
1875
|
+
url: s.page.url(),
|
|
1876
|
+
title: await s.page.title().catch(() => "")
|
|
1877
|
+
});
|
|
1878
|
+
var resolveScreenshotPath = (workdirRoot, filePath) => {
|
|
1879
|
+
if (!workdirRoot) throw new Error("Connector workdir is unavailable for screenshots.");
|
|
1880
|
+
const raw = typeof filePath === "string" ? filePath.trim() : "";
|
|
1881
|
+
if (!raw) throw new Error("file_path is required");
|
|
1882
|
+
const agentRoot = path4.resolve(workdirRoot, "agent");
|
|
1883
|
+
const relativeInput = raw === "/agent" ? "" : raw.startsWith("/agent/") ? raw.slice(7) : raw;
|
|
1884
|
+
const absolutePath = path4.resolve(agentRoot, relativeInput);
|
|
1885
|
+
if (absolutePath === agentRoot || !absolutePath.startsWith(`${agentRoot}${path4.sep}`)) {
|
|
1886
|
+
throw new Error("file_path must stay under the connector agent drive");
|
|
1284
1887
|
}
|
|
1285
|
-
return
|
|
1286
|
-
|
|
1287
|
-
"/
|
|
1288
|
-
|
|
1289
|
-
"/usr/bin/chromium-browser",
|
|
1290
|
-
"/snap/bin/chromium",
|
|
1291
|
-
"/usr/bin/microsoft-edge",
|
|
1292
|
-
"/usr/bin/microsoft-edge-stable"
|
|
1293
|
-
];
|
|
1888
|
+
return {
|
|
1889
|
+
absolutePath,
|
|
1890
|
+
relativePath: path4.relative(agentRoot, absolutePath).split(path4.sep).join("/")
|
|
1891
|
+
};
|
|
1294
1892
|
};
|
|
1295
|
-
|
|
1296
|
-
const
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
const configuredPath = env.BUDA_CONNECTOR_BROWSER_PATH?.trim();
|
|
1309
|
-
if (configuredPath) {
|
|
1310
|
-
if (pathIsExecutable(configuredPath)) {
|
|
1311
|
-
return { available: true, executablePath: configuredPath };
|
|
1312
|
-
}
|
|
1313
|
-
return {
|
|
1314
|
-
available: false,
|
|
1315
|
-
error: `BUDA_CONNECTOR_BROWSER_PATH is missing or not executable: ${configuredPath}. Set it to a Chrome, Chromium, or Edge executable.`
|
|
1893
|
+
async function handleBrowserRequest(requestPath, method, body, context = {}) {
|
|
1894
|
+
const route = new URL(requestPath, "http://connector.local").pathname.replace(
|
|
1895
|
+
/^\/browser\/?/,
|
|
1896
|
+
""
|
|
1897
|
+
);
|
|
1898
|
+
const sessionId = getRequestSessionId(context);
|
|
1899
|
+
if (!sessionId)
|
|
1900
|
+
return jsonResponse(400, { ok: false, error: "valid browser session id required" });
|
|
1901
|
+
if (route === "start" && method === "POST") {
|
|
1902
|
+
const input = body ?? {};
|
|
1903
|
+
const viewport = {
|
|
1904
|
+
width: normalizeViewportDimension(input.width, 320, 2560, DEFAULT_VIEWPORT.width),
|
|
1905
|
+
height: normalizeViewportDimension(input.height, 240, 2560, DEFAULT_VIEWPORT.height)
|
|
1316
1906
|
};
|
|
1907
|
+
const s = await ensureSession(sessionId, viewport);
|
|
1908
|
+
const clientId = normalizeSessionId(input.clientId);
|
|
1909
|
+
if (clientId) s.clientIds.add(clientId);
|
|
1910
|
+
await resizeBrowser(s, viewport);
|
|
1911
|
+
const url = input.url?.trim() || "about:blank";
|
|
1912
|
+
await writeLog("info", `browser viewport ${viewport.width}x${viewport.height}`);
|
|
1913
|
+
await navigateBrowser(s, url);
|
|
1914
|
+
await writeLog("info", `browser ready at ${s.page.url()}`);
|
|
1915
|
+
return jsonResponse(200, { ok: true, ...await getPageResult(s) });
|
|
1317
1916
|
}
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
if (executablePath) {
|
|
1322
|
-
return { available: true, executablePath };
|
|
1917
|
+
if (route === "stop" && method === "POST") {
|
|
1918
|
+
await closeSession(sessionId);
|
|
1919
|
+
return jsonResponse(200, { ok: true });
|
|
1323
1920
|
}
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
const executablePath = availability.executablePath;
|
|
1335
|
-
let browser = null;
|
|
1336
|
-
if (mode !== "headless") {
|
|
1337
|
-
try {
|
|
1338
|
-
await writeLog("info", `launching headed system browser ${executablePath}`);
|
|
1339
|
-
browser = await chromium.launch({
|
|
1340
|
-
executablePath,
|
|
1341
|
-
headless: false,
|
|
1342
|
-
args: ["--no-sandbox", "--disable-setuid-sandbox"]
|
|
1343
|
-
});
|
|
1344
|
-
} catch (error) {
|
|
1345
|
-
if (mode === "headed") throw error;
|
|
1346
|
-
await writeLog(
|
|
1347
|
-
"warn",
|
|
1348
|
-
`headed browser launch failed, falling back to headless: ${error instanceof Error ? error.message : String(error)}`
|
|
1349
|
-
);
|
|
1350
|
-
}
|
|
1921
|
+
if (route === "release" && method === "POST") {
|
|
1922
|
+
const input = body ?? {};
|
|
1923
|
+
const clientId = normalizeSessionId(input.clientId);
|
|
1924
|
+
if (!clientId) return jsonResponse(400, { ok: false, error: "valid client id required" });
|
|
1925
|
+
const s = sessions.get(sessionId);
|
|
1926
|
+
if (!s) return jsonResponse(200, { ok: true, closed: false });
|
|
1927
|
+
s.clientIds.delete(clientId);
|
|
1928
|
+
if (s.clientIds.size > 0) return jsonResponse(200, { ok: true, closed: false });
|
|
1929
|
+
await closeSession(sessionId);
|
|
1930
|
+
return jsonResponse(200, { ok: true, closed: true });
|
|
1351
1931
|
}
|
|
1352
|
-
if (
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
if (typeof value !== "string") return null;
|
|
1367
|
-
const normalized = value.trim();
|
|
1368
|
-
return SESSION_ID_PATTERN.test(normalized) ? normalized : null;
|
|
1369
|
-
};
|
|
1370
|
-
async function ensureSession(sessionId, initialViewport = DEFAULT_VIEWPORT) {
|
|
1371
|
-
const activeSession = sessions2.get(sessionId);
|
|
1372
|
-
if (activeSession) return activeSession;
|
|
1373
|
-
let sessionPromise = sessionPromises.get(sessionId);
|
|
1374
|
-
if (!sessionPromise) {
|
|
1375
|
-
sessionPromise = (async () => {
|
|
1376
|
-
await writeLog("info", `launching local browser session ${sessionId}`);
|
|
1377
|
-
const { browser, page, cdpSession } = await launchPlaywright(initialViewport);
|
|
1378
|
-
const nextSession = {
|
|
1379
|
-
page,
|
|
1380
|
-
browser,
|
|
1381
|
-
cdpSession,
|
|
1382
|
-
screencastClients: /* @__PURE__ */ new Set(),
|
|
1383
|
-
isScreencasting: false,
|
|
1384
|
-
screencastListenerAttached: false,
|
|
1385
|
-
resizeQueue: Promise.resolve(),
|
|
1386
|
-
inputQueue: Promise.resolve(),
|
|
1387
|
-
navigationQueue: Promise.resolve(),
|
|
1388
|
-
clientIds: /* @__PURE__ */ new Set()
|
|
1389
|
-
};
|
|
1390
|
-
sessions2.set(sessionId, nextSession);
|
|
1391
|
-
browser.on("disconnected", () => {
|
|
1392
|
-
if (sessions2.get(sessionId) === nextSession) sessions2.delete(sessionId);
|
|
1393
|
-
});
|
|
1394
|
-
return nextSession;
|
|
1395
|
-
})();
|
|
1396
|
-
sessionPromises.set(sessionId, sessionPromise);
|
|
1397
|
-
}
|
|
1398
|
-
try {
|
|
1399
|
-
return await sessionPromise;
|
|
1400
|
-
} finally {
|
|
1401
|
-
if (sessionPromises.get(sessionId) === sessionPromise) sessionPromises.delete(sessionId);
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
async function startScreencast(s) {
|
|
1405
|
-
if (s.isScreencasting) return;
|
|
1406
|
-
if (!s.screencastListenerAttached) {
|
|
1407
|
-
s.screencastListenerAttached = true;
|
|
1408
|
-
s.cdpSession.on("Page.screencastFrame", (params) => {
|
|
1409
|
-
const metadata = params.metadata;
|
|
1410
|
-
const viewport = s.page.viewportSize() ?? DEFAULT_VIEWPORT;
|
|
1411
|
-
const frame = {
|
|
1412
|
-
data: params.data,
|
|
1413
|
-
viewportWidth: metadata.deviceWidth ?? viewport.width,
|
|
1414
|
-
viewportHeight: metadata.deviceHeight ?? viewport.height
|
|
1415
|
-
};
|
|
1416
|
-
for (const listener of s.screencastClients) listener(frame);
|
|
1417
|
-
void s.cdpSession.send("Page.screencastFrameAck", { sessionId: params.sessionId }).catch(() => {
|
|
1418
|
-
});
|
|
1419
|
-
});
|
|
1420
|
-
}
|
|
1421
|
-
await s.cdpSession.send("Page.startScreencast", {
|
|
1422
|
-
format: "jpeg",
|
|
1423
|
-
quality: 90,
|
|
1424
|
-
maxWidth: MAX_SCREENCAST_DIMENSION,
|
|
1425
|
-
maxHeight: MAX_SCREENCAST_DIMENSION,
|
|
1426
|
-
everyNthFrame: 2
|
|
1427
|
-
});
|
|
1428
|
-
s.isScreencasting = true;
|
|
1429
|
-
}
|
|
1430
|
-
async function stopScreencast(s) {
|
|
1431
|
-
if (!s.isScreencasting) return;
|
|
1432
|
-
await s.cdpSession.send("Page.stopScreencast").catch(() => {
|
|
1433
|
-
});
|
|
1434
|
-
s.isScreencasting = false;
|
|
1435
|
-
}
|
|
1436
|
-
async function closeSession(sessionId) {
|
|
1437
|
-
const activeSession = sessions2.get(sessionId) ?? await sessionPromises.get(sessionId)?.catch(() => null);
|
|
1438
|
-
if (!activeSession) return;
|
|
1439
|
-
if (sessions2.get(sessionId) === activeSession) sessions2.delete(sessionId);
|
|
1440
|
-
await stopScreencast(activeSession);
|
|
1441
|
-
await activeSession.cdpSession.detach().catch(() => {
|
|
1442
|
-
});
|
|
1443
|
-
await activeSession.browser.close().catch(() => {
|
|
1444
|
-
});
|
|
1445
|
-
await writeLog("info", `browser session stopped ${sessionId}`);
|
|
1446
|
-
}
|
|
1447
|
-
async function closeAllBrowserSessions() {
|
|
1448
|
-
const sessionIds = /* @__PURE__ */ new Set([...sessions2.keys(), ...sessionPromises.keys()]);
|
|
1449
|
-
await Promise.all([...sessionIds].map((sessionId) => closeSession(sessionId)));
|
|
1450
|
-
}
|
|
1451
|
-
async function resizeBrowser(s, viewport) {
|
|
1452
|
-
const resize = s.resizeQueue.catch(() => {
|
|
1453
|
-
}).then(async () => {
|
|
1454
|
-
const current = s.page.viewportSize();
|
|
1455
|
-
if (current?.width === viewport.width && current.height === viewport.height) return;
|
|
1456
|
-
const wasScreencasting = s.isScreencasting;
|
|
1457
|
-
if (wasScreencasting) await stopScreencast(s);
|
|
1458
|
-
await s.page.setViewportSize(viewport);
|
|
1459
|
-
if (wasScreencasting) await startScreencast(s);
|
|
1460
|
-
await writeLog("info", `browser resized to ${viewport.width}x${viewport.height}`);
|
|
1461
|
-
});
|
|
1462
|
-
s.resizeQueue = resize;
|
|
1463
|
-
await resize;
|
|
1464
|
-
}
|
|
1465
|
-
async function navigateBrowser(s, url, options) {
|
|
1466
|
-
const navigation = s.navigationQueue.catch(() => {
|
|
1467
|
-
}).then(async () => {
|
|
1468
|
-
if (s.page.url() === url) return null;
|
|
1469
|
-
return s.page.goto(url, options);
|
|
1470
|
-
});
|
|
1471
|
-
s.navigationQueue = navigation.then(() => {
|
|
1472
|
-
});
|
|
1473
|
-
return navigation;
|
|
1474
|
-
}
|
|
1475
|
-
var normalizeViewportDimension = (value, min, max, fallback) => {
|
|
1476
|
-
const dimension = typeof value === "number" && Number.isFinite(value) ? Math.round(value) : fallback;
|
|
1477
|
-
return Math.min(max, Math.max(min, dimension));
|
|
1478
|
-
};
|
|
1479
|
-
var normalizeTimeout = (value, fallback, max) => typeof value === "number" && Number.isFinite(value) ? Math.min(max, Math.max(1e3, Math.round(value))) : fallback;
|
|
1480
|
-
function jsonResponse(status, body) {
|
|
1481
|
-
return new Response(JSON.stringify(body), {
|
|
1482
|
-
status,
|
|
1483
|
-
headers: { "content-type": "application/json" }
|
|
1484
|
-
});
|
|
1485
|
-
}
|
|
1486
|
-
var getRequestSessionId = (context) => normalizeSessionId(context.sessionId);
|
|
1487
|
-
async function handleBrowserInput(sessionIdValue, body) {
|
|
1488
|
-
const sessionId = normalizeSessionId(sessionIdValue);
|
|
1489
|
-
if (!sessionId) throw new Error("A valid browser session id is required.");
|
|
1490
|
-
const ev = body ?? {};
|
|
1491
|
-
const s = await ensureSession(sessionId);
|
|
1492
|
-
const input = s.inputQueue.catch(() => {
|
|
1493
|
-
}).then(async () => {
|
|
1494
|
-
switch (ev.type) {
|
|
1495
|
-
case "mouseMove":
|
|
1496
|
-
await s.cdpSession.send("Input.dispatchMouseEvent", {
|
|
1497
|
-
type: "mouseMoved",
|
|
1498
|
-
x: ev.x,
|
|
1499
|
-
y: ev.y
|
|
1500
|
-
});
|
|
1501
|
-
return;
|
|
1502
|
-
case "mouseDown":
|
|
1503
|
-
await s.cdpSession.send("Input.dispatchMouseEvent", {
|
|
1504
|
-
type: "mousePressed",
|
|
1505
|
-
x: ev.x,
|
|
1506
|
-
y: ev.y,
|
|
1507
|
-
button: "left",
|
|
1508
|
-
clickCount: 1
|
|
1509
|
-
});
|
|
1510
|
-
return;
|
|
1511
|
-
case "mouseUp":
|
|
1512
|
-
await s.cdpSession.send("Input.dispatchMouseEvent", {
|
|
1513
|
-
type: "mouseReleased",
|
|
1514
|
-
x: ev.x,
|
|
1515
|
-
y: ev.y,
|
|
1516
|
-
button: "left",
|
|
1517
|
-
clickCount: 1
|
|
1518
|
-
});
|
|
1519
|
-
return;
|
|
1520
|
-
case "keyDown":
|
|
1521
|
-
await s.cdpSession.send("Input.dispatchKeyEvent", {
|
|
1522
|
-
type: "keyDown",
|
|
1523
|
-
key: ev.key,
|
|
1524
|
-
text: ev.text ?? ""
|
|
1525
|
-
});
|
|
1526
|
-
return;
|
|
1527
|
-
case "keyUp":
|
|
1528
|
-
await s.cdpSession.send("Input.dispatchKeyEvent", {
|
|
1529
|
-
type: "keyUp",
|
|
1530
|
-
key: ev.key
|
|
1531
|
-
});
|
|
1532
|
-
return;
|
|
1533
|
-
case "scroll":
|
|
1534
|
-
await s.cdpSession.send("Input.dispatchMouseEvent", {
|
|
1535
|
-
type: "mouseWheel",
|
|
1536
|
-
x: ev.x,
|
|
1537
|
-
y: ev.y,
|
|
1538
|
-
deltaX: 0,
|
|
1539
|
-
deltaY: ev.deltaY
|
|
1540
|
-
});
|
|
1541
|
-
return;
|
|
1542
|
-
default:
|
|
1543
|
-
throw new Error(`Unknown browser input event: ${String(ev.type)}`);
|
|
1544
|
-
}
|
|
1545
|
-
});
|
|
1546
|
-
s.inputQueue = input;
|
|
1547
|
-
await input;
|
|
1548
|
-
}
|
|
1549
|
-
var getPageResult = async (s) => ({
|
|
1550
|
-
url: s.page.url(),
|
|
1551
|
-
title: await s.page.title().catch(() => "")
|
|
1552
|
-
});
|
|
1553
|
-
var resolveScreenshotPath = (workdirRoot, filePath) => {
|
|
1554
|
-
if (!workdirRoot) throw new Error("Connector workdir is unavailable for screenshots.");
|
|
1555
|
-
const raw = typeof filePath === "string" ? filePath.trim() : "";
|
|
1556
|
-
if (!raw) throw new Error("file_path is required");
|
|
1557
|
-
const agentRoot = path4.resolve(workdirRoot, "agent");
|
|
1558
|
-
const relativeInput = raw === "/agent" ? "" : raw.startsWith("/agent/") ? raw.slice(7) : raw;
|
|
1559
|
-
const absolutePath = path4.resolve(agentRoot, relativeInput);
|
|
1560
|
-
if (absolutePath === agentRoot || !absolutePath.startsWith(`${agentRoot}${path4.sep}`)) {
|
|
1561
|
-
throw new Error("file_path must stay under the connector agent drive");
|
|
1562
|
-
}
|
|
1563
|
-
return {
|
|
1564
|
-
absolutePath,
|
|
1565
|
-
relativePath: path4.relative(agentRoot, absolutePath).split(path4.sep).join("/")
|
|
1566
|
-
};
|
|
1567
|
-
};
|
|
1568
|
-
async function handleBrowserRequest(requestPath, method, body, context = {}) {
|
|
1569
|
-
const route = new URL(requestPath, "http://connector.local").pathname.replace(
|
|
1570
|
-
/^\/browser\/?/,
|
|
1571
|
-
""
|
|
1572
|
-
);
|
|
1573
|
-
const sessionId = getRequestSessionId(context);
|
|
1574
|
-
if (!sessionId)
|
|
1575
|
-
return jsonResponse(400, { ok: false, error: "valid browser session id required" });
|
|
1576
|
-
if (route === "start" && method === "POST") {
|
|
1577
|
-
const input = body ?? {};
|
|
1578
|
-
const viewport = {
|
|
1579
|
-
width: normalizeViewportDimension(input.width, 320, 2560, DEFAULT_VIEWPORT.width),
|
|
1580
|
-
height: normalizeViewportDimension(input.height, 240, 2560, DEFAULT_VIEWPORT.height)
|
|
1581
|
-
};
|
|
1582
|
-
const s = await ensureSession(sessionId, viewport);
|
|
1583
|
-
const clientId = normalizeSessionId(input.clientId);
|
|
1584
|
-
if (clientId) s.clientIds.add(clientId);
|
|
1585
|
-
await resizeBrowser(s, viewport);
|
|
1586
|
-
const url = input.url?.trim() || "about:blank";
|
|
1587
|
-
await writeLog("info", `browser viewport ${viewport.width}x${viewport.height}`);
|
|
1588
|
-
await navigateBrowser(s, url);
|
|
1589
|
-
await writeLog("info", `browser ready at ${s.page.url()}`);
|
|
1590
|
-
return jsonResponse(200, { ok: true, ...await getPageResult(s) });
|
|
1591
|
-
}
|
|
1592
|
-
if (route === "stop" && method === "POST") {
|
|
1593
|
-
await closeSession(sessionId);
|
|
1594
|
-
return jsonResponse(200, { ok: true });
|
|
1595
|
-
}
|
|
1596
|
-
if (route === "release" && method === "POST") {
|
|
1597
|
-
const input = body ?? {};
|
|
1598
|
-
const clientId = normalizeSessionId(input.clientId);
|
|
1599
|
-
if (!clientId) return jsonResponse(400, { ok: false, error: "valid client id required" });
|
|
1600
|
-
const s = sessions2.get(sessionId);
|
|
1601
|
-
if (!s) return jsonResponse(200, { ok: true, closed: false });
|
|
1602
|
-
s.clientIds.delete(clientId);
|
|
1603
|
-
if (s.clientIds.size > 0) return jsonResponse(200, { ok: true, closed: false });
|
|
1604
|
-
await closeSession(sessionId);
|
|
1605
|
-
return jsonResponse(200, { ok: true, closed: true });
|
|
1606
|
-
}
|
|
1607
|
-
if (route === "navigate" && method === "POST") {
|
|
1608
|
-
const input = body ?? {};
|
|
1609
|
-
const url = input.url?.trim();
|
|
1610
|
-
if (!url) return jsonResponse(400, { ok: false, error: "url required" });
|
|
1611
|
-
const s = await ensureSession(sessionId);
|
|
1612
|
-
const response = await navigateBrowser(s, url, {
|
|
1613
|
-
waitUntil: input.waitUntil ?? "domcontentloaded",
|
|
1614
|
-
timeout: normalizeTimeout(input.timeoutMs, 3e4, 6e4)
|
|
1615
|
-
});
|
|
1616
|
-
return jsonResponse(200, {
|
|
1617
|
-
ok: true,
|
|
1618
|
-
action: "navigate",
|
|
1619
|
-
...await getPageResult(s),
|
|
1620
|
-
status: response?.status() ?? null
|
|
1932
|
+
if (route === "navigate" && method === "POST") {
|
|
1933
|
+
const input = body ?? {};
|
|
1934
|
+
const url = input.url?.trim();
|
|
1935
|
+
if (!url) return jsonResponse(400, { ok: false, error: "url required" });
|
|
1936
|
+
const s = await ensureSession(sessionId);
|
|
1937
|
+
const response = await navigateBrowser(s, url, {
|
|
1938
|
+
waitUntil: input.waitUntil ?? "domcontentloaded",
|
|
1939
|
+
timeout: normalizeTimeout(input.timeoutMs, 3e4, 6e4)
|
|
1940
|
+
});
|
|
1941
|
+
return jsonResponse(200, {
|
|
1942
|
+
ok: true,
|
|
1943
|
+
action: "navigate",
|
|
1944
|
+
...await getPageResult(s),
|
|
1945
|
+
status: response?.status() ?? null
|
|
1621
1946
|
});
|
|
1622
1947
|
}
|
|
1623
1948
|
if (route === "click" && method === "POST") {
|
|
@@ -1706,7 +2031,7 @@ async function handleBrowserRequest(requestPath, method, body, context = {}) {
|
|
|
1706
2031
|
return jsonResponse(200, { ok: true });
|
|
1707
2032
|
}
|
|
1708
2033
|
if (route === "location" && method === "GET") {
|
|
1709
|
-
const s =
|
|
2034
|
+
const s = sessions.get(sessionId);
|
|
1710
2035
|
if (!s) return jsonResponse(200, { ok: true, url: "", title: "" });
|
|
1711
2036
|
return jsonResponse(200, { ok: true, ...await getPageResult(s) });
|
|
1712
2037
|
}
|
|
@@ -1737,7 +2062,7 @@ async function handleBrowserRequest(requestPath, method, body, context = {}) {
|
|
|
1737
2062
|
if (s.screencastClients.size === 0) void stopScreencast(s);
|
|
1738
2063
|
};
|
|
1739
2064
|
checkInterval = setInterval(() => {
|
|
1740
|
-
if (
|
|
2065
|
+
if (sessions.get(sessionId) !== s) {
|
|
1741
2066
|
cleanup();
|
|
1742
2067
|
controller.close();
|
|
1743
2068
|
}
|
|
@@ -1761,8 +2086,16 @@ async function handleBrowserRequest(requestPath, method, body, context = {}) {
|
|
|
1761
2086
|
|
|
1762
2087
|
// src/bundled.ts
|
|
1763
2088
|
import { createHash as createHash3 } from "crypto";
|
|
1764
|
-
import {
|
|
1765
|
-
|
|
2089
|
+
import {
|
|
2090
|
+
copyFileSync,
|
|
2091
|
+
existsSync,
|
|
2092
|
+
mkdirSync,
|
|
2093
|
+
readdirSync,
|
|
2094
|
+
renameSync,
|
|
2095
|
+
rmSync,
|
|
2096
|
+
statSync as statSync2
|
|
2097
|
+
} from "fs";
|
|
2098
|
+
import { readdir as readdir3, readFile as readFile4 } from "fs/promises";
|
|
1766
2099
|
import path5 from "path";
|
|
1767
2100
|
import { fileURLToPath } from "url";
|
|
1768
2101
|
var PKG_ROOT = path5.resolve(fileURLToPath(import.meta.url), "../..");
|
|
@@ -1771,16 +2104,39 @@ var SYSTEM_SKILLS_DIR = path5.join(BUNDLED_DIR, "system-skills");
|
|
|
1771
2104
|
var SCRIPTS_DIR = path5.join(BUNDLED_DIR, "scripts");
|
|
1772
2105
|
var SHARED_DIR = path5.join(BUNDLED_DIR, "shared");
|
|
1773
2106
|
var MONOREPO_SRC = path5.resolve(PKG_ROOT, "../buda/agent-templates");
|
|
1774
|
-
function syncTemplatesFromMonorepo() {
|
|
2107
|
+
function syncTemplatesFromMonorepo(options = {}) {
|
|
1775
2108
|
if (!existsSync(MONOREPO_SRC)) {
|
|
1776
2109
|
throw new Error(`Cannot sync agent-templates: source missing at ${MONOREPO_SRC}`);
|
|
1777
2110
|
}
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
})
|
|
2111
|
+
mkdirSync(BUNDLED_DIR, { recursive: true });
|
|
2112
|
+
syncDirectory(MONOREPO_SRC, BUNDLED_DIR, Boolean(options.prune));
|
|
2113
|
+
}
|
|
2114
|
+
function syncDirectory(sourceDirectory, destinationDirectory, prune) {
|
|
2115
|
+
const sourceEntries = /* @__PURE__ */ new Set();
|
|
2116
|
+
for (const entry of readdirSync(sourceDirectory, { withFileTypes: true })) {
|
|
2117
|
+
if (entry.name === "extensions") continue;
|
|
2118
|
+
sourceEntries.add(entry.name);
|
|
2119
|
+
const source = path5.join(sourceDirectory, entry.name);
|
|
2120
|
+
const destination = path5.join(destinationDirectory, entry.name);
|
|
2121
|
+
if (statSync2(source).isDirectory()) {
|
|
2122
|
+
mkdirSync(destination, { recursive: true });
|
|
2123
|
+
syncDirectory(source, destination, prune);
|
|
2124
|
+
continue;
|
|
2125
|
+
}
|
|
2126
|
+
const temporary = `${destination}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
2127
|
+
copyFileSync(source, temporary);
|
|
2128
|
+
try {
|
|
2129
|
+
renameSync(temporary, destination);
|
|
2130
|
+
} catch {
|
|
2131
|
+
copyFileSync(temporary, destination);
|
|
2132
|
+
rmSync(temporary, { force: true });
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
if (!prune) return;
|
|
2136
|
+
for (const entry of readdirSync(destinationDirectory, { withFileTypes: true })) {
|
|
2137
|
+
if (sourceEntries.has(entry.name)) continue;
|
|
2138
|
+
rmSync(path5.join(destinationDirectory, entry.name), { recursive: true, force: true });
|
|
2139
|
+
}
|
|
1784
2140
|
}
|
|
1785
2141
|
ensureBundledDir();
|
|
1786
2142
|
function ensureBundledDir() {
|
|
@@ -1798,7 +2154,7 @@ function ensureBundledDir() {
|
|
|
1798
2154
|
var cached = null;
|
|
1799
2155
|
async function readBundledManifest() {
|
|
1800
2156
|
if (cached) return cached;
|
|
1801
|
-
const systemSkillNames = (await
|
|
2157
|
+
const systemSkillNames = (await readdir3(SYSTEM_SKILLS_DIR, { withFileTypes: true }).catch(() => [])).filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
1802
2158
|
const version = await hashDir(BUNDLED_DIR);
|
|
1803
2159
|
cached = {
|
|
1804
2160
|
version,
|
|
@@ -1813,12 +2169,12 @@ async function readBundledManifest() {
|
|
|
1813
2169
|
async function hashDir(dir) {
|
|
1814
2170
|
const hash = createHash3("sha256");
|
|
1815
2171
|
await walk(dir, async (relPath, abs) => {
|
|
1816
|
-
hash.update(`${relPath}:`).update(await
|
|
2172
|
+
hash.update(`${relPath}:`).update(await readFile4(abs));
|
|
1817
2173
|
});
|
|
1818
2174
|
return hash.digest("hex");
|
|
1819
2175
|
}
|
|
1820
2176
|
async function walk(dir, visit, base = dir) {
|
|
1821
|
-
const entries = await
|
|
2177
|
+
const entries = await readdir3(dir, { withFileTypes: true }).catch(() => []);
|
|
1822
2178
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
1823
2179
|
for (const entry of entries) {
|
|
1824
2180
|
const abs = path5.join(dir, entry.name);
|
|
@@ -1843,6 +2199,30 @@ var BASE_CONNECTOR_CAPABILITIES = [
|
|
|
1843
2199
|
"shell",
|
|
1844
2200
|
"git"
|
|
1845
2201
|
];
|
|
2202
|
+
async function resolveConnectorScope(deviceId, launchConfig) {
|
|
2203
|
+
const serverUrl = normalizeServerOrigin(getServerUrl(launchConfig));
|
|
2204
|
+
const headers = { "Content-Type": "application/json" };
|
|
2205
|
+
const oauthToken = launchConfig?.oauthToken?.trim();
|
|
2206
|
+
const token = getConnectorToken(launchConfig);
|
|
2207
|
+
if (oauthToken) headers.Authorization = `Bearer ${oauthToken}`;
|
|
2208
|
+
if (token) headers["X-Buda-Connector-Token"] = token;
|
|
2209
|
+
const response = await fetch(`${serverUrl}/api/connectors/scope`, {
|
|
2210
|
+
method: "POST",
|
|
2211
|
+
headers,
|
|
2212
|
+
body: JSON.stringify({ deviceId })
|
|
2213
|
+
});
|
|
2214
|
+
if (!response.ok) {
|
|
2215
|
+
throw new Error(`/api/connectors/scope failed: ${response.status} ${await response.text()}`);
|
|
2216
|
+
}
|
|
2217
|
+
const result = await response.json();
|
|
2218
|
+
if (typeof result.spaceId !== "string" || !result.spaceId.trim()) {
|
|
2219
|
+
throw new Error("Connector scope response did not include spaceId");
|
|
2220
|
+
}
|
|
2221
|
+
return {
|
|
2222
|
+
...typeof result.connectorId === "string" ? { connectorId: result.connectorId } : {},
|
|
2223
|
+
spaceId: result.spaceId.trim()
|
|
2224
|
+
};
|
|
2225
|
+
}
|
|
1846
2226
|
var browserAvailabilityWarningLogged = false;
|
|
1847
2227
|
var getBrowserAvailabilityWarning = (browserAvailability) => browserAvailability.available ? null : `${browserAvailability.error} Browser automation is disabled, but shell, filesystem, Git, and coding capabilities remain available.`;
|
|
1848
2228
|
var getAdvertisedCapabilities = (browserAvailability = resolveBrowserAvailability(), deviceToken) => [
|
|
@@ -1864,7 +2244,7 @@ async function buildPayload(config, extra) {
|
|
|
1864
2244
|
deviceName: config.deviceName,
|
|
1865
2245
|
hostLabel: config.hostLabel,
|
|
1866
2246
|
platform: `${platform3()} ${release()}`,
|
|
1867
|
-
version:
|
|
2247
|
+
version: CONNECTOR_RUNTIME_VERSION,
|
|
1868
2248
|
pid: process.pid,
|
|
1869
2249
|
workdirRoot: config.workdirRoot,
|
|
1870
2250
|
baseUrl: extra?.bunnyDaemon?.baseUrl,
|
|
@@ -1893,127 +2273,1064 @@ async function buildPayload(config, extra) {
|
|
|
1893
2273
|
...extra?.logs?.length ? { logs: extra.logs } : {},
|
|
1894
2274
|
...extra?.recovery ? { recovery: extra.recovery } : {}
|
|
1895
2275
|
};
|
|
1896
|
-
}
|
|
1897
|
-
function buildAuthHeaders(config, launchConfig) {
|
|
1898
|
-
const headers = {
|
|
1899
|
-
"X-Buda-Connector-Id": config.connectorId
|
|
2276
|
+
}
|
|
2277
|
+
function buildAuthHeaders(config, launchConfig) {
|
|
2278
|
+
const headers = {
|
|
2279
|
+
"X-Buda-Connector-Id": config.connectorId
|
|
2280
|
+
};
|
|
2281
|
+
const token = config.token?.trim() || getConnectorToken(launchConfig);
|
|
2282
|
+
const oauthToken = launchConfig?.oauthToken?.trim();
|
|
2283
|
+
if (oauthToken) headers.Authorization = `Bearer ${oauthToken}`;
|
|
2284
|
+
if (token) headers["X-Buda-Connector-Token"] = token;
|
|
2285
|
+
if (config.deviceToken) headers["X-Buda-Connector-Device-Token"] = config.deviceToken;
|
|
2286
|
+
return headers;
|
|
2287
|
+
}
|
|
2288
|
+
async function postJson(serverUrl, pathname, body, config, launchConfig) {
|
|
2289
|
+
const response = await fetch(`${serverUrl}${pathname}`, {
|
|
2290
|
+
method: "POST",
|
|
2291
|
+
headers: {
|
|
2292
|
+
"Content-Type": "application/json",
|
|
2293
|
+
...buildAuthHeaders(config, launchConfig)
|
|
2294
|
+
},
|
|
2295
|
+
body: JSON.stringify(body)
|
|
2296
|
+
});
|
|
2297
|
+
if (!response.ok) {
|
|
2298
|
+
const text2 = await response.text().catch(() => "");
|
|
2299
|
+
let errorCode;
|
|
2300
|
+
let errorMessage3 = text2;
|
|
2301
|
+
try {
|
|
2302
|
+
const payload = JSON.parse(text2);
|
|
2303
|
+
errorCode = typeof payload.code === "string" ? payload.code : void 0;
|
|
2304
|
+
errorMessage3 = typeof payload.error === "string" ? payload.error : text2;
|
|
2305
|
+
} catch {
|
|
2306
|
+
}
|
|
2307
|
+
if (errorCode === "device_revoked") {
|
|
2308
|
+
throw new Error(
|
|
2309
|
+
`This device is prohibited/revoked and cannot reconnect. Contact support. (${errorMessage3})`
|
|
2310
|
+
);
|
|
2311
|
+
}
|
|
2312
|
+
throw new Error(`${pathname} failed: ${response.status} ${errorMessage3}`);
|
|
2313
|
+
}
|
|
2314
|
+
return await response.json();
|
|
2315
|
+
}
|
|
2316
|
+
function formatConnectorOwner(connector) {
|
|
2317
|
+
return connector.userEmail ?? connector.userId ?? "anonymous";
|
|
2318
|
+
}
|
|
2319
|
+
async function warnIfStaleBundle(result) {
|
|
2320
|
+
if (!result.bundle?.staleClient) return;
|
|
2321
|
+
await writeLog(
|
|
2322
|
+
"warn",
|
|
2323
|
+
`connector bundle out of date \u2014 server expects ${result.bundle.expectedVersion ?? "newer"}; run \`pnpm up @buda-ai/connector\``
|
|
2324
|
+
);
|
|
2325
|
+
}
|
|
2326
|
+
async function registerConnector(config, launchConfig, bunnyDaemon, options) {
|
|
2327
|
+
const result = await postJson(
|
|
2328
|
+
config.serverUrl,
|
|
2329
|
+
"/api/connectors/register",
|
|
2330
|
+
await buildPayload(config, {
|
|
2331
|
+
bunnyDaemon,
|
|
2332
|
+
...options?.recoverMissingProfile ? { recovery: "missing_local_profile" } : {}
|
|
2333
|
+
}),
|
|
2334
|
+
config,
|
|
2335
|
+
launchConfig
|
|
2336
|
+
);
|
|
2337
|
+
await writeLog(
|
|
2338
|
+
"info",
|
|
2339
|
+
`registered connector=${result.connector.connectorId} status=${result.connector.status} owner=${formatConnectorOwner(result.connector)}`
|
|
2340
|
+
);
|
|
2341
|
+
await warnIfStaleBundle(result);
|
|
2342
|
+
return {
|
|
2343
|
+
connectorId: result.connector.connectorId,
|
|
2344
|
+
spaceId: result.connector.spaceId ?? result.spaceId,
|
|
2345
|
+
relayProtocol: result.relayProtocol
|
|
2346
|
+
};
|
|
2347
|
+
}
|
|
2348
|
+
async function heartbeatConnector(config, launchConfig, bunnyDaemon) {
|
|
2349
|
+
const logs = takePendingLogs();
|
|
2350
|
+
const result = await postJson(
|
|
2351
|
+
config.serverUrl,
|
|
2352
|
+
"/api/connectors/heartbeat",
|
|
2353
|
+
await buildPayload(config, {
|
|
2354
|
+
bunnyDaemon,
|
|
2355
|
+
logs
|
|
2356
|
+
}),
|
|
2357
|
+
config,
|
|
2358
|
+
launchConfig
|
|
2359
|
+
).catch((error) => {
|
|
2360
|
+
restorePendingLogs(logs);
|
|
2361
|
+
throw error;
|
|
2362
|
+
});
|
|
2363
|
+
return { relayProtocol: result.relayProtocol };
|
|
2364
|
+
}
|
|
2365
|
+
async function unregisterConnector(config, launchConfig) {
|
|
2366
|
+
await postJson(
|
|
2367
|
+
config.serverUrl,
|
|
2368
|
+
"/api/connectors/unregister",
|
|
2369
|
+
{
|
|
2370
|
+
connectorId: config.connectorId
|
|
2371
|
+
},
|
|
2372
|
+
config,
|
|
2373
|
+
launchConfig
|
|
2374
|
+
).catch((error) => {
|
|
2375
|
+
void writeLog("warn", `unregister failed: ${error instanceof Error ? error.message : error}`);
|
|
2376
|
+
});
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
// src/runtime-install.ts
|
|
2380
|
+
import { execFile } from "child_process";
|
|
2381
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
2382
|
+
import { access, cp, mkdir as mkdir5, readFile as readFile5, rename as rename4, rm, writeFile as writeFile3 } from "fs/promises";
|
|
2383
|
+
import { homedir as homedir3 } from "os";
|
|
2384
|
+
import path6 from "path";
|
|
2385
|
+
import { promisify } from "util";
|
|
2386
|
+
var execFileAsync = promisify(execFile);
|
|
2387
|
+
var RUNTIME_INSTALL_TIMEOUT_MS = 5 * 6e4;
|
|
2388
|
+
var exists = async (filePath) => access(filePath).then(() => true).catch(() => false);
|
|
2389
|
+
var runtimeRoot = () => process.env.BUDA_CONNECTOR_RUNTIME_ROOT || path6.join(homedir3(), ".buda", "runtime");
|
|
2390
|
+
var runtimePlatformKey = `${process.platform}-${process.arch}-node${process.versions.modules}`;
|
|
2391
|
+
var versionDirectory = () => path6.join(runtimeRoot(), runtimePlatformKey, CONNECTOR_RUNTIME_VERSION);
|
|
2392
|
+
var manifestPath = () => path6.join(versionDirectory(), "runtime.json");
|
|
2393
|
+
var readRuntimeManifest = async () => {
|
|
2394
|
+
try {
|
|
2395
|
+
const parsed = JSON.parse(await readFile5(manifestPath(), "utf8"));
|
|
2396
|
+
if (parsed.version !== CONNECTOR_RUNTIME_VERSION || !await exists(parsed.entrypoint) || !await exists(parsed.nodeExecutable)) {
|
|
2397
|
+
return null;
|
|
2398
|
+
}
|
|
2399
|
+
return parsed;
|
|
2400
|
+
} catch {
|
|
2401
|
+
return null;
|
|
2402
|
+
}
|
|
2403
|
+
};
|
|
2404
|
+
var writeManifest = async (directory, manifest) => {
|
|
2405
|
+
await writeFile3(path6.join(directory, "runtime.json"), `${JSON.stringify(manifest, null, 2)}
|
|
2406
|
+
`, {
|
|
2407
|
+
mode: 384
|
|
2408
|
+
});
|
|
2409
|
+
};
|
|
2410
|
+
var validateRuntime = async (runtime) => {
|
|
2411
|
+
const { stdout } = await execFileAsync(
|
|
2412
|
+
runtime.nodeExecutable,
|
|
2413
|
+
[...runtime.nodeArgs ?? [], runtime.entrypoint, "--help"],
|
|
2414
|
+
{ timeout: 15e3 }
|
|
2415
|
+
);
|
|
2416
|
+
if (!stdout.includes("Help: buda-connector")) {
|
|
2417
|
+
throw new Error("Staged Connector runtime failed its CLI validation");
|
|
2418
|
+
}
|
|
2419
|
+
};
|
|
2420
|
+
var installFromNpm = async (directory) => {
|
|
2421
|
+
await mkdir5(directory, { recursive: true, mode: 448 });
|
|
2422
|
+
await execFileAsync(
|
|
2423
|
+
process.platform === "win32" ? "npm.cmd" : "npm",
|
|
2424
|
+
[
|
|
2425
|
+
"install",
|
|
2426
|
+
"--ignore-scripts=false",
|
|
2427
|
+
"--no-audit",
|
|
2428
|
+
"--no-fund",
|
|
2429
|
+
"--omit=dev",
|
|
2430
|
+
"--prefix",
|
|
2431
|
+
directory,
|
|
2432
|
+
`@buda-ai/connector@${CONNECTOR_RUNTIME_VERSION}`
|
|
2433
|
+
],
|
|
2434
|
+
{ env: process.env, timeout: RUNTIME_INSTALL_TIMEOUT_MS }
|
|
2435
|
+
);
|
|
2436
|
+
const entrypoint = path6.join(
|
|
2437
|
+
directory,
|
|
2438
|
+
"node_modules",
|
|
2439
|
+
"@buda-ai",
|
|
2440
|
+
"connector",
|
|
2441
|
+
"dist",
|
|
2442
|
+
"cli.js"
|
|
2443
|
+
);
|
|
2444
|
+
const nodeExecutable = path6.join(directory, "node", path6.basename(process.execPath));
|
|
2445
|
+
await mkdir5(path6.dirname(nodeExecutable), { recursive: true });
|
|
2446
|
+
await cp(process.execPath, nodeExecutable, { force: true });
|
|
2447
|
+
if (!await exists(entrypoint)) throw new Error("Installed Connector runtime is incomplete");
|
|
2448
|
+
return { entrypoint, nodeExecutable, version: CONNECTOR_RUNTIME_VERSION };
|
|
2449
|
+
};
|
|
2450
|
+
var installFromBundledRuntime = async (directory, sourceRoot) => {
|
|
2451
|
+
const sourceNode = process.env.BUDA_CONNECTOR_RUNTIME_NODE || process.execPath;
|
|
2452
|
+
const sourceEntrypoint = process.env.BUDA_CONNECTOR_RUNTIME_ENTRYPOINT || process.argv[1];
|
|
2453
|
+
if (!sourceEntrypoint) throw new Error("Bundled Connector runtime entrypoint is missing");
|
|
2454
|
+
await cp(sourceRoot, directory, { recursive: true, force: true });
|
|
2455
|
+
const relativeEntrypoint = path6.relative(sourceRoot, sourceEntrypoint);
|
|
2456
|
+
if (relativeEntrypoint.startsWith("..") || path6.isAbsolute(relativeEntrypoint)) {
|
|
2457
|
+
throw new Error("Bundled Connector entrypoint must be inside its runtime root");
|
|
2458
|
+
}
|
|
2459
|
+
const relativeNode = path6.relative(sourceRoot, sourceNode);
|
|
2460
|
+
const entrypoint = path6.join(directory, relativeEntrypoint);
|
|
2461
|
+
const nodeExecutable = relativeNode.startsWith("..") ? path6.join(directory, "node", path6.basename(sourceNode)) : path6.join(directory, relativeNode);
|
|
2462
|
+
if (relativeNode.startsWith("..")) {
|
|
2463
|
+
await mkdir5(path6.dirname(nodeExecutable), { recursive: true });
|
|
2464
|
+
await cp(sourceNode, nodeExecutable, { force: true });
|
|
2465
|
+
}
|
|
2466
|
+
if (!await exists(entrypoint))
|
|
2467
|
+
throw new Error("Copied Connector runtime entrypoint is missing");
|
|
2468
|
+
return { entrypoint, nodeExecutable, version: CONNECTOR_RUNTIME_VERSION };
|
|
2469
|
+
};
|
|
2470
|
+
async function ensureStableConnectorRuntime() {
|
|
2471
|
+
if (process.env.BUDA_CONNECTOR_SKIP_RUNTIME_STAGE === "1" || process.argv[1]?.endsWith(".ts")) {
|
|
2472
|
+
const entrypoint = process.argv[1];
|
|
2473
|
+
if (!entrypoint) throw new Error("Connector CLI entrypoint is missing");
|
|
2474
|
+
return {
|
|
2475
|
+
entrypoint,
|
|
2476
|
+
...entrypoint.endsWith(".ts") ? { nodeArgs: [...process.execArgv] } : {},
|
|
2477
|
+
nodeExecutable: process.execPath,
|
|
2478
|
+
version: CONNECTOR_RUNTIME_VERSION
|
|
2479
|
+
};
|
|
2480
|
+
}
|
|
2481
|
+
const existing = await readRuntimeManifest();
|
|
2482
|
+
if (existing) return existing;
|
|
2483
|
+
const platformRoot = path6.join(runtimeRoot(), runtimePlatformKey);
|
|
2484
|
+
await mkdir5(platformRoot, { recursive: true, mode: 448 });
|
|
2485
|
+
return withExclusiveFileLock(
|
|
2486
|
+
path6.join(platformRoot, `install-${CONNECTOR_RUNTIME_VERSION}`),
|
|
2487
|
+
async () => {
|
|
2488
|
+
const installed = await readRuntimeManifest();
|
|
2489
|
+
if (installed) return installed;
|
|
2490
|
+
const temporary = path6.join(
|
|
2491
|
+
platformRoot,
|
|
2492
|
+
`.${CONNECTOR_RUNTIME_VERSION}.${randomUUID4()}.tmp`
|
|
2493
|
+
);
|
|
2494
|
+
await rm(temporary, { recursive: true, force: true });
|
|
2495
|
+
try {
|
|
2496
|
+
const bundledSource = process.env.BUDA_CONNECTOR_BUNDLED_RUNTIME_ROOT;
|
|
2497
|
+
const manifest = bundledSource ? await installFromBundledRuntime(temporary, bundledSource) : await installFromNpm(temporary);
|
|
2498
|
+
await validateRuntime(manifest);
|
|
2499
|
+
const finalDirectory = versionDirectory();
|
|
2500
|
+
const rebased = {
|
|
2501
|
+
...manifest,
|
|
2502
|
+
entrypoint: path6.join(finalDirectory, path6.relative(temporary, manifest.entrypoint)),
|
|
2503
|
+
nodeExecutable: path6.join(
|
|
2504
|
+
finalDirectory,
|
|
2505
|
+
path6.relative(temporary, manifest.nodeExecutable)
|
|
2506
|
+
)
|
|
2507
|
+
};
|
|
2508
|
+
await writeManifest(temporary, rebased);
|
|
2509
|
+
await rm(finalDirectory, { recursive: true, force: true });
|
|
2510
|
+
await rename4(temporary, finalDirectory);
|
|
2511
|
+
return rebased;
|
|
2512
|
+
} catch (error) {
|
|
2513
|
+
await rm(temporary, { recursive: true, force: true });
|
|
2514
|
+
throw error;
|
|
2515
|
+
}
|
|
2516
|
+
},
|
|
2517
|
+
{ timeoutMs: RUNTIME_INSTALL_TIMEOUT_MS }
|
|
2518
|
+
);
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
// src/daemon-client.ts
|
|
2522
|
+
var START_TIMEOUT_MS = 45e3;
|
|
2523
|
+
var BOOTSTRAP_TIMEOUT_MS = 5 * 6e4;
|
|
2524
|
+
var POLL_INTERVAL_MS = 250;
|
|
2525
|
+
var wait2 = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
2526
|
+
var getConfigRoot = () => process.env.BUDA_CONNECTOR_CONFIG_ROOT;
|
|
2527
|
+
var readLiveStatus = async (config, configRoot = getConfigRoot(), timeoutMs) => requestDaemonControl(config, "status", configRoot, { timeoutMs }).catch(() => null);
|
|
2528
|
+
var isProcessAlive2 = (pid) => {
|
|
2529
|
+
try {
|
|
2530
|
+
process.kill(pid, 0);
|
|
2531
|
+
return true;
|
|
2532
|
+
} catch (error) {
|
|
2533
|
+
return !(typeof error === "object" && error !== null && "code" in error && error.code === "ESRCH");
|
|
2534
|
+
}
|
|
2535
|
+
};
|
|
2536
|
+
var getRuntimeCommandArgs = (runtime, mode) => [
|
|
2537
|
+
...runtime.nodeArgs ?? [],
|
|
2538
|
+
runtime.entrypoint,
|
|
2539
|
+
"daemon",
|
|
2540
|
+
"--internal-daemon",
|
|
2541
|
+
"--daemon-mode",
|
|
2542
|
+
mode,
|
|
2543
|
+
"--plain"
|
|
2544
|
+
];
|
|
2545
|
+
var compareVersions = (left, right) => {
|
|
2546
|
+
const leftParts = left.split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0);
|
|
2547
|
+
const rightParts = right.split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0);
|
|
2548
|
+
for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index += 1) {
|
|
2549
|
+
const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
|
|
2550
|
+
if (difference !== 0) return difference;
|
|
2551
|
+
}
|
|
2552
|
+
return 0;
|
|
2553
|
+
};
|
|
2554
|
+
var shouldReplaceDaemonVersion = (runningVersion, candidateVersion) => compareVersions(candidateVersion, runningVersion) > 0;
|
|
2555
|
+
var waitForDaemon = async (launchConfig, deadline) => {
|
|
2556
|
+
let lastConfig = null;
|
|
2557
|
+
while (Date.now() < deadline) {
|
|
2558
|
+
lastConfig = await readDeviceConfig(launchConfig, getConfigRoot());
|
|
2559
|
+
if (lastConfig) {
|
|
2560
|
+
const status = await readLiveStatus(lastConfig);
|
|
2561
|
+
if (status) return { config: lastConfig, status };
|
|
2562
|
+
}
|
|
2563
|
+
await wait2(POLL_INTERVAL_MS);
|
|
2564
|
+
}
|
|
2565
|
+
throw new Error(
|
|
2566
|
+
lastConfig ? "Connector daemon did not expose its control channel in time" : "Connector registration did not create a canonical local identity in time"
|
|
2567
|
+
);
|
|
2568
|
+
};
|
|
2569
|
+
var spawnInternalDaemon = async (launchConfig, mode, preparedRuntime) => {
|
|
2570
|
+
const runtime = preparedRuntime ?? await ensureStableConnectorRuntime();
|
|
2571
|
+
const child = spawn(runtime.nodeExecutable, getRuntimeCommandArgs(runtime, mode), {
|
|
2572
|
+
detached: true,
|
|
2573
|
+
env: {
|
|
2574
|
+
...process.env,
|
|
2575
|
+
BUDA_CONNECTOR_INTERNAL_LAUNCH_CONFIG: JSON.stringify(launchConfig ?? {})
|
|
2576
|
+
},
|
|
2577
|
+
stdio: "ignore",
|
|
2578
|
+
windowsHide: true
|
|
2579
|
+
});
|
|
2580
|
+
child.unref();
|
|
2581
|
+
};
|
|
2582
|
+
var stopOlderIdleDaemon = async (config, status) => {
|
|
2583
|
+
if (!shouldReplaceDaemonVersion(status.version, CONNECTOR_RUNTIME_VERSION)) return false;
|
|
2584
|
+
if (!status.idle) return false;
|
|
2585
|
+
const stopStatus = await requestDaemonControl(config, "stop-if-idle", getConfigRoot());
|
|
2586
|
+
if (!stopStatus.idle) return false;
|
|
2587
|
+
const deadline = Date.now() + 1e4;
|
|
2588
|
+
while (Date.now() < deadline) {
|
|
2589
|
+
if (!await readLiveStatus(config)) return true;
|
|
2590
|
+
await wait2(POLL_INTERVAL_MS);
|
|
2591
|
+
}
|
|
2592
|
+
throw new Error("Older Connector daemon did not stop cleanly; existing identity was preserved");
|
|
2593
|
+
};
|
|
2594
|
+
async function ensureConnectorDaemon(launchConfig, mode) {
|
|
2595
|
+
const deviceId = await getOrCreateSystemDeviceId(getConfigRoot());
|
|
2596
|
+
const scope = await resolveConnectorScope(deviceId, launchConfig);
|
|
2597
|
+
launchConfig = { ...launchConfig, spaceId: scope.spaceId };
|
|
2598
|
+
const bootstrapTarget = getConnectorBootstrapLockTarget(launchConfig, getConfigRoot());
|
|
2599
|
+
await mkdir6(path7.dirname(bootstrapTarget), { recursive: true, mode: 448 });
|
|
2600
|
+
const bootstrapLock = await acquireExclusiveFileLock(bootstrapTarget, {
|
|
2601
|
+
timeoutMs: BOOTSTRAP_TIMEOUT_MS
|
|
2602
|
+
});
|
|
2603
|
+
try {
|
|
2604
|
+
const existingConfig = await readDeviceConfig(launchConfig, getConfigRoot());
|
|
2605
|
+
if (existingConfig) {
|
|
2606
|
+
const existingStatus = await readLiveStatus(existingConfig);
|
|
2607
|
+
if (existingStatus) {
|
|
2608
|
+
const preparedRuntime = !shouldReplaceDaemonVersion(existingStatus.version, CONNECTOR_RUNTIME_VERSION) || !existingStatus.idle ? void 0 : await ensureStableConnectorRuntime();
|
|
2609
|
+
const replaced = await stopOlderIdleDaemon(existingConfig, existingStatus);
|
|
2610
|
+
if (!replaced) {
|
|
2611
|
+
const status = mode === "persistent" ? await requestDaemonControl(existingConfig, "persist", getConfigRoot()) : existingStatus;
|
|
2612
|
+
return { config: existingConfig, status };
|
|
2613
|
+
}
|
|
2614
|
+
await spawnInternalDaemon(launchConfig, mode, preparedRuntime);
|
|
2615
|
+
return await waitForDaemon(launchConfig, Date.now() + START_TIMEOUT_MS);
|
|
2616
|
+
}
|
|
2617
|
+
const persistedStatus = await readPersistedDaemonStatus(
|
|
2618
|
+
existingConfig,
|
|
2619
|
+
getConfigRoot()
|
|
2620
|
+
).catch(() => null);
|
|
2621
|
+
if (persistedStatus && isProcessAlive2(persistedStatus.pid)) {
|
|
2622
|
+
throw new Error(
|
|
2623
|
+
`Connector daemon PID ${persistedStatus.pid} is running but its local control channel is not responding. Stop that process and retry.`
|
|
2624
|
+
);
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
await spawnInternalDaemon(launchConfig, mode);
|
|
2628
|
+
return await waitForDaemon(launchConfig, Date.now() + START_TIMEOUT_MS);
|
|
2629
|
+
} finally {
|
|
2630
|
+
await bootstrapLock.release();
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
var streamDaemonLogs = (options) => {
|
|
2634
|
+
const logPath = getConnectorPaths(options.serverUrl, getConfigRoot()).logPath;
|
|
2635
|
+
let activePath = "";
|
|
2636
|
+
let seenLines = 0;
|
|
2637
|
+
const poll = async () => {
|
|
2638
|
+
const datedPath = getDatedConnectorLogPath(logPath);
|
|
2639
|
+
const raw = await readFile6(datedPath, "utf8").catch(() => "");
|
|
2640
|
+
const lines = raw.split("\n").filter(Boolean);
|
|
2641
|
+
if (datedPath !== activePath || lines.length < seenLines) {
|
|
2642
|
+
activePath = datedPath;
|
|
2643
|
+
seenLines = 0;
|
|
2644
|
+
}
|
|
2645
|
+
for (const line of lines.slice(seenLines)) {
|
|
2646
|
+
try {
|
|
2647
|
+
const entry = JSON.parse(line);
|
|
2648
|
+
const connectorId = entry.metadata?.connectorId;
|
|
2649
|
+
if (options.dashboardActive && typeof connectorId === "string") {
|
|
2650
|
+
const output2 = entry.metadata?.source === "daemon-console" ? entry.message : `[buda-connector] ${entry.message}`;
|
|
2651
|
+
appendConnectorDashboardLog(connectorId, entry.level, output2);
|
|
2652
|
+
continue;
|
|
2653
|
+
}
|
|
2654
|
+
if (connectorId !== options.connectorId) continue;
|
|
2655
|
+
const output = entry.metadata?.source === "daemon-console" ? entry.message : `[buda-connector] ${entry.message}`;
|
|
2656
|
+
if (entry.level === "error") console.error(output);
|
|
2657
|
+
else if (entry.level === "warn") console.warn(output);
|
|
2658
|
+
else console.log(output);
|
|
2659
|
+
} catch {
|
|
2660
|
+
if (options.plain) console.log(line);
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
seenLines = lines.length;
|
|
2664
|
+
};
|
|
2665
|
+
void poll();
|
|
2666
|
+
const timer = setInterval(() => void poll(), 500);
|
|
2667
|
+
return () => clearInterval(timer);
|
|
2668
|
+
};
|
|
2669
|
+
var toDashboardConnector = (config, status) => ({
|
|
2670
|
+
agent: status ? status.idle ? "Ready" : "Busy" : "Stopped",
|
|
2671
|
+
connectorId: config.connectorId,
|
|
2672
|
+
heartbeat: status?.lastHeartbeatAt ? `OK ${new Date(status.lastHeartbeatAt).toLocaleTimeString("en-GB", { hour12: false })}` : status ? "Waiting" : "Stopped",
|
|
2673
|
+
relay: status?.relayConnected ? "Connected" : "Disconnected",
|
|
2674
|
+
serverUrl: config.serverUrl,
|
|
2675
|
+
spaceId: config.spaceId ?? "Unknown",
|
|
2676
|
+
status: status ? status.relayConnected ? "Online" : "Running" : "Stopped"
|
|
2677
|
+
});
|
|
2678
|
+
async function readLocalConnectorDashboardRows(configRoot = getConfigRoot()) {
|
|
2679
|
+
const configs = await listDeviceConfigs(configRoot);
|
|
2680
|
+
const persistedStatuses = await listPersistedDaemonStatuses(configRoot);
|
|
2681
|
+
const persistedByIdentity = new Map(
|
|
2682
|
+
persistedStatuses.map((status) => [
|
|
2683
|
+
`${normalizeServerOrigin(status.serverUrl)}
|
|
2684
|
+
${status.connectorId}`,
|
|
2685
|
+
status
|
|
2686
|
+
])
|
|
2687
|
+
);
|
|
2688
|
+
const identities = /* @__PURE__ */ new Map();
|
|
2689
|
+
for (const config of configs) {
|
|
2690
|
+
identities.set(`${normalizeServerOrigin(config.serverUrl)}
|
|
2691
|
+
${config.connectorId}`, {
|
|
2692
|
+
identity: config,
|
|
2693
|
+
stored: true
|
|
2694
|
+
});
|
|
2695
|
+
}
|
|
2696
|
+
for (const status of persistedStatuses) {
|
|
2697
|
+
const key = `${normalizeServerOrigin(status.serverUrl)}
|
|
2698
|
+
${status.connectorId}`;
|
|
2699
|
+
if (!identities.has(key)) {
|
|
2700
|
+
identities.set(key, { identity: status, stored: false });
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
const rows = await Promise.all(
|
|
2704
|
+
[...identities.entries()].map(async ([key, { identity, stored }]) => {
|
|
2705
|
+
const status = await readLiveStatus(identity, configRoot, 1500);
|
|
2706
|
+
if (status) return toDashboardConnector(identity, status);
|
|
2707
|
+
const persistedStatus = persistedByIdentity.get(key) ?? await readPersistedDaemonStatus(identity, configRoot).catch(() => null);
|
|
2708
|
+
const row = toDashboardConnector(identity, null);
|
|
2709
|
+
if (!stored && (!persistedStatus || !isProcessAlive2(persistedStatus.pid))) return null;
|
|
2710
|
+
return persistedStatus && isProcessAlive2(persistedStatus.pid) ? { ...row, status: "Unresponsive" } : row;
|
|
2711
|
+
})
|
|
2712
|
+
);
|
|
2713
|
+
return rows.filter((row) => row !== null);
|
|
2714
|
+
}
|
|
2715
|
+
async function stopLocalConnectorDaemon(connectorId, serverUrl, configRoot = getConfigRoot()) {
|
|
2716
|
+
const configs = await listDeviceConfigs(configRoot);
|
|
2717
|
+
const persistedStatuses = await listPersistedDaemonStatuses(configRoot);
|
|
2718
|
+
const serverOrigin = normalizeServerOrigin(serverUrl);
|
|
2719
|
+
const config = [...configs, ...persistedStatuses].find(
|
|
2720
|
+
(candidate) => candidate.connectorId === connectorId && normalizeServerOrigin(candidate.serverUrl) === serverOrigin
|
|
2721
|
+
);
|
|
2722
|
+
if (!config) throw new Error(`Local Connector ${connectorId} was not found`);
|
|
2723
|
+
const status = await readLiveStatus(config, configRoot);
|
|
2724
|
+
if (!status) return;
|
|
2725
|
+
await requestDaemonControl(config, "stop", configRoot);
|
|
2726
|
+
}
|
|
2727
|
+
async function stopAllLocalConnectorDaemons(configRoot = getConfigRoot()) {
|
|
2728
|
+
const configs = await listDeviceConfigs(configRoot);
|
|
2729
|
+
const persistedStatuses = await listPersistedDaemonStatuses(configRoot);
|
|
2730
|
+
const identities = /* @__PURE__ */ new Map();
|
|
2731
|
+
for (const config of [...configs, ...persistedStatuses]) {
|
|
2732
|
+
identities.set(`${normalizeServerOrigin(config.serverUrl)}
|
|
2733
|
+
${config.connectorId}`, config);
|
|
2734
|
+
}
|
|
2735
|
+
const results = await Promise.allSettled(
|
|
2736
|
+
[...identities.values()].map(async (config) => {
|
|
2737
|
+
const status = await readLiveStatus(config, configRoot);
|
|
2738
|
+
if (status) await requestDaemonControl(config, "stop", configRoot);
|
|
2739
|
+
})
|
|
2740
|
+
);
|
|
2741
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
2742
|
+
if (failures.length) {
|
|
2743
|
+
throw new Error(`Failed to stop ${failures.length} local Connector daemon(s)`);
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
var waitForConnectorViewerExit = async (dashboardClosed) => {
|
|
2747
|
+
let finishSignal = () => void 0;
|
|
2748
|
+
const signal = new Promise((resolve) => {
|
|
2749
|
+
finishSignal = resolve;
|
|
2750
|
+
});
|
|
2751
|
+
process.once("SIGINT", finishSignal);
|
|
2752
|
+
process.once("SIGTERM", finishSignal);
|
|
2753
|
+
await (dashboardClosed ? Promise.race([dashboardClosed, signal]) : signal);
|
|
2754
|
+
process.off("SIGINT", finishSignal);
|
|
2755
|
+
process.off("SIGTERM", finishSignal);
|
|
2756
|
+
};
|
|
2757
|
+
async function runConnectorDashboard(launchConfig, options = {}) {
|
|
2758
|
+
let finishDashboard = () => void 0;
|
|
2759
|
+
const dashboardClosed = new Promise((resolve) => {
|
|
2760
|
+
finishDashboard = resolve;
|
|
2761
|
+
});
|
|
2762
|
+
const dashboard = startConnectorConsoleDashboard({
|
|
2763
|
+
manager: {
|
|
2764
|
+
onExit: finishDashboard,
|
|
2765
|
+
onStopAll: () => stopAllLocalConnectorDaemons(),
|
|
2766
|
+
onStopConnector: (connectorId, serverUrl) => stopLocalConnectorDaemon(connectorId, serverUrl)
|
|
2767
|
+
},
|
|
2768
|
+
plain: options.plain
|
|
2769
|
+
});
|
|
2770
|
+
if (!dashboard) {
|
|
2771
|
+
const { config } = await ensureConnectorDaemon(launchConfig, "persistent");
|
|
2772
|
+
const stopLogs2 = streamDaemonLogs({
|
|
2773
|
+
connectorId: config.connectorId,
|
|
2774
|
+
dashboardActive: false,
|
|
2775
|
+
plain: Boolean(options.plain),
|
|
2776
|
+
serverUrl: config.serverUrl
|
|
2777
|
+
});
|
|
2778
|
+
if (!process.stdin.isTTY) {
|
|
2779
|
+
stopLogs2();
|
|
2780
|
+
console.log(`Buda Connector ${config.connectorId} is running in the background.`);
|
|
2781
|
+
return;
|
|
2782
|
+
}
|
|
2783
|
+
await waitForConnectorViewerExit();
|
|
2784
|
+
stopLogs2();
|
|
2785
|
+
return;
|
|
2786
|
+
}
|
|
2787
|
+
const pendingConnectorId = `pending:${normalizeServerOrigin(getServerUrl(launchConfig))}`;
|
|
2788
|
+
let pendingConnector = {
|
|
2789
|
+
agent: "Starting",
|
|
2790
|
+
connectorId: pendingConnectorId,
|
|
2791
|
+
heartbeat: "Waiting",
|
|
2792
|
+
label: "New Connector",
|
|
2793
|
+
relay: "Connecting",
|
|
2794
|
+
serverUrl: getServerUrl(launchConfig),
|
|
2795
|
+
spaceId: "Resolving Space",
|
|
2796
|
+
status: "Connecting"
|
|
2797
|
+
};
|
|
2798
|
+
let preferredConnectorId = pendingConnectorId;
|
|
2799
|
+
let bootstrapSettled = false;
|
|
2800
|
+
const stopLogs = streamDaemonLogs({
|
|
2801
|
+
dashboardActive: true,
|
|
2802
|
+
plain: false,
|
|
2803
|
+
serverUrl: getServerUrl(launchConfig)
|
|
2804
|
+
});
|
|
2805
|
+
let refreshing = false;
|
|
2806
|
+
const refresh = async () => {
|
|
2807
|
+
if (refreshing) return;
|
|
2808
|
+
refreshing = true;
|
|
2809
|
+
try {
|
|
2810
|
+
const connectors = await readLocalConnectorDashboardRows();
|
|
2811
|
+
updateConnectorDashboardConnectors(
|
|
2812
|
+
pendingConnector ? [...connectors, pendingConnector] : connectors,
|
|
2813
|
+
preferredConnectorId
|
|
2814
|
+
);
|
|
2815
|
+
} finally {
|
|
2816
|
+
refreshing = false;
|
|
2817
|
+
}
|
|
2818
|
+
};
|
|
2819
|
+
updateConnectorDashboardConnectors([pendingConnector], pendingConnectorId);
|
|
2820
|
+
void readDeviceConfig(launchConfig, getConfigRoot()).then((config) => {
|
|
2821
|
+
if (!config) return;
|
|
2822
|
+
pendingConnector = void 0;
|
|
2823
|
+
preferredConnectorId = config.connectorId;
|
|
2824
|
+
}).catch(() => void 0).finally(() => refresh());
|
|
2825
|
+
const timer = setInterval(() => void refresh(), 1e3);
|
|
2826
|
+
void ensureConnectorDaemon(launchConfig, "persistent").then(async ({ config }) => {
|
|
2827
|
+
pendingConnector = void 0;
|
|
2828
|
+
preferredConnectorId = config.connectorId;
|
|
2829
|
+
await refresh();
|
|
2830
|
+
}).catch(async (error) => {
|
|
2831
|
+
const config = await readDeviceConfig(launchConfig, getConfigRoot()).catch(() => null);
|
|
2832
|
+
const message = `connector startup failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
2833
|
+
if (config) {
|
|
2834
|
+
pendingConnector = void 0;
|
|
2835
|
+
preferredConnectorId = config.connectorId;
|
|
2836
|
+
appendConnectorDashboardLog(config.connectorId, "error", `[buda-connector] ${message}`);
|
|
2837
|
+
await refresh();
|
|
2838
|
+
} else {
|
|
2839
|
+
pendingConnector = {
|
|
2840
|
+
...pendingConnector,
|
|
2841
|
+
agent: "Error",
|
|
2842
|
+
heartbeat: "Failed",
|
|
2843
|
+
relay: "Failed",
|
|
2844
|
+
status: "Error"
|
|
2845
|
+
};
|
|
2846
|
+
appendConnectorDashboardLog(pendingConnectorId, "error", `[buda-connector] ${message}`);
|
|
2847
|
+
await refresh();
|
|
2848
|
+
}
|
|
2849
|
+
}).finally(() => {
|
|
2850
|
+
bootstrapSettled = true;
|
|
2851
|
+
});
|
|
2852
|
+
await waitForConnectorViewerExit(dashboardClosed);
|
|
2853
|
+
clearInterval(timer);
|
|
2854
|
+
stopLogs();
|
|
2855
|
+
dashboard.stop();
|
|
2856
|
+
if (!bootstrapSettled) process.exit(0);
|
|
2857
|
+
}
|
|
2858
|
+
async function runDesktopConnectorController(launchConfig) {
|
|
2859
|
+
const { config } = await ensureConnectorDaemon(launchConfig, "transient");
|
|
2860
|
+
if (!config.spaceId?.trim()) {
|
|
2861
|
+
throw new Error("Connector scope did not resolve a canonical spaceId");
|
|
2862
|
+
}
|
|
2863
|
+
const lease = await holdDaemonControl(config, getConfigRoot());
|
|
2864
|
+
process.stdout.write(
|
|
2865
|
+
`${JSON.stringify({
|
|
2866
|
+
type: "buda-connector-ready",
|
|
2867
|
+
connectorId: lease.status.connectorId,
|
|
2868
|
+
pid: lease.status.pid,
|
|
2869
|
+
spaceId: lease.status.spaceId
|
|
2870
|
+
})}
|
|
2871
|
+
`
|
|
2872
|
+
);
|
|
2873
|
+
const stopped = new Promise((resolve) => {
|
|
2874
|
+
process.once("SIGINT", resolve);
|
|
2875
|
+
process.once("SIGTERM", resolve);
|
|
2876
|
+
process.stdin.once("end", resolve);
|
|
2877
|
+
process.stdin.once("close", resolve);
|
|
2878
|
+
if (process.stdin.readableEnded || process.stdin.destroyed) resolve();
|
|
2879
|
+
});
|
|
2880
|
+
await Promise.race([lease.closed, stopped]);
|
|
2881
|
+
lease.close();
|
|
2882
|
+
}
|
|
2883
|
+
async function releaseDesktopConnector(launchConfig) {
|
|
2884
|
+
if (!launchConfig?.spaceId?.trim()) return;
|
|
2885
|
+
const config = await readDeviceConfig(launchConfig, getConfigRoot());
|
|
2886
|
+
if (!config) return;
|
|
2887
|
+
await requestDaemonControl(config, "release", getConfigRoot()).catch(() => void 0);
|
|
2888
|
+
}
|
|
2889
|
+
var getInternalLaunchConfig = () => {
|
|
2890
|
+
const raw = process.env.BUDA_CONNECTOR_INTERNAL_LAUNCH_CONFIG;
|
|
2891
|
+
if (!raw) return void 0;
|
|
2892
|
+
return JSON.parse(raw);
|
|
2893
|
+
};
|
|
2894
|
+
var getDaemonModeFlag = () => {
|
|
2895
|
+
const index = process.argv.indexOf("--daemon-mode");
|
|
2896
|
+
return process.argv[index + 1] === "transient" ? "transient" : "persistent";
|
|
2897
|
+
};
|
|
2898
|
+
|
|
2899
|
+
// src/onboard.ts
|
|
2900
|
+
import { cancel, intro, isCancel, log, note, outro, spinner, text } from "@clack/prompts";
|
|
2901
|
+
|
|
2902
|
+
// src/bunny-daemon.ts
|
|
2903
|
+
import { mkdir as mkdir7 } from "fs/promises";
|
|
2904
|
+
import { createDaemon } from "@bunny-agent/daemon";
|
|
2905
|
+
|
|
2906
|
+
// src/terminal.ts
|
|
2907
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
2908
|
+
import { chmod as chmod3, stat as stat3 } from "fs/promises";
|
|
2909
|
+
import { createRequire } from "module";
|
|
2910
|
+
import path8 from "path";
|
|
2911
|
+
import WebSocket, { WebSocketServer } from "ws";
|
|
2912
|
+
var TERMINAL_IDLE_TTL_MS = 10 * 6e4;
|
|
2913
|
+
var TERMINAL_HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
2914
|
+
var LOCAL_TERMINAL_GRANT_TTL_MS = 3e4;
|
|
2915
|
+
var LOCAL_TERMINAL_CLOSE_SPAWN_FAILED = 4401;
|
|
2916
|
+
var MAX_BUFFER_CHUNKS = 1e3;
|
|
2917
|
+
var MAX_TERMINAL_FRAME_BYTES = 1024 * 1024;
|
|
2918
|
+
var require2 = createRequire(import.meta.url);
|
|
2919
|
+
var ensureNodePtySpawnHelperExecutable = async (options = {}) => {
|
|
2920
|
+
const platform4 = options.platform ?? process.platform;
|
|
2921
|
+
if (platform4 !== "darwin") return;
|
|
2922
|
+
const arch = options.arch ?? process.arch;
|
|
2923
|
+
const entryPath = options.entryPath ?? require2.resolve("node-pty");
|
|
2924
|
+
const packageRoot = path8.dirname(path8.dirname(entryPath));
|
|
2925
|
+
const helperPath = path8.join(packageRoot, "prebuilds", `${platform4}-${arch}`, "spawn-helper");
|
|
2926
|
+
const helper = await stat3(helperPath);
|
|
2927
|
+
if ((helper.mode & 64) === 0) await chmod3(helperPath, helper.mode | 64);
|
|
2928
|
+
};
|
|
2929
|
+
var loadNodePty = async () => {
|
|
2930
|
+
await ensureNodePtySpawnHelperExecutable();
|
|
2931
|
+
return import("node-pty");
|
|
2932
|
+
};
|
|
2933
|
+
var normalizeTerminalCloseCode = (code, fallback = 1011) => code >= 1e3 && code <= 1014 && ![1004, 1005, 1006].includes(code) || code >= 3e3 && code <= 4999 ? code : fallback;
|
|
2934
|
+
var truncateTerminalCloseReason = (reason, maxBytes = 120) => {
|
|
2935
|
+
let result = reason;
|
|
2936
|
+
while (Buffer.byteLength(result, "utf8") > maxBytes) result = result.slice(0, -1);
|
|
2937
|
+
return result;
|
|
2938
|
+
};
|
|
2939
|
+
var sessions2 = /* @__PURE__ */ new Map();
|
|
2940
|
+
var localGrants = /* @__PURE__ */ new Map();
|
|
2941
|
+
var installedServers = /* @__PURE__ */ new WeakSet();
|
|
2942
|
+
var isSafeId = (value) => typeof value === "string" && /^[A-Za-z0-9_.:-]{1,160}$/.test(value);
|
|
2943
|
+
var parseDimension = (value, fallback, min, max) => typeof value === "number" && Number.isSafeInteger(value) ? Math.max(min, Math.min(max, value)) : fallback;
|
|
2944
|
+
var parseConnectorTerminalRequest = (request) => {
|
|
2945
|
+
if (request.method !== "CONNECT" || request.path !== "/api/terminal") {
|
|
2946
|
+
throw new Error("Invalid connector terminal request.");
|
|
2947
|
+
}
|
|
2948
|
+
if (!request.body || typeof request.body !== "object" || Array.isArray(request.body)) {
|
|
2949
|
+
throw new Error("Connector terminal request body is invalid.");
|
|
2950
|
+
}
|
|
2951
|
+
const body = request.body;
|
|
2952
|
+
if (!isSafeId(body.sessionId) || !isSafeId(body.tabId)) {
|
|
2953
|
+
throw new Error("Connector terminal session identity is invalid.");
|
|
2954
|
+
}
|
|
2955
|
+
if (typeof body.token !== "string" || body.token.length < 32 || body.token.length > 2048) {
|
|
2956
|
+
throw new Error("Connector terminal token is invalid.");
|
|
2957
|
+
}
|
|
2958
|
+
return {
|
|
2959
|
+
sessionId: body.sessionId,
|
|
2960
|
+
tabId: body.tabId,
|
|
2961
|
+
token: body.token,
|
|
2962
|
+
cols: parseDimension(body.cols, 80, 10, 500),
|
|
2963
|
+
rows: parseDimension(body.rows, 24, 5, 200)
|
|
2964
|
+
};
|
|
2965
|
+
};
|
|
2966
|
+
var clearIdleTimer = (session) => {
|
|
2967
|
+
if (!session.idleTimer) return;
|
|
2968
|
+
clearTimeout(session.idleTimer);
|
|
2969
|
+
session.idleTimer = null;
|
|
2970
|
+
};
|
|
2971
|
+
var scheduleIdleCleanup = (sessionKey, session) => {
|
|
2972
|
+
clearIdleTimer(session);
|
|
2973
|
+
session.idleTimer = setTimeout(() => {
|
|
2974
|
+
if (session.clients.size > 0 || sessions2.get(sessionKey) !== session) return;
|
|
2975
|
+
sessions2.delete(sessionKey);
|
|
2976
|
+
try {
|
|
2977
|
+
session.pty.kill();
|
|
2978
|
+
} catch {
|
|
2979
|
+
}
|
|
2980
|
+
}, TERMINAL_IDLE_TTL_MS);
|
|
2981
|
+
session.idleTimer.unref();
|
|
2982
|
+
};
|
|
2983
|
+
var sendJson = (socket, value) => {
|
|
2984
|
+
if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify(value));
|
|
2985
|
+
};
|
|
2986
|
+
var LocalTerminalUnavailableError = class extends Error {
|
|
2987
|
+
};
|
|
2988
|
+
var serializeErrorMessage = (error) => error instanceof Error ? error.message : "Unknown error";
|
|
2989
|
+
var createTerminalSession = async (grant, nodePtyLoader) => {
|
|
2990
|
+
let nodePty;
|
|
2991
|
+
try {
|
|
2992
|
+
nodePty = await nodePtyLoader();
|
|
2993
|
+
} catch (error) {
|
|
2994
|
+
void writeLog(
|
|
2995
|
+
"error",
|
|
2996
|
+
`local terminal could not load node-pty: ${serializeErrorMessage(error)}`
|
|
2997
|
+
);
|
|
2998
|
+
throw new LocalTerminalUnavailableError(
|
|
2999
|
+
"Local terminal is unavailable because node-pty could not be loaded. The connector remains available for other capabilities."
|
|
3000
|
+
);
|
|
3001
|
+
}
|
|
3002
|
+
const shell = process.platform === "win32" ? "powershell.exe" : process.env.SHELL || "/bin/bash";
|
|
3003
|
+
const shellArgs = process.platform === "win32" ? [] : ["-l"];
|
|
3004
|
+
let pty;
|
|
3005
|
+
try {
|
|
3006
|
+
pty = nodePty.spawn(shell, shellArgs, {
|
|
3007
|
+
name: "xterm-256color",
|
|
3008
|
+
cols: grant.cols,
|
|
3009
|
+
rows: grant.rows,
|
|
3010
|
+
cwd: path8.resolve(grant.cwd),
|
|
3011
|
+
env: { ...process.env, TERM: "xterm-256color" }
|
|
3012
|
+
});
|
|
3013
|
+
} catch (error) {
|
|
3014
|
+
const message = serializeErrorMessage(error);
|
|
3015
|
+
void writeLog(
|
|
3016
|
+
"error",
|
|
3017
|
+
`local terminal failed to spawn shell=${shell} cwd=${grant.cwd}: ${message}`
|
|
3018
|
+
);
|
|
3019
|
+
throw new LocalTerminalUnavailableError(
|
|
3020
|
+
"Local terminal is unavailable because node-pty could not start a shell. The connector remains available for other capabilities."
|
|
3021
|
+
);
|
|
3022
|
+
}
|
|
3023
|
+
const session = {
|
|
3024
|
+
pty,
|
|
3025
|
+
clients: /* @__PURE__ */ new Set(),
|
|
3026
|
+
buffer: [],
|
|
3027
|
+
cols: grant.cols,
|
|
3028
|
+
rows: grant.rows,
|
|
3029
|
+
idleTimer: null
|
|
3030
|
+
};
|
|
3031
|
+
sessions2.set(grant.sessionKey, session);
|
|
3032
|
+
pty.onData((data) => {
|
|
3033
|
+
session.buffer.push(data);
|
|
3034
|
+
if (session.buffer.length > MAX_BUFFER_CHUNKS) session.buffer.shift();
|
|
3035
|
+
for (const socket of session.clients) {
|
|
3036
|
+
if (socket.readyState === WebSocket.OPEN) socket.send(data);
|
|
3037
|
+
}
|
|
3038
|
+
});
|
|
3039
|
+
pty.onExit(({ exitCode }) => {
|
|
3040
|
+
if (sessions2.get(grant.sessionKey) === session) sessions2.delete(grant.sessionKey);
|
|
3041
|
+
clearIdleTimer(session);
|
|
3042
|
+
for (const socket of session.clients) {
|
|
3043
|
+
sendJson(socket, { type: "status", message: `exited (code ${exitCode})` });
|
|
3044
|
+
socket.close(1e3, "Terminal exited");
|
|
3045
|
+
}
|
|
3046
|
+
session.clients.clear();
|
|
3047
|
+
});
|
|
3048
|
+
return session;
|
|
3049
|
+
};
|
|
3050
|
+
var attachLocalTerminal = async (socket, grant, nodePtyLoader) => {
|
|
3051
|
+
const pendingMessages = [];
|
|
3052
|
+
let session;
|
|
3053
|
+
let started = false;
|
|
3054
|
+
const handleMessage = (data) => {
|
|
3055
|
+
if (!session) {
|
|
3056
|
+
pendingMessages.push(data);
|
|
3057
|
+
return;
|
|
3058
|
+
}
|
|
3059
|
+
try {
|
|
3060
|
+
const message = JSON.parse(data);
|
|
3061
|
+
if (message.type === "bridge-start") {
|
|
3062
|
+
if (!started) {
|
|
3063
|
+
started = true;
|
|
3064
|
+
sendJson(socket, { type: "status", message: "connected" });
|
|
3065
|
+
for (const chunk of session.buffer) socket.send(chunk);
|
|
3066
|
+
}
|
|
3067
|
+
return;
|
|
3068
|
+
}
|
|
3069
|
+
if (message.type === "input" && typeof message.data === "string") {
|
|
3070
|
+
session.pty.write(message.data);
|
|
3071
|
+
return;
|
|
3072
|
+
}
|
|
3073
|
+
if (message.type === "resize") {
|
|
3074
|
+
const cols = parseDimension(message.cols, session.cols, 10, 500);
|
|
3075
|
+
const rows = parseDimension(message.rows, session.rows, 5, 200);
|
|
3076
|
+
if (cols !== session.cols || rows !== session.rows) {
|
|
3077
|
+
session.cols = cols;
|
|
3078
|
+
session.rows = rows;
|
|
3079
|
+
session.pty.resize(cols, rows);
|
|
3080
|
+
}
|
|
3081
|
+
return;
|
|
3082
|
+
}
|
|
3083
|
+
} catch {
|
|
3084
|
+
}
|
|
3085
|
+
session.pty.write(data);
|
|
3086
|
+
};
|
|
3087
|
+
socket.on("message", (raw) => handleMessage(raw.toString()));
|
|
3088
|
+
session = sessions2.get(grant.sessionKey);
|
|
3089
|
+
if (!session) session = await createTerminalSession(grant, nodePtyLoader);
|
|
3090
|
+
else clearIdleTimer(session);
|
|
3091
|
+
session.clients.add(socket);
|
|
3092
|
+
for (const data of pendingMessages.splice(0)) handleMessage(data);
|
|
3093
|
+
const detach = () => {
|
|
3094
|
+
if (!session) return;
|
|
3095
|
+
session.clients.delete(socket);
|
|
3096
|
+
if (session.clients.size === 0) scheduleIdleCleanup(grant.sessionKey, session);
|
|
3097
|
+
};
|
|
3098
|
+
socket.once("close", detach);
|
|
3099
|
+
socket.once("error", detach);
|
|
3100
|
+
};
|
|
3101
|
+
var installConnectorTerminalEndpoint = (server, options = {}) => {
|
|
3102
|
+
if (installedServers.has(server)) return;
|
|
3103
|
+
installedServers.add(server);
|
|
3104
|
+
const terminalServer = new WebSocketServer({ noServer: true });
|
|
3105
|
+
server.on("upgrade", (request, socket, head) => {
|
|
3106
|
+
const url = new URL(request.url ?? "/", "http://connector.local");
|
|
3107
|
+
if (url.pathname !== "/api/terminal") return;
|
|
3108
|
+
const tokenHeader = request.headers["x-buda-local-terminal-token"];
|
|
3109
|
+
const token = Array.isArray(tokenHeader) ? tokenHeader[0] : tokenHeader;
|
|
3110
|
+
const grant = token ? localGrants.get(token) : void 0;
|
|
3111
|
+
if (!token || !grant || grant.expiresAt <= Date.now()) {
|
|
3112
|
+
if (token) localGrants.delete(token);
|
|
3113
|
+
socket.destroy();
|
|
3114
|
+
return;
|
|
3115
|
+
}
|
|
3116
|
+
localGrants.delete(token);
|
|
3117
|
+
terminalServer.handleUpgrade(request, socket, head, (client) => {
|
|
3118
|
+
void attachLocalTerminal(client, grant, options.nodePtyLoader ?? loadNodePty).catch(
|
|
3119
|
+
(error) => {
|
|
3120
|
+
const message = error instanceof LocalTerminalUnavailableError ? error.message : "Local terminal is unavailable. The connector remains available for other capabilities.";
|
|
3121
|
+
sendJson(client, { type: "error", fatal: true, message });
|
|
3122
|
+
client.close(LOCAL_TERMINAL_CLOSE_SPAWN_FAILED, "Local terminal unavailable");
|
|
3123
|
+
}
|
|
3124
|
+
);
|
|
3125
|
+
});
|
|
3126
|
+
});
|
|
3127
|
+
server.once("close", () => {
|
|
3128
|
+
for (const client of terminalServer.clients) client.terminate();
|
|
3129
|
+
terminalServer.close();
|
|
3130
|
+
localGrants.clear();
|
|
3131
|
+
for (const session of sessions2.values()) {
|
|
3132
|
+
clearIdleTimer(session);
|
|
3133
|
+
try {
|
|
3134
|
+
session.pty.kill();
|
|
3135
|
+
} catch {
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
sessions2.clear();
|
|
3139
|
+
});
|
|
3140
|
+
};
|
|
3141
|
+
var registerLocalTerminal = (input) => {
|
|
3142
|
+
if (!isSafeId(input.workspaceKey) || !isSafeId(input.tabId)) {
|
|
3143
|
+
throw new Error("Invalid local terminal workspace session.");
|
|
3144
|
+
}
|
|
3145
|
+
const token = randomBytes3(32).toString("base64url");
|
|
3146
|
+
localGrants.set(token, {
|
|
3147
|
+
sessionKey: `${input.workspaceKey}:${input.tabId}`,
|
|
3148
|
+
workspaceKey: input.workspaceKey,
|
|
3149
|
+
cwd: input.cwd,
|
|
3150
|
+
cols: input.cols,
|
|
3151
|
+
rows: input.rows,
|
|
3152
|
+
expiresAt: Date.now() + LOCAL_TERMINAL_GRANT_TTL_MS
|
|
3153
|
+
});
|
|
3154
|
+
const cleanupTimer = setTimeout(() => localGrants.delete(token), LOCAL_TERMINAL_GRANT_TTL_MS);
|
|
3155
|
+
cleanupTimer.unref();
|
|
3156
|
+
return token;
|
|
3157
|
+
};
|
|
3158
|
+
var toLocalTerminalUrl = (baseUrl) => {
|
|
3159
|
+
const url = new URL("/api/terminal", baseUrl);
|
|
3160
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
3161
|
+
return url.toString();
|
|
3162
|
+
};
|
|
3163
|
+
var connectSocket = async (socket) => new Promise((resolve, reject) => {
|
|
3164
|
+
const onOpen = () => {
|
|
3165
|
+
socket.removeListener("error", onError);
|
|
3166
|
+
resolve();
|
|
3167
|
+
};
|
|
3168
|
+
const onError = (error) => {
|
|
3169
|
+
socket.removeListener("open", onOpen);
|
|
3170
|
+
reject(error);
|
|
3171
|
+
};
|
|
3172
|
+
socket.once("open", onOpen);
|
|
3173
|
+
socket.once("error", onError);
|
|
3174
|
+
});
|
|
3175
|
+
var proxyConnectorTerminalTunnel = async (input) => {
|
|
3176
|
+
const cloud = new WebSocket(
|
|
3177
|
+
getConnectorTerminalUrl(input.serverUrl, input.connectorId, input.sessionId),
|
|
3178
|
+
{
|
|
3179
|
+
headers: { "X-Buda-Terminal-Token": input.cloudToken },
|
|
3180
|
+
handshakeTimeout: TERMINAL_HANDSHAKE_TIMEOUT_MS
|
|
3181
|
+
}
|
|
3182
|
+
);
|
|
3183
|
+
const local = new WebSocket(toLocalTerminalUrl(input.localBaseUrl), {
|
|
3184
|
+
headers: { "X-Buda-Local-Terminal-Token": input.localToken },
|
|
3185
|
+
handshakeTimeout: TERMINAL_HANDSHAKE_TIMEOUT_MS
|
|
3186
|
+
});
|
|
3187
|
+
try {
|
|
3188
|
+
await Promise.all([connectSocket(cloud), connectSocket(local)]);
|
|
3189
|
+
} catch (error) {
|
|
3190
|
+
cloud.terminate();
|
|
3191
|
+
local.terminate();
|
|
3192
|
+
throw error;
|
|
3193
|
+
}
|
|
3194
|
+
let bridgeAccepted = false;
|
|
3195
|
+
let activityEnded = false;
|
|
3196
|
+
const endActivity = () => {
|
|
3197
|
+
if (activityEnded) return;
|
|
3198
|
+
activityEnded = true;
|
|
3199
|
+
input.onActivityEnd?.();
|
|
1900
3200
|
};
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
3201
|
+
let pendingBytes = 0;
|
|
3202
|
+
const pendingCloudFrames = [];
|
|
3203
|
+
const pendingLocalFrames = [];
|
|
3204
|
+
const bufferFrame = (frames, data, isBinary) => {
|
|
3205
|
+
const bytes = Array.isArray(data) ? data.reduce((total, chunk) => total + chunk.byteLength, 0) : data.byteLength;
|
|
3206
|
+
if (pendingBytes + bytes > MAX_TERMINAL_FRAME_BYTES) {
|
|
3207
|
+
cloud.close(1009, "Terminal handshake buffer too large");
|
|
3208
|
+
local.close(1009, "Terminal handshake buffer too large");
|
|
3209
|
+
return false;
|
|
3210
|
+
}
|
|
3211
|
+
pendingBytes += bytes;
|
|
3212
|
+
frames.push([data, isBinary]);
|
|
3213
|
+
return true;
|
|
3214
|
+
};
|
|
3215
|
+
cloud.on("message", (data, isBinary) => {
|
|
3216
|
+
if (!bridgeAccepted) {
|
|
3217
|
+
if (!isBinary && data.toString() === '{"type":"bridge-accepted"}') {
|
|
3218
|
+
bridgeAccepted = true;
|
|
3219
|
+
local.send(JSON.stringify({ type: "bridge-start" }));
|
|
3220
|
+
for (const [frame, binary] of pendingCloudFrames) {
|
|
3221
|
+
if (local.readyState === WebSocket.OPEN) local.send(frame, { binary });
|
|
3222
|
+
}
|
|
3223
|
+
for (const [frame, binary] of pendingLocalFrames) {
|
|
3224
|
+
if (cloud.readyState === WebSocket.OPEN) cloud.send(frame, { binary });
|
|
3225
|
+
}
|
|
3226
|
+
pendingCloudFrames.length = 0;
|
|
3227
|
+
pendingLocalFrames.length = 0;
|
|
3228
|
+
pendingBytes = 0;
|
|
3229
|
+
return;
|
|
3230
|
+
}
|
|
3231
|
+
bufferFrame(pendingCloudFrames, data, isBinary);
|
|
3232
|
+
return;
|
|
3233
|
+
}
|
|
3234
|
+
if (local.readyState === WebSocket.OPEN) local.send(data, { binary: isBinary });
|
|
1914
3235
|
});
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
try {
|
|
1920
|
-
const payload = JSON.parse(text2);
|
|
1921
|
-
errorCode = typeof payload.code === "string" ? payload.code : void 0;
|
|
1922
|
-
errorMessage3 = typeof payload.error === "string" ? payload.error : text2;
|
|
1923
|
-
} catch {
|
|
3236
|
+
local.on("message", (data, isBinary) => {
|
|
3237
|
+
if (!bridgeAccepted) {
|
|
3238
|
+
bufferFrame(pendingLocalFrames, data, isBinary);
|
|
3239
|
+
return;
|
|
1924
3240
|
}
|
|
1925
|
-
if (
|
|
1926
|
-
|
|
1927
|
-
|
|
3241
|
+
if (cloud.readyState === WebSocket.OPEN) cloud.send(data, { binary: isBinary });
|
|
3242
|
+
});
|
|
3243
|
+
cloud.once("close", (code, reason) => {
|
|
3244
|
+
endActivity();
|
|
3245
|
+
if (!bridgeAccepted) {
|
|
3246
|
+
void writeLog(
|
|
3247
|
+
"warn",
|
|
3248
|
+
`terminal cloud tunnel closed before readiness session=${input.sessionId} code=${code} reason=${truncateTerminalCloseReason(reason.toString())}`
|
|
1928
3249
|
);
|
|
1929
3250
|
}
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
}
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
);
|
|
1943
|
-
}
|
|
1944
|
-
async function registerConnector(config, launchConfig, bunnyDaemon, options) {
|
|
1945
|
-
const result = await postJson(
|
|
1946
|
-
config.serverUrl,
|
|
1947
|
-
"/api/connectors/register",
|
|
1948
|
-
await buildPayload(config, {
|
|
1949
|
-
bunnyDaemon,
|
|
1950
|
-
...options?.recoverMissingProfile ? { recovery: "missing_local_profile" } : {}
|
|
1951
|
-
}),
|
|
1952
|
-
config,
|
|
1953
|
-
launchConfig
|
|
1954
|
-
);
|
|
3251
|
+
if (local.readyState === WebSocket.OPEN) {
|
|
3252
|
+
local.close(normalizeTerminalCloseCode(code), truncateTerminalCloseReason(reason.toString()));
|
|
3253
|
+
}
|
|
3254
|
+
});
|
|
3255
|
+
local.once("close", (code, reason) => {
|
|
3256
|
+
endActivity();
|
|
3257
|
+
if (cloud.readyState === WebSocket.OPEN) {
|
|
3258
|
+
cloud.close(normalizeTerminalCloseCode(code), truncateTerminalCloseReason(reason.toString()));
|
|
3259
|
+
}
|
|
3260
|
+
});
|
|
3261
|
+
cloud.once("error", () => local.terminate());
|
|
3262
|
+
local.once("error", () => cloud.terminate());
|
|
3263
|
+
input.onActivityStart?.();
|
|
3264
|
+
cloud.send(JSON.stringify({ type: "bridge-ready" }));
|
|
1955
3265
|
await writeLog(
|
|
1956
3266
|
"info",
|
|
1957
|
-
`
|
|
3267
|
+
`terminal proxy connected session=${input.sessionId} workspace=${input.workspaceKey}`
|
|
1958
3268
|
);
|
|
1959
|
-
|
|
3269
|
+
};
|
|
3270
|
+
|
|
3271
|
+
// src/bunny-daemon.ts
|
|
3272
|
+
async function startEmbeddedBunnyDaemon(config, launchConfig) {
|
|
3273
|
+
const host = getBunnyDaemonHost(launchConfig);
|
|
3274
|
+
const port = getBunnyDaemonPort(launchConfig);
|
|
3275
|
+
const root = config.workdirRoot;
|
|
3276
|
+
await mkdir7(root, { recursive: true });
|
|
3277
|
+
const server = createDaemon({ host, port, root });
|
|
3278
|
+
installConnectorTerminalEndpoint(server);
|
|
3279
|
+
await new Promise((resolve, reject) => {
|
|
3280
|
+
const onError = (error) => {
|
|
3281
|
+
server.off("listening", onListening);
|
|
3282
|
+
reject(error);
|
|
3283
|
+
};
|
|
3284
|
+
const onListening = () => {
|
|
3285
|
+
server.off("error", onError);
|
|
3286
|
+
resolve();
|
|
3287
|
+
};
|
|
3288
|
+
server.once("error", onError);
|
|
3289
|
+
server.once("listening", onListening);
|
|
3290
|
+
server.listen(port, host);
|
|
3291
|
+
});
|
|
3292
|
+
const address = server.address();
|
|
3293
|
+
const actualPort = typeof address === "object" && address ? address.port : port;
|
|
3294
|
+
const baseUrl = `http://${host}:${actualPort}`;
|
|
3295
|
+
await writeLog("info", `bunny daemon listening ${baseUrl}`, {
|
|
3296
|
+
root
|
|
3297
|
+
});
|
|
1960
3298
|
return {
|
|
1961
|
-
|
|
1962
|
-
|
|
3299
|
+
server,
|
|
3300
|
+
host,
|
|
3301
|
+
port: actualPort,
|
|
3302
|
+
baseUrl,
|
|
3303
|
+
root
|
|
1963
3304
|
};
|
|
1964
3305
|
}
|
|
1965
|
-
async function
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
config.serverUrl,
|
|
1969
|
-
"/api/connectors/heartbeat",
|
|
1970
|
-
await buildPayload(config, {
|
|
1971
|
-
bunnyDaemon,
|
|
1972
|
-
logs
|
|
1973
|
-
}),
|
|
1974
|
-
config,
|
|
1975
|
-
launchConfig
|
|
1976
|
-
).catch((error) => {
|
|
1977
|
-
restorePendingLogs(logs);
|
|
1978
|
-
throw error;
|
|
1979
|
-
});
|
|
1980
|
-
return { relayProtocol: result.relayProtocol };
|
|
1981
|
-
}
|
|
1982
|
-
async function unregisterConnector(config, launchConfig) {
|
|
1983
|
-
await postJson(
|
|
1984
|
-
config.serverUrl,
|
|
1985
|
-
"/api/connectors/unregister",
|
|
1986
|
-
{
|
|
1987
|
-
connectorId: config.connectorId
|
|
1988
|
-
},
|
|
1989
|
-
config,
|
|
1990
|
-
launchConfig
|
|
1991
|
-
).catch((error) => {
|
|
1992
|
-
void writeLog("warn", `unregister failed: ${error instanceof Error ? error.message : error}`);
|
|
3306
|
+
async function stopEmbeddedBunnyDaemon(daemon) {
|
|
3307
|
+
await new Promise((resolve) => {
|
|
3308
|
+
daemon.server.close(() => resolve());
|
|
1993
3309
|
});
|
|
3310
|
+
await writeLog("info", `bunny daemon stopped ${daemon.baseUrl}`);
|
|
1994
3311
|
}
|
|
1995
3312
|
|
|
1996
3313
|
// src/legacy-workspace-migration.ts
|
|
1997
|
-
import { createHash as createHash5, randomUUID as
|
|
3314
|
+
import { createHash as createHash5, randomUUID as randomUUID5 } from "crypto";
|
|
1998
3315
|
import { constants as constants2, createReadStream } from "fs";
|
|
1999
3316
|
import {
|
|
2000
|
-
chmod as
|
|
3317
|
+
chmod as chmod4,
|
|
2001
3318
|
copyFile,
|
|
2002
3319
|
lstat,
|
|
2003
|
-
mkdir as
|
|
3320
|
+
mkdir as mkdir8,
|
|
2004
3321
|
open as open2,
|
|
2005
|
-
readdir as
|
|
2006
|
-
readFile as
|
|
3322
|
+
readdir as readdir4,
|
|
3323
|
+
readFile as readFile7,
|
|
2007
3324
|
readlink,
|
|
2008
3325
|
realpath,
|
|
2009
|
-
rename as
|
|
2010
|
-
rm,
|
|
3326
|
+
rename as rename5,
|
|
3327
|
+
rm as rm2,
|
|
2011
3328
|
symlink,
|
|
2012
|
-
unlink as
|
|
3329
|
+
unlink as unlink5
|
|
2013
3330
|
} from "fs/promises";
|
|
2014
|
-
import
|
|
3331
|
+
import path9 from "path";
|
|
2015
3332
|
var MIGRATION_VERSION = 1;
|
|
2016
|
-
var MIGRATION_DIRECTORY =
|
|
3333
|
+
var MIGRATION_DIRECTORY = path9.join(".buda", "migrations", "legacy-workspace-v1");
|
|
2017
3334
|
var JOURNAL_FILENAME = "journal.json";
|
|
2018
3335
|
var IMMUTABLE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
2019
3336
|
var LEGACY_TEMPLATE_FILES = [
|
|
@@ -2049,7 +3366,7 @@ var LegacyWorkspaceMigrationError = class extends Error {
|
|
|
2049
3366
|
recoverable = true;
|
|
2050
3367
|
};
|
|
2051
3368
|
var migrateLegacyWorkspaceRoot = async (storageRoot, options = {}) => {
|
|
2052
|
-
await
|
|
3369
|
+
await mkdir8(storageRoot, { recursive: true, mode: 448 });
|
|
2053
3370
|
const rootInfo = await lstat(storageRoot);
|
|
2054
3371
|
if (rootInfo.isSymbolicLink() || !rootInfo.isDirectory()) {
|
|
2055
3372
|
throw new LegacyWorkspaceMigrationError(
|
|
@@ -2058,9 +3375,9 @@ var migrateLegacyWorkspaceRoot = async (storageRoot, options = {}) => {
|
|
|
2058
3375
|
);
|
|
2059
3376
|
}
|
|
2060
3377
|
const canonicalRoot = await realpath(storageRoot);
|
|
2061
|
-
const migrationDirectory =
|
|
3378
|
+
const migrationDirectory = path9.join(canonicalRoot, MIGRATION_DIRECTORY);
|
|
2062
3379
|
await ensurePrivateDirectoryWithin(canonicalRoot, migrationDirectory);
|
|
2063
|
-
const journalPath =
|
|
3380
|
+
const journalPath = path9.join(migrationDirectory, JOURNAL_FILENAME);
|
|
2064
3381
|
let journal = await readJournal(journalPath);
|
|
2065
3382
|
if (journal) validateJournalPaths(journal, canonicalRoot, migrationDirectory);
|
|
2066
3383
|
if (journal?.status === "complete") {
|
|
@@ -2093,8 +3410,8 @@ var migrateLegacyWorkspaceRoot = async (storageRoot, options = {}) => {
|
|
|
2093
3410
|
};
|
|
2094
3411
|
var migrateItem = async (storageRoot, journalPath, journal, item, options) => {
|
|
2095
3412
|
if (item.phase === "committed") return;
|
|
2096
|
-
if (await
|
|
2097
|
-
if (!await
|
|
3413
|
+
if (await exists2(item.target)) {
|
|
3414
|
+
if (!await exists2(item.staging) && ["verified", "copied", "copying"].includes(item.phase)) {
|
|
2098
3415
|
await verifyTree(item.source, item.target);
|
|
2099
3416
|
item.phase = "committed";
|
|
2100
3417
|
await writeJournal(journalPath, journal);
|
|
@@ -2109,8 +3426,8 @@ var migrateItem = async (storageRoot, journalPath, journal, item, options) => {
|
|
|
2109
3426
|
item.phase = "copying";
|
|
2110
3427
|
await writeJournal(journalPath, journal);
|
|
2111
3428
|
await checkpoint(options, "before-copy", item.id);
|
|
2112
|
-
await
|
|
2113
|
-
await ensurePrivateDirectoryWithin(storageRoot,
|
|
3429
|
+
await rm2(item.staging, { recursive: true, force: true });
|
|
3430
|
+
await ensurePrivateDirectoryWithin(storageRoot, path9.dirname(item.staging));
|
|
2114
3431
|
await copyTree(item.source, item.staging, () => checkpoint(options, "during-copy", item.id));
|
|
2115
3432
|
await checkpoint(options, "after-copy", item.id);
|
|
2116
3433
|
item.phase = "copied";
|
|
@@ -2121,7 +3438,7 @@ var migrateItem = async (storageRoot, journalPath, journal, item, options) => {
|
|
|
2121
3438
|
try {
|
|
2122
3439
|
await verifyTree(item.source, item.staging);
|
|
2123
3440
|
} catch (error) {
|
|
2124
|
-
await
|
|
3441
|
+
await rm2(item.staging, { recursive: true, force: true });
|
|
2125
3442
|
item.phase = "copying";
|
|
2126
3443
|
await writeJournal(journalPath, journal);
|
|
2127
3444
|
throw error;
|
|
@@ -2132,9 +3449,9 @@ var migrateItem = async (storageRoot, journalPath, journal, item, options) => {
|
|
|
2132
3449
|
}
|
|
2133
3450
|
if (item.phase === "verified") {
|
|
2134
3451
|
await checkpoint(options, "before-rename", item.id);
|
|
2135
|
-
await ensurePrivateDirectoryWithin(storageRoot,
|
|
2136
|
-
await
|
|
2137
|
-
await
|
|
3452
|
+
await ensurePrivateDirectoryWithin(storageRoot, path9.dirname(item.target));
|
|
3453
|
+
await rename5(item.staging, item.target);
|
|
3454
|
+
await syncDirectory2(path9.dirname(item.target));
|
|
2138
3455
|
await checkpoint(options, "after-rename", item.id);
|
|
2139
3456
|
item.phase = "committed";
|
|
2140
3457
|
await writeJournal(journalPath, journal);
|
|
@@ -2144,14 +3461,14 @@ var resumeRollback = async (journalPath, journal, options) => {
|
|
|
2144
3461
|
for (const item of [...journal.items].reverse()) {
|
|
2145
3462
|
if (item.phase === "rolled-back") continue;
|
|
2146
3463
|
await checkpoint(options, "before-rollback", item.id);
|
|
2147
|
-
if (!await
|
|
3464
|
+
if (!await exists2(item.source)) {
|
|
2148
3465
|
throw new LegacyWorkspaceMigrationError(
|
|
2149
3466
|
"MIGRATION_VERIFY_FAILED",
|
|
2150
3467
|
`Cannot roll back ${item.id}: the preserved legacy source is missing`
|
|
2151
3468
|
);
|
|
2152
3469
|
}
|
|
2153
|
-
await
|
|
2154
|
-
await
|
|
3470
|
+
await rm2(item.staging, { recursive: true, force: true });
|
|
3471
|
+
await rm2(item.target, { recursive: true, force: true });
|
|
2155
3472
|
await checkpoint(options, "after-rollback", item.id);
|
|
2156
3473
|
item.phase = "rolled-back";
|
|
2157
3474
|
await writeJournal(journalPath, journal);
|
|
@@ -2165,12 +3482,12 @@ var createJournal = async (storageRoot, migrationDirectory, owner) => {
|
|
|
2165
3482
|
const ownership = owner ? "explicit-owner" : "shared-legacy";
|
|
2166
3483
|
const label = owner ? `Legacy Connector root assigned to ${owner.spaceId}/${owner.agentId}` : "Shared legacy Connector root (ownership unassigned)";
|
|
2167
3484
|
const sources = await inventoryLegacySources(storageRoot);
|
|
2168
|
-
const stagingRoot =
|
|
3485
|
+
const stagingRoot = path9.join(migrationDirectory, "staging", randomUUID5());
|
|
2169
3486
|
const items = sources.map(({ id, source }) => ({
|
|
2170
3487
|
id,
|
|
2171
3488
|
source,
|
|
2172
3489
|
target: deriveTarget(storageRoot, id, owner),
|
|
2173
|
-
staging:
|
|
3490
|
+
staging: path9.join(stagingRoot, id),
|
|
2174
3491
|
phase: "pending"
|
|
2175
3492
|
}));
|
|
2176
3493
|
assertDistinctTargets(items);
|
|
@@ -2187,29 +3504,29 @@ var createJournal = async (storageRoot, migrationDirectory, owner) => {
|
|
|
2187
3504
|
};
|
|
2188
3505
|
};
|
|
2189
3506
|
var inventoryLegacySources = async (storageRoot) => {
|
|
2190
|
-
const rootEntries = await
|
|
3507
|
+
const rootEntries = await readdir4(storageRoot, { withFileTypes: true });
|
|
2191
3508
|
const templateNames = new Set(LEGACY_TEMPLATE_FILES);
|
|
2192
3509
|
for (const entry of rootEntries) {
|
|
2193
3510
|
if (entry.name.endsWith(".md")) templateNames.add(entry.name);
|
|
2194
3511
|
}
|
|
2195
3512
|
const candidates = [
|
|
2196
|
-
{ id: "agent", source:
|
|
2197
|
-
{ id: "space", source:
|
|
2198
|
-
{ id: "logs", source:
|
|
2199
|
-
{ id: "connector-log", source:
|
|
2200
|
-
{ id: "runtime-config", source:
|
|
2201
|
-
{ id: "runtime-config-hidden", source:
|
|
2202
|
-
{ id: "runtime-config-file", source:
|
|
2203
|
-
{ id: "runtime-config-hidden-file", source:
|
|
2204
|
-
{ id: "bunny-runtime", source:
|
|
3513
|
+
{ id: "agent", source: path9.join(storageRoot, "agent") },
|
|
3514
|
+
{ id: "space", source: path9.join(storageRoot, "space") },
|
|
3515
|
+
{ id: "logs", source: path9.join(storageRoot, "logs") },
|
|
3516
|
+
{ id: "connector-log", source: path9.join(storageRoot, "connector.log") },
|
|
3517
|
+
{ id: "runtime-config", source: path9.join(storageRoot, "runtime-config") },
|
|
3518
|
+
{ id: "runtime-config-hidden", source: path9.join(storageRoot, ".runtime-config") },
|
|
3519
|
+
{ id: "runtime-config-file", source: path9.join(storageRoot, "runtime-config.json") },
|
|
3520
|
+
{ id: "runtime-config-hidden-file", source: path9.join(storageRoot, ".runtime-config.json") },
|
|
3521
|
+
{ id: "bunny-runtime", source: path9.join(storageRoot, ".bunny") },
|
|
2205
3522
|
...[...templateNames].sort().map((name) => ({
|
|
2206
3523
|
id: `template-${name}`,
|
|
2207
|
-
source:
|
|
3524
|
+
source: path9.join(storageRoot, name)
|
|
2208
3525
|
}))
|
|
2209
3526
|
];
|
|
2210
3527
|
const found = [];
|
|
2211
3528
|
for (const candidate of candidates) {
|
|
2212
|
-
if (!await
|
|
3529
|
+
if (!await exists2(candidate.source)) continue;
|
|
2213
3530
|
const info = await lstat(candidate.source);
|
|
2214
3531
|
if (info.isSymbolicLink()) {
|
|
2215
3532
|
throw new LegacyWorkspaceMigrationError(
|
|
@@ -2217,36 +3534,36 @@ var inventoryLegacySources = async (storageRoot) => {
|
|
|
2217
3534
|
`Legacy migration source ${candidate.id} must not be a symbolic link`
|
|
2218
3535
|
);
|
|
2219
3536
|
}
|
|
2220
|
-
if (info.isDirectory() && (await
|
|
3537
|
+
if (info.isDirectory() && (await readdir4(candidate.source)).length === 0) continue;
|
|
2221
3538
|
found.push(candidate);
|
|
2222
3539
|
}
|
|
2223
3540
|
return found;
|
|
2224
3541
|
};
|
|
2225
3542
|
var deriveTarget = (storageRoot, id, owner) => {
|
|
2226
|
-
if (!owner) return
|
|
2227
|
-
const spaceRoot =
|
|
2228
|
-
const agentRoot =
|
|
2229
|
-
if (id === "agent") return
|
|
2230
|
-
if (id === "space") return
|
|
3543
|
+
if (!owner) return path9.join(storageRoot, ".buda", "legacy", "shared", id);
|
|
3544
|
+
const spaceRoot = path9.join(storageRoot, ".buda", "spaces", owner.spaceId);
|
|
3545
|
+
const agentRoot = path9.join(spaceRoot, "agents", owner.agentId);
|
|
3546
|
+
if (id === "agent") return path9.join(agentRoot, "workspace", "legacy-connector-root");
|
|
3547
|
+
if (id === "space") return path9.join(spaceRoot, "shared", "legacy-connector-root");
|
|
2231
3548
|
if (id.startsWith("template-")) {
|
|
2232
|
-
return
|
|
3549
|
+
return path9.join(spaceRoot, "shared", "legacy-templates", id.slice("template-".length));
|
|
2233
3550
|
}
|
|
2234
|
-
return
|
|
3551
|
+
return path9.join(agentRoot, "state", "legacy-connector-root", id);
|
|
2235
3552
|
};
|
|
2236
3553
|
var copyTree = async (source, destination, onEntryCopied) => {
|
|
2237
3554
|
const info = await lstat(source);
|
|
2238
3555
|
if (info.isDirectory()) {
|
|
2239
|
-
await
|
|
2240
|
-
const entries = (await
|
|
3556
|
+
await mkdir8(destination, { mode: info.mode & 4095 });
|
|
3557
|
+
const entries = (await readdir4(source)).sort();
|
|
2241
3558
|
for (const entry of entries) {
|
|
2242
|
-
await copyTree(
|
|
3559
|
+
await copyTree(path9.join(source, entry), path9.join(destination, entry), onEntryCopied);
|
|
2243
3560
|
}
|
|
2244
|
-
await
|
|
3561
|
+
await chmod4(destination, info.mode & 4095);
|
|
2245
3562
|
return;
|
|
2246
3563
|
}
|
|
2247
3564
|
if (info.isFile()) {
|
|
2248
3565
|
await copyFile(source, destination, constants2.COPYFILE_EXCL);
|
|
2249
|
-
await
|
|
3566
|
+
await chmod4(destination, info.mode & 4095);
|
|
2250
3567
|
await onEntryCopied();
|
|
2251
3568
|
return;
|
|
2252
3569
|
}
|
|
@@ -2274,8 +3591,8 @@ var verifyTree = async (source, destination) => {
|
|
|
2274
3591
|
}
|
|
2275
3592
|
if (sourceInfo.isDirectory()) {
|
|
2276
3593
|
const [sourceEntries, destinationEntries] = await Promise.all([
|
|
2277
|
-
|
|
2278
|
-
|
|
3594
|
+
readdir4(source),
|
|
3595
|
+
readdir4(destination)
|
|
2279
3596
|
]);
|
|
2280
3597
|
sourceEntries.sort();
|
|
2281
3598
|
destinationEntries.sort();
|
|
@@ -2283,7 +3600,7 @@ var verifyTree = async (source, destination) => {
|
|
|
2283
3600
|
throw verifyFailure("directory entries differ");
|
|
2284
3601
|
}
|
|
2285
3602
|
for (const entry of sourceEntries) {
|
|
2286
|
-
await verifyTree(
|
|
3603
|
+
await verifyTree(path9.join(source, entry), path9.join(destination, entry));
|
|
2287
3604
|
}
|
|
2288
3605
|
return;
|
|
2289
3606
|
}
|
|
@@ -2313,7 +3630,7 @@ var hashFile = async (filePath) => {
|
|
|
2313
3630
|
var readJournal = async (journalPath) => {
|
|
2314
3631
|
let raw;
|
|
2315
3632
|
try {
|
|
2316
|
-
raw = await
|
|
3633
|
+
raw = await readFile7(journalPath, "utf8");
|
|
2317
3634
|
} catch (error) {
|
|
2318
3635
|
if (isMissing(error)) return void 0;
|
|
2319
3636
|
throw error;
|
|
@@ -2325,13 +3642,13 @@ var readJournal = async (journalPath) => {
|
|
|
2325
3642
|
throw corrupt("Legacy migration journal contains invalid JSON");
|
|
2326
3643
|
}
|
|
2327
3644
|
if (!isJournal(value)) throw corrupt("Legacy migration journal is malformed");
|
|
2328
|
-
await
|
|
3645
|
+
await chmod4(journalPath, 384);
|
|
2329
3646
|
return value;
|
|
2330
3647
|
};
|
|
2331
3648
|
var writeJournal = async (journalPath, journal) => {
|
|
2332
3649
|
journal.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2333
|
-
await ensurePrivateDirectory(
|
|
2334
|
-
const temporaryPath = `${journalPath}.${process.pid}.${
|
|
3650
|
+
await ensurePrivateDirectory(path9.dirname(journalPath));
|
|
3651
|
+
const temporaryPath = `${journalPath}.${process.pid}.${randomUUID5()}.tmp`;
|
|
2335
3652
|
const handle = await open2(
|
|
2336
3653
|
temporaryPath,
|
|
2337
3654
|
constants2.O_CREAT | constants2.O_EXCL | constants2.O_WRONLY,
|
|
@@ -2345,14 +3662,14 @@ var writeJournal = async (journalPath, journal) => {
|
|
|
2345
3662
|
await handle.close();
|
|
2346
3663
|
}
|
|
2347
3664
|
try {
|
|
2348
|
-
await
|
|
2349
|
-
await
|
|
2350
|
-
await
|
|
3665
|
+
await rename5(temporaryPath, journalPath);
|
|
3666
|
+
await chmod4(journalPath, 384);
|
|
3667
|
+
await syncDirectory2(path9.dirname(journalPath));
|
|
2351
3668
|
} finally {
|
|
2352
|
-
await
|
|
3669
|
+
await unlink5(temporaryPath).catch(() => void 0);
|
|
2353
3670
|
}
|
|
2354
3671
|
};
|
|
2355
|
-
var
|
|
3672
|
+
var syncDirectory2 = async (directory) => {
|
|
2356
3673
|
const handle = await open2(directory, constants2.O_RDONLY);
|
|
2357
3674
|
try {
|
|
2358
3675
|
await handle.sync();
|
|
@@ -2361,7 +3678,7 @@ var syncDirectory = async (directory) => {
|
|
|
2361
3678
|
}
|
|
2362
3679
|
};
|
|
2363
3680
|
var ensurePrivateDirectory = async (directory) => {
|
|
2364
|
-
await
|
|
3681
|
+
await mkdir8(directory, { mode: 448 }).catch((error) => {
|
|
2365
3682
|
if (!isAlreadyExists2(error)) throw error;
|
|
2366
3683
|
});
|
|
2367
3684
|
const info = await lstat(directory);
|
|
@@ -2371,16 +3688,16 @@ var ensurePrivateDirectory = async (directory) => {
|
|
|
2371
3688
|
"Legacy migration control path must be a real directory"
|
|
2372
3689
|
);
|
|
2373
3690
|
}
|
|
2374
|
-
await
|
|
3691
|
+
await chmod4(directory, 448);
|
|
2375
3692
|
};
|
|
2376
3693
|
var ensurePrivateDirectoryWithin = async (storageRoot, directory) => {
|
|
2377
3694
|
if (directory !== storageRoot && !isPathInside(storageRoot, directory)) {
|
|
2378
3695
|
throw corrupt("Legacy migration control path escapes protected storage");
|
|
2379
3696
|
}
|
|
2380
|
-
const relative =
|
|
3697
|
+
const relative = path9.relative(storageRoot, directory);
|
|
2381
3698
|
let current = storageRoot;
|
|
2382
|
-
for (const segment of relative.split(
|
|
2383
|
-
current =
|
|
3699
|
+
for (const segment of relative.split(path9.sep).filter(Boolean)) {
|
|
3700
|
+
current = path9.join(current, segment);
|
|
2384
3701
|
await ensurePrivateDirectory(current);
|
|
2385
3702
|
}
|
|
2386
3703
|
};
|
|
@@ -2405,7 +3722,7 @@ var validateJournalPaths = (journal, storageRoot, migrationDirectory) => {
|
|
|
2405
3722
|
const stagingPaths = /* @__PURE__ */ new Set();
|
|
2406
3723
|
for (const item of journal.items) {
|
|
2407
3724
|
const sourceRelative = sourceRelativePath(item.id);
|
|
2408
|
-
if (!sourceRelative || item.source !==
|
|
3725
|
+
if (!sourceRelative || item.source !== path9.join(storageRoot, sourceRelative)) {
|
|
2409
3726
|
throw corrupt("Legacy migration journal contains an unexpected source path");
|
|
2410
3727
|
}
|
|
2411
3728
|
if (item.target !== deriveTarget(storageRoot, item.id, journal.owner)) {
|
|
@@ -2414,8 +3731,8 @@ var validateJournalPaths = (journal, storageRoot, migrationDirectory) => {
|
|
|
2414
3731
|
if (!isPathInside(migrationDirectory, item.staging)) {
|
|
2415
3732
|
throw corrupt("Legacy migration journal staging path escapes the migration directory");
|
|
2416
3733
|
}
|
|
2417
|
-
const stagingParent =
|
|
2418
|
-
if (
|
|
3734
|
+
const stagingParent = path9.dirname(item.staging);
|
|
3735
|
+
if (path9.dirname(stagingParent) !== path9.join(migrationDirectory, "staging") || item.staging !== path9.join(stagingParent, item.id)) {
|
|
2419
3736
|
throw corrupt("Legacy migration journal staging path is not canonical");
|
|
2420
3737
|
}
|
|
2421
3738
|
stagingParents.add(stagingParent);
|
|
@@ -2445,7 +3762,7 @@ var sourceRelativePath = (id) => {
|
|
|
2445
3762
|
if (known[id]) return known[id];
|
|
2446
3763
|
if (!id.startsWith("template-")) return null;
|
|
2447
3764
|
const name = id.slice("template-".length);
|
|
2448
|
-
return name.endsWith(".md") &&
|
|
3765
|
+
return name.endsWith(".md") && path9.basename(name) === name ? name : null;
|
|
2449
3766
|
};
|
|
2450
3767
|
var validateJournalOwnership = (journal) => {
|
|
2451
3768
|
if (journal.ownership === "explicit-owner") {
|
|
@@ -2508,7 +3825,7 @@ var isJournal = (value) => {
|
|
|
2508
3825
|
(item) => item && typeof item === "object" && typeof item.id === "string" && typeof item.source === "string" && typeof item.target === "string" && typeof item.staging === "string" && typeof item.phase === "string" && ITEM_PHASES.has(item.phase)
|
|
2509
3826
|
);
|
|
2510
3827
|
};
|
|
2511
|
-
var
|
|
3828
|
+
var exists2 = async (filePath) => {
|
|
2512
3829
|
try {
|
|
2513
3830
|
await lstat(filePath);
|
|
2514
3831
|
return true;
|
|
@@ -2518,8 +3835,8 @@ var exists = async (filePath) => {
|
|
|
2518
3835
|
}
|
|
2519
3836
|
};
|
|
2520
3837
|
var isPathInside = (parent, child) => {
|
|
2521
|
-
const relative =
|
|
2522
|
-
return relative !== "" && relative !== ".." && !relative.startsWith(`..${
|
|
3838
|
+
const relative = path9.relative(parent, child);
|
|
3839
|
+
return relative !== "" && relative !== ".." && !relative.startsWith(`..${path9.sep}`) && !path9.isAbsolute(relative);
|
|
2523
3840
|
};
|
|
2524
3841
|
var isMissing = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
2525
3842
|
var isAlreadyExists2 = (error) => error instanceof Error && "code" in error && error.code === "EEXIST";
|
|
@@ -2531,7 +3848,7 @@ var corrupt = (message) => new LegacyWorkspaceMigrationError("MIGRATION_CORRUPT"
|
|
|
2531
3848
|
var errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
2532
3849
|
|
|
2533
3850
|
// src/relay.ts
|
|
2534
|
-
import
|
|
3851
|
+
import path11 from "path";
|
|
2535
3852
|
import { createNextHandler } from "@bunny-agent/daemon/nextjs";
|
|
2536
3853
|
import WebSocket2 from "ws";
|
|
2537
3854
|
|
|
@@ -2703,15 +4020,15 @@ import { constants as constants3 } from "fs";
|
|
|
2703
4020
|
import {
|
|
2704
4021
|
copyFile as copyFile2,
|
|
2705
4022
|
lstat as lstat2,
|
|
2706
|
-
mkdir as
|
|
2707
|
-
readdir as
|
|
2708
|
-
readFile as
|
|
4023
|
+
mkdir as mkdir9,
|
|
4024
|
+
readdir as readdir5,
|
|
4025
|
+
readFile as readFile8,
|
|
2709
4026
|
realpath as realpath2,
|
|
2710
|
-
rm as
|
|
4027
|
+
rm as rm3,
|
|
2711
4028
|
symlink as symlink2,
|
|
2712
|
-
writeFile as
|
|
4029
|
+
writeFile as writeFile4
|
|
2713
4030
|
} from "fs/promises";
|
|
2714
|
-
import
|
|
4031
|
+
import path10 from "path";
|
|
2715
4032
|
var MOUNT_DIRECTORY = "mount";
|
|
2716
4033
|
var ARCHIVE_MARKER = "archived.json";
|
|
2717
4034
|
var AGENT_CONTEXT_TEMPLATE_FILES = [
|
|
@@ -2723,8 +4040,8 @@ var AGENT_CONTEXT_TEMPLATE_FILES = [
|
|
|
2723
4040
|
"BOOTSTRAP.md",
|
|
2724
4041
|
"MEMORY.md"
|
|
2725
4042
|
];
|
|
2726
|
-
var deriveSystemVolume = (descriptor) =>
|
|
2727
|
-
|
|
4043
|
+
var deriveSystemVolume = (descriptor) => path10.join(
|
|
4044
|
+
path10.dirname(path10.dirname(path10.dirname(path10.dirname(descriptor.sharedVolume)))),
|
|
2728
4045
|
"system"
|
|
2729
4046
|
);
|
|
2730
4047
|
var WorkspaceVolumeLifecycle = class {
|
|
@@ -2736,14 +4053,14 @@ var WorkspaceVolumeLifecycle = class {
|
|
|
2736
4053
|
async mount() {
|
|
2737
4054
|
await this.ensureVolumes();
|
|
2738
4055
|
await this.bootstrapAgentTemplates((await readBundledManifest()).sharedDir);
|
|
2739
|
-
const mountRoot =
|
|
2740
|
-
await
|
|
4056
|
+
const mountRoot = path10.join(this.descriptor.stateVolume, MOUNT_DIRECTORY);
|
|
4057
|
+
await mkdir9(mountRoot, { recursive: true, mode: 448 });
|
|
2741
4058
|
const mounts = {
|
|
2742
4059
|
mountRoot,
|
|
2743
|
-
agent:
|
|
2744
|
-
space:
|
|
2745
|
-
system:
|
|
2746
|
-
temporary:
|
|
4060
|
+
agent: path10.join(mountRoot, "agent"),
|
|
4061
|
+
space: path10.join(mountRoot, "space"),
|
|
4062
|
+
system: path10.join(mountRoot, "system"),
|
|
4063
|
+
temporary: path10.join(mountRoot, "tmp")
|
|
2747
4064
|
};
|
|
2748
4065
|
await Promise.all([
|
|
2749
4066
|
ensureVolumeLink(this.descriptor.privateVolume, mounts.agent),
|
|
@@ -2751,69 +4068,69 @@ var WorkspaceVolumeLifecycle = class {
|
|
|
2751
4068
|
ensureVolumeLink(this.systemVolume, mounts.system),
|
|
2752
4069
|
ensureVolumeLink(this.descriptor.temporaryVolume, mounts.temporary)
|
|
2753
4070
|
]);
|
|
2754
|
-
const daemonVolumes =
|
|
2755
|
-
await
|
|
4071
|
+
const daemonVolumes = path10.join(mountRoot, "volumes");
|
|
4072
|
+
await mkdir9(daemonVolumes, { recursive: true, mode: 448 });
|
|
2756
4073
|
await Promise.all([
|
|
2757
|
-
ensureVolumeLink(this.descriptor.privateVolume,
|
|
2758
|
-
ensureVolumeLink(this.descriptor.sharedVolume,
|
|
2759
|
-
ensureVolumeLink(this.systemVolume,
|
|
2760
|
-
ensureVolumeLink(this.descriptor.temporaryVolume,
|
|
4074
|
+
ensureVolumeLink(this.descriptor.privateVolume, path10.join(daemonVolumes, "agent")),
|
|
4075
|
+
ensureVolumeLink(this.descriptor.sharedVolume, path10.join(daemonVolumes, "space")),
|
|
4076
|
+
ensureVolumeLink(this.systemVolume, path10.join(daemonVolumes, "system")),
|
|
4077
|
+
ensureVolumeLink(this.descriptor.temporaryVolume, path10.join(daemonVolumes, "tmp"))
|
|
2761
4078
|
]);
|
|
2762
4079
|
return mounts;
|
|
2763
4080
|
}
|
|
2764
4081
|
async unmount() {
|
|
2765
|
-
await
|
|
4082
|
+
await rm3(path10.join(this.descriptor.stateVolume, MOUNT_DIRECTORY), {
|
|
2766
4083
|
recursive: true,
|
|
2767
4084
|
force: true
|
|
2768
4085
|
});
|
|
2769
4086
|
}
|
|
2770
4087
|
async archiveAgent() {
|
|
2771
4088
|
await this.unmount();
|
|
2772
|
-
await
|
|
2773
|
-
await
|
|
2774
|
-
|
|
4089
|
+
await mkdir9(this.descriptor.stateVolume, { recursive: true, mode: 448 });
|
|
4090
|
+
await writeFile4(
|
|
4091
|
+
path10.join(this.descriptor.stateVolume, ARCHIVE_MARKER),
|
|
2775
4092
|
`${JSON.stringify({ archivedAt: (/* @__PURE__ */ new Date()).toISOString() })}
|
|
2776
4093
|
`,
|
|
2777
4094
|
{ encoding: "utf8", mode: 384 }
|
|
2778
4095
|
);
|
|
2779
4096
|
}
|
|
2780
4097
|
async restoreAgent() {
|
|
2781
|
-
await
|
|
4098
|
+
await rm3(path10.join(this.descriptor.stateVolume, ARCHIVE_MARKER), { force: true });
|
|
2782
4099
|
return this.mount();
|
|
2783
4100
|
}
|
|
2784
4101
|
async deleteAgent(options) {
|
|
2785
4102
|
requireConfirmation(options);
|
|
2786
4103
|
await this.unmount();
|
|
2787
|
-
const agentRoot =
|
|
2788
|
-
await
|
|
4104
|
+
const agentRoot = path10.dirname(this.descriptor.privateVolume);
|
|
4105
|
+
await rm3(agentRoot, { recursive: true, force: true });
|
|
2789
4106
|
}
|
|
2790
4107
|
async archiveSpace() {
|
|
2791
|
-
await
|
|
2792
|
-
await
|
|
2793
|
-
|
|
4108
|
+
await mkdir9(this.descriptor.sharedVolume, { recursive: true, mode: 448 });
|
|
4109
|
+
await writeFile4(
|
|
4110
|
+
path10.join(this.descriptor.sharedVolume, ARCHIVE_MARKER),
|
|
2794
4111
|
`${JSON.stringify({ archivedAt: (/* @__PURE__ */ new Date()).toISOString() })}
|
|
2795
4112
|
`,
|
|
2796
4113
|
{ encoding: "utf8", mode: 384 }
|
|
2797
4114
|
);
|
|
2798
4115
|
}
|
|
2799
4116
|
async restoreSpace() {
|
|
2800
|
-
await
|
|
4117
|
+
await rm3(path10.join(this.descriptor.sharedVolume, ARCHIVE_MARKER), { force: true });
|
|
2801
4118
|
}
|
|
2802
4119
|
async deleteSpace(options) {
|
|
2803
4120
|
requireConfirmation(options);
|
|
2804
4121
|
await this.unmount();
|
|
2805
|
-
await
|
|
4122
|
+
await rm3(this.descriptor.sharedVolume, { recursive: true, force: true });
|
|
2806
4123
|
}
|
|
2807
4124
|
async bootstrapAgentTemplates(templateDirectory) {
|
|
2808
|
-
await
|
|
4125
|
+
await mkdir9(this.descriptor.privateVolume, { recursive: true, mode: 448 });
|
|
2809
4126
|
const bootstrapped = [];
|
|
2810
4127
|
const skipped = [];
|
|
2811
4128
|
const entries = new Set(
|
|
2812
|
-
(await
|
|
4129
|
+
(await readdir5(templateDirectory, { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isFile()).map((entry) => entry.name)
|
|
2813
4130
|
);
|
|
2814
4131
|
for (const filename of AGENT_CONTEXT_TEMPLATE_FILES) {
|
|
2815
4132
|
if (!entries.has(filename)) continue;
|
|
2816
|
-
const destination =
|
|
4133
|
+
const destination = path10.join(this.descriptor.privateVolume, filename);
|
|
2817
4134
|
try {
|
|
2818
4135
|
await lstat2(destination);
|
|
2819
4136
|
skipped.push(filename);
|
|
@@ -2821,7 +4138,7 @@ var WorkspaceVolumeLifecycle = class {
|
|
|
2821
4138
|
if (!isMissingFile(error)) throw error;
|
|
2822
4139
|
try {
|
|
2823
4140
|
await copyFile2(
|
|
2824
|
-
|
|
4141
|
+
path10.join(templateDirectory, filename),
|
|
2825
4142
|
destination,
|
|
2826
4143
|
constants3.COPYFILE_EXCL
|
|
2827
4144
|
);
|
|
@@ -2842,7 +4159,7 @@ var WorkspaceVolumeLifecycle = class {
|
|
|
2842
4159
|
this.descriptor.temporaryVolume,
|
|
2843
4160
|
this.systemVolume
|
|
2844
4161
|
]) {
|
|
2845
|
-
await
|
|
4162
|
+
await mkdir9(volume, { recursive: true, mode: 448 });
|
|
2846
4163
|
const info = await lstat2(volume);
|
|
2847
4164
|
if (info.isSymbolicLink() || !info.isDirectory() || await realpath2(volume) !== volume) {
|
|
2848
4165
|
throw new Error("Workspace volume must be a canonical real directory");
|
|
@@ -2926,6 +4243,8 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
|
|
|
2926
4243
|
let reconnectDelayMs = RELAY_RECONNECT_INTERVAL_MS;
|
|
2927
4244
|
let hadOpenConnection = false;
|
|
2928
4245
|
let activeRelayProtocol = relayProtocol;
|
|
4246
|
+
let currentInflight = /* @__PURE__ */ new Map();
|
|
4247
|
+
let activeTerminalTunnels = 0;
|
|
2929
4248
|
const replayGuard = new ConnectorExecutionReplayGuard();
|
|
2930
4249
|
const clearTimers = () => {
|
|
2931
4250
|
if (pingTimer) {
|
|
@@ -2979,6 +4298,7 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
|
|
|
2979
4298
|
});
|
|
2980
4299
|
socket = ws;
|
|
2981
4300
|
const inflight = /* @__PURE__ */ new Map();
|
|
4301
|
+
currentInflight = inflight;
|
|
2982
4302
|
let detached = false;
|
|
2983
4303
|
const detach = () => {
|
|
2984
4304
|
if (detached) return;
|
|
@@ -3024,7 +4344,15 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
|
|
|
3024
4344
|
replayGuard,
|
|
3025
4345
|
workspaceRegistry,
|
|
3026
4346
|
config.serverUrl,
|
|
3027
|
-
bunnyDaemon.baseUrl
|
|
4347
|
+
bunnyDaemon.baseUrl,
|
|
4348
|
+
{
|
|
4349
|
+
onTerminalOpen: () => {
|
|
4350
|
+
activeTerminalTunnels += 1;
|
|
4351
|
+
},
|
|
4352
|
+
onTerminalClose: () => {
|
|
4353
|
+
activeTerminalTunnels = Math.max(0, activeTerminalTunnels - 1);
|
|
4354
|
+
}
|
|
4355
|
+
}
|
|
3028
4356
|
);
|
|
3029
4357
|
});
|
|
3030
4358
|
ws.on("close", (code) => {
|
|
@@ -3056,6 +4384,12 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
|
|
|
3056
4384
|
scheduleReconnect("connect-throw");
|
|
3057
4385
|
}
|
|
3058
4386
|
return {
|
|
4387
|
+
isIdle() {
|
|
4388
|
+
return currentInflight.size === 0 && activeTerminalTunnels === 0;
|
|
4389
|
+
},
|
|
4390
|
+
isConnected() {
|
|
4391
|
+
return socket?.readyState === WebSocket2.OPEN && !suspendedForAccessRevocation;
|
|
4392
|
+
},
|
|
3059
4393
|
updateRelayProtocol(nextRelayProtocol) {
|
|
3060
4394
|
const previousKeyId = activeRelayProtocol?.keyId;
|
|
3061
4395
|
activeRelayProtocol = nextRelayProtocol;
|
|
@@ -3087,7 +4421,7 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
|
|
|
3087
4421
|
}
|
|
3088
4422
|
};
|
|
3089
4423
|
}
|
|
3090
|
-
async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedConnectorId, relayProtocol, replayGuard, workspaceRegistry, serverUrl, localDaemonBaseUrl) {
|
|
4424
|
+
async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedConnectorId, relayProtocol, replayGuard, workspaceRegistry, serverUrl, localDaemonBaseUrl, activity) {
|
|
3091
4425
|
if (!socket || socket.readyState !== WebSocket2.OPEN) return;
|
|
3092
4426
|
let message;
|
|
3093
4427
|
try {
|
|
@@ -3141,7 +4475,9 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
|
|
|
3141
4475
|
cloudToken: terminal.token,
|
|
3142
4476
|
localBaseUrl: localDaemonBaseUrl,
|
|
3143
4477
|
localToken,
|
|
3144
|
-
workspaceKey: descriptor.workspaceKey
|
|
4478
|
+
workspaceKey: descriptor.workspaceKey,
|
|
4479
|
+
onActivityStart: activity?.onTerminalOpen,
|
|
4480
|
+
onActivityEnd: activity?.onTerminalClose
|
|
3145
4481
|
});
|
|
3146
4482
|
} catch (error) {
|
|
3147
4483
|
await writeLog(
|
|
@@ -3334,12 +4670,12 @@ var normalizeWorkspaceRequest = (request, mounts) => {
|
|
|
3334
4670
|
}
|
|
3335
4671
|
const body = request.body;
|
|
3336
4672
|
const cwd = typeof body.cwd === "string" ? body.cwd : "/agent";
|
|
3337
|
-
const normalized =
|
|
4673
|
+
const normalized = path11.posix.normalize(cwd);
|
|
3338
4674
|
const [mount, relative] = normalized === "/agent" || normalized.startsWith("/agent/") ? [mounts.agent, normalized.slice("/agent".length)] : normalized === "/space" || normalized.startsWith("/space/") ? [mounts.space, normalized.slice("/space".length)] : [];
|
|
3339
4675
|
if (!mount || relative === void 0) {
|
|
3340
4676
|
throw new ConnectorRelayProtocolError("WORKSPACE_MISMATCH");
|
|
3341
4677
|
}
|
|
3342
|
-
return { ...request, body: { ...body, cwd:
|
|
4678
|
+
return { ...request, body: { ...body, cwd: path11.join(mount, relative) } };
|
|
3343
4679
|
};
|
|
3344
4680
|
function sendRelayError(socket, requestId, error) {
|
|
3345
4681
|
sendRelayMessage(socket, {
|
|
@@ -3370,14 +4706,14 @@ function getHeaderValue(headers, name) {
|
|
|
3370
4706
|
}
|
|
3371
4707
|
|
|
3372
4708
|
// src/workdir-layout.ts
|
|
3373
|
-
import { lstat as lstat3, mkdir as
|
|
3374
|
-
import
|
|
4709
|
+
import { lstat as lstat3, mkdir as mkdir10, symlink as symlink3, unlink as unlink6 } from "fs/promises";
|
|
4710
|
+
import path12 from "path";
|
|
3375
4711
|
async function provisionWorkdirLayout(workdirRoot) {
|
|
3376
4712
|
const manifest = await readBundledManifest();
|
|
3377
|
-
await
|
|
3378
|
-
await
|
|
3379
|
-
const systemSkillsLink =
|
|
3380
|
-
const systemScriptsLink =
|
|
4713
|
+
await mkdir10(workdirRoot, { recursive: true });
|
|
4714
|
+
await mkdir10(path12.join(workdirRoot, "system", ".agents"), { recursive: true });
|
|
4715
|
+
const systemSkillsLink = path12.join(workdirRoot, "system", ".agents", "skills");
|
|
4716
|
+
const systemScriptsLink = path12.join(workdirRoot, "system", "scripts");
|
|
3381
4717
|
await ensureSymlink(manifest.systemSkillsDir, systemSkillsLink);
|
|
3382
4718
|
await ensureSymlink(manifest.scriptsDir, systemScriptsLink);
|
|
3383
4719
|
await writeLog("info", "workdir layout provisioned", {
|
|
@@ -3397,7 +4733,7 @@ async function ensureSymlink(target, linkPath) {
|
|
|
3397
4733
|
try {
|
|
3398
4734
|
const existing = await lstat3(linkPath);
|
|
3399
4735
|
if (existing.isSymbolicLink()) {
|
|
3400
|
-
await
|
|
4736
|
+
await unlink6(linkPath);
|
|
3401
4737
|
} else if (existing.isDirectory()) {
|
|
3402
4738
|
await writeLog(
|
|
3403
4739
|
"warn",
|
|
@@ -3405,28 +4741,28 @@ async function ensureSymlink(target, linkPath) {
|
|
|
3405
4741
|
);
|
|
3406
4742
|
return;
|
|
3407
4743
|
} else {
|
|
3408
|
-
await
|
|
4744
|
+
await unlink6(linkPath);
|
|
3409
4745
|
}
|
|
3410
4746
|
} catch {
|
|
3411
4747
|
}
|
|
3412
|
-
await
|
|
4748
|
+
await mkdir10(path12.dirname(linkPath), { recursive: true });
|
|
3413
4749
|
await symlink3(target, linkPath, "dir");
|
|
3414
4750
|
}
|
|
3415
4751
|
|
|
3416
4752
|
// src/workspace-registry.ts
|
|
3417
4753
|
import { constants as constants4 } from "fs";
|
|
3418
4754
|
import {
|
|
3419
|
-
chmod as
|
|
4755
|
+
chmod as chmod5,
|
|
3420
4756
|
lstat as lstat4,
|
|
3421
|
-
mkdir as
|
|
4757
|
+
mkdir as mkdir11,
|
|
3422
4758
|
open as open3,
|
|
3423
|
-
readFile as
|
|
4759
|
+
readFile as readFile9,
|
|
3424
4760
|
realpath as realpath3,
|
|
3425
|
-
rename as
|
|
4761
|
+
rename as rename6,
|
|
3426
4762
|
statfs,
|
|
3427
|
-
unlink as
|
|
4763
|
+
unlink as unlink7
|
|
3428
4764
|
} from "fs/promises";
|
|
3429
|
-
import
|
|
4765
|
+
import path13 from "path";
|
|
3430
4766
|
var REGISTRY_VERSION = 1;
|
|
3431
4767
|
var REGISTRY_DIRECTORY = ".buda";
|
|
3432
4768
|
var REGISTRY_FILENAME = "workspace-registry.json";
|
|
@@ -3447,16 +4783,16 @@ var LocalWorkspaceRegistry = class _LocalWorkspaceRegistry {
|
|
|
3447
4783
|
this.document = document;
|
|
3448
4784
|
}
|
|
3449
4785
|
static async open(storageRoot) {
|
|
3450
|
-
await
|
|
4786
|
+
await mkdir11(storageRoot, { recursive: true, mode: 448 });
|
|
3451
4787
|
await rejectUnsafeFilesystem(storageRoot);
|
|
3452
4788
|
const canonicalRoot = await canonicalDirectory(storageRoot, "storage root");
|
|
3453
|
-
const registryDirectory =
|
|
4789
|
+
const registryDirectory = path13.join(canonicalRoot, REGISTRY_DIRECTORY);
|
|
3454
4790
|
await ensurePrivateDirectory2(registryDirectory);
|
|
3455
|
-
const registryPath =
|
|
4791
|
+
const registryPath = path13.join(registryDirectory, REGISTRY_FILENAME);
|
|
3456
4792
|
const document = await withExclusiveFileLock(registryPath, async () => {
|
|
3457
4793
|
let current;
|
|
3458
4794
|
try {
|
|
3459
|
-
current = parseRegistry(await
|
|
4795
|
+
current = parseRegistry(await readFile9(registryPath, "utf8"));
|
|
3460
4796
|
} catch (error) {
|
|
3461
4797
|
if (isMissingFile2(error)) {
|
|
3462
4798
|
current = { version: REGISTRY_VERSION, storageRoot: canonicalRoot, workspaces: {} };
|
|
@@ -3470,11 +4806,11 @@ var LocalWorkspaceRegistry = class _LocalWorkspaceRegistry {
|
|
|
3470
4806
|
if (current.storageRoot !== canonicalRoot) {
|
|
3471
4807
|
throw corrupt2("Registry storage root does not match the selected storage root");
|
|
3472
4808
|
}
|
|
3473
|
-
await
|
|
4809
|
+
await chmod5(registryPath, 384);
|
|
3474
4810
|
await reconcileDocument(current, canonicalRoot);
|
|
3475
4811
|
return current;
|
|
3476
4812
|
});
|
|
3477
|
-
await
|
|
4813
|
+
await chmod5(registryPath, 384);
|
|
3478
4814
|
return new _LocalWorkspaceRegistry(registryPath, canonicalRoot, document);
|
|
3479
4815
|
}
|
|
3480
4816
|
async resolve(spaceId, agentId) {
|
|
@@ -3521,23 +4857,23 @@ var LocalWorkspaceRegistry = class _LocalWorkspaceRegistry {
|
|
|
3521
4857
|
};
|
|
3522
4858
|
var readRegistryDocument = async (registryPath) => {
|
|
3523
4859
|
try {
|
|
3524
|
-
return parseRegistry(await
|
|
4860
|
+
return parseRegistry(await readFile9(registryPath, "utf8"));
|
|
3525
4861
|
} catch (error) {
|
|
3526
4862
|
if (error instanceof WorkspaceRegistryError) throw error;
|
|
3527
4863
|
throw corrupt2(`Cannot read local workspace registry: ${errorMessage2(error)}`);
|
|
3528
4864
|
}
|
|
3529
4865
|
};
|
|
3530
4866
|
var deriveWorkspace = (storageRoot, spaceId, agentId) => {
|
|
3531
|
-
const spaceRoot =
|
|
3532
|
-
const agentRoot =
|
|
4867
|
+
const spaceRoot = path13.join(storageRoot, REGISTRY_DIRECTORY, "spaces", spaceId);
|
|
4868
|
+
const agentRoot = path13.join(spaceRoot, "agents", agentId);
|
|
3533
4869
|
return {
|
|
3534
4870
|
workspaceKey: `${spaceId}:${agentId}`,
|
|
3535
4871
|
spaceId,
|
|
3536
4872
|
agentId,
|
|
3537
|
-
privateVolume:
|
|
3538
|
-
sharedVolume:
|
|
3539
|
-
stateVolume:
|
|
3540
|
-
temporaryVolume:
|
|
4873
|
+
privateVolume: path13.join(agentRoot, "workspace"),
|
|
4874
|
+
sharedVolume: path13.join(spaceRoot, "shared"),
|
|
4875
|
+
stateVolume: path13.join(agentRoot, "state"),
|
|
4876
|
+
temporaryVolume: path13.join(agentRoot, "tmp")
|
|
3541
4877
|
};
|
|
3542
4878
|
};
|
|
3543
4879
|
var parseRegistry = (raw) => {
|
|
@@ -3619,12 +4955,12 @@ var assertNoCollision = (workspaces, candidate) => {
|
|
|
3619
4955
|
}
|
|
3620
4956
|
};
|
|
3621
4957
|
var ensurePrivateDirectory2 = async (directory) => {
|
|
3622
|
-
await
|
|
4958
|
+
await mkdir11(directory, { recursive: true, mode: 448 });
|
|
3623
4959
|
const info = await lstat4(directory);
|
|
3624
4960
|
if (info.isSymbolicLink() || !info.isDirectory()) {
|
|
3625
4961
|
throw corrupt2("Workspace registry path is not a real directory");
|
|
3626
4962
|
}
|
|
3627
|
-
await
|
|
4963
|
+
await chmod5(directory, 448);
|
|
3628
4964
|
};
|
|
3629
4965
|
var canonicalDirectory = async (directory, label) => {
|
|
3630
4966
|
const info = await lstat4(directory);
|
|
@@ -3658,10 +4994,10 @@ var writeRegistryAtomically = async (registryPath, document) => {
|
|
|
3658
4994
|
await handle.close();
|
|
3659
4995
|
}
|
|
3660
4996
|
try {
|
|
3661
|
-
await
|
|
3662
|
-
await
|
|
4997
|
+
await rename6(temporaryPath, registryPath);
|
|
4998
|
+
await chmod5(registryPath, 384);
|
|
3663
4999
|
} catch (error) {
|
|
3664
|
-
await
|
|
5000
|
+
await unlink7(temporaryPath).catch(() => void 0);
|
|
3665
5001
|
throw error;
|
|
3666
5002
|
}
|
|
3667
5003
|
};
|
|
@@ -3684,8 +5020,8 @@ var assertContained = (root, candidate) => {
|
|
|
3684
5020
|
if (!isPathInside2(root, candidate)) throw corrupt2("Workspace path escapes the storage root");
|
|
3685
5021
|
};
|
|
3686
5022
|
var isPathInside2 = (parent, child) => {
|
|
3687
|
-
const relative =
|
|
3688
|
-
return relative !== "" && relative !== ".." && !relative.startsWith(`..${
|
|
5023
|
+
const relative = path13.relative(parent, child);
|
|
5024
|
+
return relative !== "" && relative !== ".." && !relative.startsWith(`..${path13.sep}`) && !path13.isAbsolute(relative);
|
|
3689
5025
|
};
|
|
3690
5026
|
var toDescriptor = (workspace) => ({
|
|
3691
5027
|
workspaceKey: workspace.workspaceKey,
|
|
@@ -3761,10 +5097,12 @@ async function runOnboard(launchConfig, options = {}) {
|
|
|
3761
5097
|
await provisionWorkdirLayout(config2.workdirRoot);
|
|
3762
5098
|
try {
|
|
3763
5099
|
const registration = await registerConnector(config2, launchConfig, void 0, {
|
|
3764
|
-
recoverMissingProfile:
|
|
5100
|
+
recoverMissingProfile: false
|
|
3765
5101
|
});
|
|
3766
5102
|
config2.connectorId = registration.connectorId;
|
|
5103
|
+
config2.spaceId = registration.spaceId ?? config2.spaceId;
|
|
3767
5104
|
relayProtocol = registration.relayProtocol;
|
|
5105
|
+
if (!config2.spaceId) throw new Error("Registration did not resolve a Space");
|
|
3768
5106
|
await writeDeviceConfig(config2);
|
|
3769
5107
|
} catch (error) {
|
|
3770
5108
|
s2.stop("Registration failed");
|
|
@@ -3854,8 +5192,31 @@ async function runOnboard(launchConfig, options = {}) {
|
|
|
3854
5192
|
}
|
|
3855
5193
|
|
|
3856
5194
|
// src/runtime.ts
|
|
3857
|
-
import { mkdir as
|
|
3858
|
-
import
|
|
5195
|
+
import { mkdir as mkdir12 } from "fs/promises";
|
|
5196
|
+
import path14 from "path";
|
|
5197
|
+
var RECOVERED_DAEMON_STOP_TIMEOUT_MS = 1e4;
|
|
5198
|
+
var RECOVERED_DAEMON_STOP_POLL_MS = 100;
|
|
5199
|
+
var wait3 = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
5200
|
+
async function stopRecoveredIdleDaemon(config, configRoot) {
|
|
5201
|
+
const stopStatus = await requestDaemonControl(config, "stop-if-idle", configRoot).catch(
|
|
5202
|
+
() => null
|
|
5203
|
+
);
|
|
5204
|
+
if (!stopStatus) return;
|
|
5205
|
+
if (!stopStatus.idle) {
|
|
5206
|
+
throw new Error(
|
|
5207
|
+
`The recovered Connector ${config.connectorId} is still running an Agent task and cannot be replaced yet.`
|
|
5208
|
+
);
|
|
5209
|
+
}
|
|
5210
|
+
const deadline = Date.now() + RECOVERED_DAEMON_STOP_TIMEOUT_MS;
|
|
5211
|
+
while (Date.now() < deadline) {
|
|
5212
|
+
const status = await requestDaemonControl(config, "status", configRoot, {
|
|
5213
|
+
timeoutMs: 500
|
|
5214
|
+
}).catch(() => null);
|
|
5215
|
+
if (!status) return;
|
|
5216
|
+
await wait3(RECOVERED_DAEMON_STOP_POLL_MS);
|
|
5217
|
+
}
|
|
5218
|
+
throw new Error(`Recovered Connector ${config.connectorId} did not stop cleanly.`);
|
|
5219
|
+
}
|
|
3859
5220
|
async function runNewConnector(launchConfig) {
|
|
3860
5221
|
const existing = await readDeviceConfig(launchConfig);
|
|
3861
5222
|
const config = await ensureDeviceConfig(launchConfig);
|
|
@@ -3864,27 +5225,41 @@ async function runNewConnector(launchConfig) {
|
|
|
3864
5225
|
await provisionWorkdirLayout(config.workdirRoot);
|
|
3865
5226
|
await LocalWorkspaceRegistry.open(config.workdirRoot);
|
|
3866
5227
|
const registration = await registerConnector(config, launchConfig, void 0, {
|
|
3867
|
-
recoverMissingProfile:
|
|
5228
|
+
recoverMissingProfile: shouldRecoverMissingProfile(Boolean(existing), launchConfig)
|
|
3868
5229
|
});
|
|
3869
5230
|
config.connectorId = registration.connectorId;
|
|
5231
|
+
config.spaceId = registration.spaceId ?? config.spaceId;
|
|
3870
5232
|
await writeDeviceConfig(config);
|
|
3871
5233
|
await writeLog("info", `wrote ${paths.configPath}`);
|
|
3872
5234
|
await writeLog("info", `workdir root ${config.workdirRoot}`);
|
|
3873
5235
|
await writeLog("info", `log file ${getDatedConnectorLogPath(paths.logPath)}`);
|
|
3874
5236
|
}
|
|
3875
|
-
async function runConnectorDaemon(launchConfig) {
|
|
3876
|
-
const
|
|
3877
|
-
const
|
|
3878
|
-
const
|
|
3879
|
-
|
|
3880
|
-
|
|
5237
|
+
async function runConnectorDaemon(launchConfig, options = {}) {
|
|
5238
|
+
const configRoot = options.configRoot ?? process.env.BUDA_CONNECTOR_CONFIG_ROOT;
|
|
5239
|
+
const existing = await readDeviceConfig(launchConfig, configRoot);
|
|
5240
|
+
const config = await ensureDeviceConfig(launchConfig, configRoot);
|
|
5241
|
+
setConnectorLogContext({
|
|
5242
|
+
configRoot,
|
|
5243
|
+
connectorId: config.connectorId,
|
|
5244
|
+
serverUrl: config.serverUrl
|
|
5245
|
+
});
|
|
5246
|
+
const paths = getConnectorPaths(config.serverUrl, configRoot);
|
|
5247
|
+
const daemonLockTarget = getConnectorDaemonLockTarget(
|
|
5248
|
+
config.serverUrl,
|
|
5249
|
+
config.connectorId,
|
|
5250
|
+
configRoot
|
|
5251
|
+
);
|
|
5252
|
+
await mkdir12(path14.dirname(daemonLockTarget), { recursive: true, mode: 448 });
|
|
3881
5253
|
let daemonLock = await acquireExclusiveFileLock(daemonLockTarget, { timeoutMs: 0 }).catch(() => {
|
|
3882
5254
|
throw new Error(
|
|
3883
5255
|
`Another Buda Connector daemon is already running for this profile on ${config.serverUrl}. Stop it before starting another instance.`
|
|
3884
5256
|
);
|
|
3885
5257
|
});
|
|
5258
|
+
const restoreConsole = options.captureConsole ? installConnectorConsoleCapture() : void 0;
|
|
3886
5259
|
let bunnyDaemon;
|
|
3887
5260
|
let relayProtocol;
|
|
5261
|
+
let control;
|
|
5262
|
+
let lastHeartbeatAt;
|
|
3888
5263
|
try {
|
|
3889
5264
|
await migrateLegacyWorkspaceRoot(config.workdirRoot);
|
|
3890
5265
|
await provisionWorkdirLayout(config.workdirRoot);
|
|
@@ -3892,15 +5267,22 @@ async function runConnectorDaemon(launchConfig) {
|
|
|
3892
5267
|
bunnyDaemon = await startEmbeddedBunnyDaemon(config, launchConfig);
|
|
3893
5268
|
const activeBunnyDaemon = bunnyDaemon;
|
|
3894
5269
|
const registration = await registerConnector(config, launchConfig, activeBunnyDaemon, {
|
|
3895
|
-
recoverMissingProfile:
|
|
5270
|
+
recoverMissingProfile: shouldRecoverMissingProfile(Boolean(existing), launchConfig)
|
|
3896
5271
|
});
|
|
3897
5272
|
const connectorIdChanged = config.connectorId !== registration.connectorId;
|
|
3898
5273
|
if (connectorIdChanged) {
|
|
5274
|
+
const registeredConfig = {
|
|
5275
|
+
...config,
|
|
5276
|
+
connectorId: registration.connectorId,
|
|
5277
|
+
spaceId: registration.spaceId ?? config.spaceId
|
|
5278
|
+
};
|
|
5279
|
+
await stopRecoveredIdleDaemon(registeredConfig, configRoot);
|
|
3899
5280
|
const registeredLockTarget = getConnectorDaemonLockTarget(
|
|
3900
5281
|
config.serverUrl,
|
|
3901
|
-
registration.connectorId
|
|
5282
|
+
registration.connectorId,
|
|
5283
|
+
configRoot
|
|
3902
5284
|
);
|
|
3903
|
-
await
|
|
5285
|
+
await mkdir12(path14.dirname(registeredLockTarget), { recursive: true, mode: 448 });
|
|
3904
5286
|
const registeredLock = await acquireExclusiveFileLock(registeredLockTarget, {
|
|
3905
5287
|
timeoutMs: 0
|
|
3906
5288
|
}).catch(() => {
|
|
@@ -3912,8 +5294,15 @@ async function runConnectorDaemon(launchConfig) {
|
|
|
3912
5294
|
daemonLock = registeredLock;
|
|
3913
5295
|
}
|
|
3914
5296
|
config.connectorId = registration.connectorId;
|
|
5297
|
+
setConnectorLogContext({ connectorId: config.connectorId });
|
|
5298
|
+
config.spaceId = registration.spaceId ?? config.spaceId;
|
|
3915
5299
|
relayProtocol = registration.relayProtocol;
|
|
3916
|
-
if (!
|
|
5300
|
+
if (!config.spaceId) {
|
|
5301
|
+
throw new Error("Connector registration did not return a canonical spaceId");
|
|
5302
|
+
}
|
|
5303
|
+
if (!existing || connectorIdChanged || existing.spaceId !== config.spaceId) {
|
|
5304
|
+
await writeDeviceConfig(config, configRoot);
|
|
5305
|
+
}
|
|
3917
5306
|
const relay = startConnectorRelay(
|
|
3918
5307
|
config,
|
|
3919
5308
|
activeBunnyDaemon,
|
|
@@ -3928,6 +5317,7 @@ async function runConnectorDaemon(launchConfig) {
|
|
|
3928
5317
|
await writeLog("info", `bunny daemon ${activeBunnyDaemon.baseUrl}`);
|
|
3929
5318
|
const interval = setInterval(() => {
|
|
3930
5319
|
heartbeatConnector(config, launchConfig, activeBunnyDaemon).then(({ relayProtocol: nextRelayProtocol }) => {
|
|
5320
|
+
lastHeartbeatAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3931
5321
|
relay.updateRelayProtocol(nextRelayProtocol);
|
|
3932
5322
|
relay.resumeAfterAccessRestore();
|
|
3933
5323
|
updateConnectorDashboardStatus({
|
|
@@ -3951,18 +5341,32 @@ async function runConnectorDaemon(launchConfig) {
|
|
|
3951
5341
|
await unregisterConnector(config, launchConfig);
|
|
3952
5342
|
await stopEmbeddedBunnyDaemon(activeBunnyDaemon);
|
|
3953
5343
|
} finally {
|
|
5344
|
+
await control?.close();
|
|
3954
5345
|
await daemonLock.release();
|
|
3955
|
-
|
|
5346
|
+
await restoreConsole?.();
|
|
5347
|
+
if (options.exitOnShutdown !== false) process.exit(0);
|
|
3956
5348
|
}
|
|
3957
5349
|
};
|
|
5350
|
+
control = await startDaemonControlServer({
|
|
5351
|
+
config,
|
|
5352
|
+
configRoot,
|
|
5353
|
+
getLastHeartbeatAt: () => lastHeartbeatAt,
|
|
5354
|
+
isIdle: () => relay.isIdle(),
|
|
5355
|
+
isRelayConnected: () => relay.isConnected(),
|
|
5356
|
+
mode: options.mode ?? "persistent",
|
|
5357
|
+
onStop: shutdown,
|
|
5358
|
+
version: options.version
|
|
5359
|
+
});
|
|
3958
5360
|
process.once("SIGINT", () => void shutdown());
|
|
3959
5361
|
process.once("SIGTERM", () => void shutdown());
|
|
3960
5362
|
} catch (error) {
|
|
3961
5363
|
if (bunnyDaemon) await stopEmbeddedBunnyDaemon(bunnyDaemon);
|
|
3962
5364
|
await daemonLock.release();
|
|
5365
|
+
await restoreConsole?.();
|
|
3963
5366
|
throw error;
|
|
3964
5367
|
}
|
|
3965
5368
|
}
|
|
5369
|
+
var shouldRecoverMissingProfile = (hasExistingProfile, launchConfig) => !hasExistingProfile && Boolean(getConnectorToken(launchConfig)) && !launchConfig?.oauthToken?.trim();
|
|
3966
5370
|
|
|
3967
5371
|
// src/cli.ts
|
|
3968
5372
|
function printHelp() {
|
|
@@ -3980,6 +5384,7 @@ function readCliLaunchConfig() {
|
|
|
3980
5384
|
const bunnyDaemonPort = readFlagValue("--bunny-daemon-port");
|
|
3981
5385
|
return {
|
|
3982
5386
|
serverUrl: readFlagValue("--server-url"),
|
|
5387
|
+
spaceId: readFlagValue("--space-id"),
|
|
3983
5388
|
workdirRoot: readFlagValue("--workdir"),
|
|
3984
5389
|
oauthToken: readFlagValue("--oauth-token"),
|
|
3985
5390
|
token: readFlagValue("--token"),
|
|
@@ -4027,13 +5432,15 @@ async function main() {
|
|
|
4027
5432
|
const isTTY = process.stdin.isTTY && process.stdout.isTTY;
|
|
4028
5433
|
const defaultCommand = isTTY ? "onboard" : "daemon";
|
|
4029
5434
|
const command = !rawCommand || rawCommand.startsWith("-") ? defaultCommand : rawCommand;
|
|
5435
|
+
const configRoot = readFlagValue("--config-root");
|
|
5436
|
+
if (configRoot) process.env.BUDA_CONNECTOR_CONFIG_ROOT = configRoot;
|
|
4030
5437
|
process.on("unhandledRejection", (reason) => {
|
|
4031
5438
|
console.error("[buda-connector] unhandledRejection:", reason);
|
|
4032
5439
|
});
|
|
4033
5440
|
process.on("uncaughtException", (error) => {
|
|
4034
5441
|
console.error("[buda-connector] uncaughtException:", error);
|
|
4035
5442
|
});
|
|
4036
|
-
const launchConfig = await readLaunchConfig();
|
|
5443
|
+
const launchConfig = process.argv.includes("--internal-daemon") ? getInternalLaunchConfig() : await readLaunchConfig();
|
|
4037
5444
|
const plain = process.argv.includes("--plain");
|
|
4038
5445
|
if (command === "new") {
|
|
4039
5446
|
await runNewConnector(launchConfig);
|
|
@@ -4043,11 +5450,31 @@ async function main() {
|
|
|
4043
5450
|
await runOnboard(launchConfig, { plain });
|
|
4044
5451
|
return;
|
|
4045
5452
|
}
|
|
4046
|
-
if (command === "dev" ||
|
|
5453
|
+
if (command === "dev" || process.argv.includes("--foreground")) {
|
|
4047
5454
|
startConnectorConsoleDashboard({ plain });
|
|
4048
5455
|
await runConnectorDaemon(launchConfig);
|
|
4049
5456
|
return;
|
|
4050
5457
|
}
|
|
5458
|
+
if (command === "daemon" && process.argv.includes("--internal-daemon")) {
|
|
5459
|
+
await runConnectorDaemon(launchConfig, {
|
|
5460
|
+
captureConsole: true,
|
|
5461
|
+
mode: getDaemonModeFlag(),
|
|
5462
|
+
version: CONNECTOR_RUNTIME_VERSION
|
|
5463
|
+
});
|
|
5464
|
+
return;
|
|
5465
|
+
}
|
|
5466
|
+
if (command === "daemon" && process.argv.includes("--desktop-release")) {
|
|
5467
|
+
await releaseDesktopConnector(launchConfig);
|
|
5468
|
+
return;
|
|
5469
|
+
}
|
|
5470
|
+
if (command === "daemon" && process.argv.includes("--desktop")) {
|
|
5471
|
+
await runDesktopConnectorController(launchConfig);
|
|
5472
|
+
return;
|
|
5473
|
+
}
|
|
5474
|
+
if (command === "daemon") {
|
|
5475
|
+
await runConnectorDashboard(launchConfig, { plain });
|
|
5476
|
+
return;
|
|
5477
|
+
}
|
|
4051
5478
|
printHelp();
|
|
4052
5479
|
process.exit(1);
|
|
4053
5480
|
}
|