replicas-computer 0.3.29 → 0.3.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +125 -64
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/client.ts
|
|
4
|
-
import { createConnection as
|
|
4
|
+
import { createConnection as createConnection4 } from "net";
|
|
5
5
|
|
|
6
6
|
// src/daemon.ts
|
|
7
7
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
8
8
|
import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync6, rmSync as rmSync5 } from "fs";
|
|
9
|
-
import { createConnection as
|
|
9
|
+
import { createConnection as createConnection3, createServer as createServer2 } from "net";
|
|
10
10
|
import { dirname as dirname5 } from "path";
|
|
11
11
|
|
|
12
12
|
// src/commands/desktop.ts
|
|
@@ -21,6 +21,8 @@ function isRecord(value) {
|
|
|
21
21
|
|
|
22
22
|
// ../shared/src/engine/environment.ts
|
|
23
23
|
var DESKTOP_STREAM_PORT = 6080;
|
|
24
|
+
var DESKTOP_COMPUTER_SOCKET = "/tmp/replicas-desktop-computer.sock";
|
|
25
|
+
var DESKTOP_COMPUTER_VIEW_PORT = 6091;
|
|
24
26
|
var DEVICES_DIR = ".ssh/replicas-devices";
|
|
25
27
|
var DEVICE_SCREEN_FILE_SUFFIX = ".screen.json";
|
|
26
28
|
function parseDeviceScreenState(value) {
|
|
@@ -1128,6 +1130,10 @@ async function computerRecordStopCommand() {
|
|
|
1128
1130
|
}
|
|
1129
1131
|
|
|
1130
1132
|
// src/lib/chrome-session.ts
|
|
1133
|
+
function filterChromePages(pages, filter) {
|
|
1134
|
+
const has = (value, needle) => (value ?? "").toLowerCase().includes(needle.toLowerCase());
|
|
1135
|
+
return pages.filter((page) => (!filter.targetId || page.id === filter.targetId) && (!filter.title || has(page.title, filter.title)) && (!filter.url || has(page.url, filter.url)));
|
|
1136
|
+
}
|
|
1131
1137
|
async function openChromeSession(webSocketDebuggerUrl) {
|
|
1132
1138
|
const socket = new WebSocket(webSocketDebuggerUrl);
|
|
1133
1139
|
const pending = /* @__PURE__ */ new Map();
|
|
@@ -1137,9 +1143,9 @@ async function openChromeSession(webSocketDebuggerUrl) {
|
|
|
1137
1143
|
const fail2 = (error) => {
|
|
1138
1144
|
if (closed) return;
|
|
1139
1145
|
closed = true;
|
|
1140
|
-
for (const
|
|
1141
|
-
clearTimeout(
|
|
1142
|
-
|
|
1146
|
+
for (const request2 of pending.values()) {
|
|
1147
|
+
clearTimeout(request2.timeout);
|
|
1148
|
+
request2.reject(error);
|
|
1143
1149
|
}
|
|
1144
1150
|
pending.clear();
|
|
1145
1151
|
socket.close();
|
|
@@ -1156,12 +1162,12 @@ async function openChromeSession(webSocketDebuggerUrl) {
|
|
|
1156
1162
|
if (!isRecord(message)) return;
|
|
1157
1163
|
if (typeof message.method === "string" && isRecord(message.params)) listeners.get(message.method)?.(message.params);
|
|
1158
1164
|
if (typeof message.id !== "number") return;
|
|
1159
|
-
const
|
|
1160
|
-
if (!
|
|
1161
|
-
clearTimeout(
|
|
1165
|
+
const request2 = pending.get(message.id);
|
|
1166
|
+
if (!request2) return;
|
|
1167
|
+
clearTimeout(request2.timeout);
|
|
1162
1168
|
pending.delete(message.id);
|
|
1163
1169
|
const detail = isRecord(message.error) && typeof message.error.message === "string" ? message.error.message : void 0;
|
|
1164
|
-
|
|
1170
|
+
request2.resolve(message.error ? { error: { message: detail } } : { result: isRecord(message.result) ? message.result : {} });
|
|
1165
1171
|
});
|
|
1166
1172
|
socket.addEventListener("error", () => fail2(new Error("Chrome DevTools WebSocket failed")));
|
|
1167
1173
|
socket.addEventListener("close", () => fail2(new Error("Chrome DevTools WebSocket closed unexpectedly")));
|
|
@@ -1231,18 +1237,7 @@ async function getChromePages() {
|
|
|
1231
1237
|
}
|
|
1232
1238
|
async function selectChromePage(options = {}) {
|
|
1233
1239
|
const pages = await getChromePages();
|
|
1234
|
-
|
|
1235
|
-
if (options.targetId) {
|
|
1236
|
-
matches = matches.filter((page2) => page2.id === options.targetId);
|
|
1237
|
-
}
|
|
1238
|
-
if (options.title) {
|
|
1239
|
-
const needle = options.title.toLowerCase();
|
|
1240
|
-
matches = matches.filter((page2) => (page2.title ?? "").toLowerCase().includes(needle));
|
|
1241
|
-
}
|
|
1242
|
-
if (options.url) {
|
|
1243
|
-
const needle = options.url.toLowerCase();
|
|
1244
|
-
matches = matches.filter((page2) => (page2.url ?? "").toLowerCase().includes(needle));
|
|
1245
|
-
}
|
|
1240
|
+
const matches = filterChromePages(pages, options);
|
|
1246
1241
|
const index = options.page ? parseCoord(options.page, "--page") : 0;
|
|
1247
1242
|
if (index < 0) fail("--page must be >= 0");
|
|
1248
1243
|
const page = matches[index];
|
|
@@ -3007,10 +3002,68 @@ async function computerDragCommand(fx, fy, tx, ty) {
|
|
|
3007
3002
|
console.log(`dragged (${fromX},${fromY}) -> (${toX},${toY})`);
|
|
3008
3003
|
}
|
|
3009
3004
|
|
|
3010
|
-
// src/commands/
|
|
3005
|
+
// src/commands/mac.ts
|
|
3011
3006
|
import { spawn as spawn3 } from "child_process";
|
|
3012
|
-
import {
|
|
3013
|
-
import {
|
|
3007
|
+
import { randomUUID } from "crypto";
|
|
3008
|
+
import { writeFileSync as writeFileSync5 } from "fs";
|
|
3009
|
+
import { createConnection } from "net";
|
|
3010
|
+
var REQUEST_TIMEOUT_MS = 12e4;
|
|
3011
|
+
function request(payload) {
|
|
3012
|
+
return new Promise((resolve5, reject) => {
|
|
3013
|
+
const socket = createConnection(DESKTOP_COMPUTER_SOCKET);
|
|
3014
|
+
let reply = "";
|
|
3015
|
+
const disconnected = () => reject(new Error("No Mac is connected. Open this workspace in the Replicas desktop app on the Mac, with computer use turned on."));
|
|
3016
|
+
socket.once("error", disconnected);
|
|
3017
|
+
socket.once("close", disconnected);
|
|
3018
|
+
socket.setTimeout(REQUEST_TIMEOUT_MS, () => {
|
|
3019
|
+
socket.destroy();
|
|
3020
|
+
reject(new Error(`The Mac's driver didn't answer within ${REQUEST_TIMEOUT_MS / 1e3} seconds.`));
|
|
3021
|
+
});
|
|
3022
|
+
socket.on("data", (chunk) => {
|
|
3023
|
+
reply += chunk.toString("utf8");
|
|
3024
|
+
const end = reply.indexOf("\n");
|
|
3025
|
+
if (end === -1) return;
|
|
3026
|
+
socket.destroy();
|
|
3027
|
+
try {
|
|
3028
|
+
resolve5(JSON.parse(reply.slice(0, end)));
|
|
3029
|
+
} catch (error) {
|
|
3030
|
+
reject(error);
|
|
3031
|
+
}
|
|
3032
|
+
});
|
|
3033
|
+
socket.write(`${JSON.stringify(payload)}
|
|
3034
|
+
`);
|
|
3035
|
+
});
|
|
3036
|
+
}
|
|
3037
|
+
async function showView() {
|
|
3038
|
+
const origin = `http://127.0.0.1:${DESKTOP_COMPUTER_VIEW_PORT}/`;
|
|
3039
|
+
if ((await getChromePages().catch(() => [])).some((page) => page.url?.startsWith(origin))) return;
|
|
3040
|
+
ensureServicesRunning();
|
|
3041
|
+
spawn3(CHROME_WRAPPER, ["--new-window", "--start-maximized", `--app=${origin}`], { env: withDisplay(), detached: true, stdio: "ignore" }).unref();
|
|
3042
|
+
}
|
|
3043
|
+
async function computerMacCommand(tool, json, options) {
|
|
3044
|
+
let args;
|
|
3045
|
+
try {
|
|
3046
|
+
args = JSON.parse(json ?? "{}");
|
|
3047
|
+
} catch {
|
|
3048
|
+
fail(`Arguments must be a JSON object, got: ${json}`);
|
|
3049
|
+
}
|
|
3050
|
+
if (!isRecord(args)) fail(`Arguments must be a JSON object, got: ${json}`);
|
|
3051
|
+
const session = options.session ?? `replicas-${randomUUID()}`;
|
|
3052
|
+
const reply = await request({ method: "call", name: tool, args: { ...args, session }, session_id: session, client_kind: "cli", observation_origin: "direct" }).catch((error) => fail(error instanceof Error ? error.message : String(error)));
|
|
3053
|
+
if (!options.session) await request({ method: "session_end", session_id: session, client_kind: "cli" }).catch(() => void 0);
|
|
3054
|
+
await showView().catch(() => void 0);
|
|
3055
|
+
if (!isRecord(reply) || reply.ok !== true || !isRecord(reply.result)) fail(`The Mac's driver refused: ${JSON.stringify(isRecord(reply) ? reply.error ?? reply : reply)}`);
|
|
3056
|
+
const { content, structuredContent } = reply.result;
|
|
3057
|
+
const blocks = Array.isArray(content) ? content.filter(isRecord) : [];
|
|
3058
|
+
const image = blocks.find((block) => block.type === "image" && typeof block.data === "string");
|
|
3059
|
+
if (image && options.screenshot) writeFileSync5(options.screenshot, Buffer.from(String(image.data), "base64"));
|
|
3060
|
+
console.log(structuredContent === void 0 ? blocks.filter((block) => block.type === "text").map((block) => block.text).join("\n") : JSON.stringify(structuredContent, null, 2));
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
// src/commands/screen.ts
|
|
3064
|
+
import { spawn as spawn4 } from "child_process";
|
|
3065
|
+
import { closeSync, existsSync as existsSync5, mkdirSync as mkdirSync5, openSync, readdirSync as readdirSync2, readFileSync as readFileSync5, rmSync as rmSync4, symlinkSync, writeFileSync as writeFileSync6 } from "fs";
|
|
3066
|
+
import { createConnection as createConnection2, createServer } from "net";
|
|
3014
3067
|
import { homedir } from "os";
|
|
3015
3068
|
import { join } from "path";
|
|
3016
3069
|
import { setTimeout as sleep2 } from "timers/promises";
|
|
@@ -3026,7 +3079,7 @@ function ownsProcess(session, pid) {
|
|
|
3026
3079
|
}
|
|
3027
3080
|
function isListening(port2) {
|
|
3028
3081
|
return new Promise((resolve5) => {
|
|
3029
|
-
const socket =
|
|
3082
|
+
const socket = createConnection2({ host: "127.0.0.1", port: port2 });
|
|
3030
3083
|
const done = (listening) => {
|
|
3031
3084
|
socket.destroy();
|
|
3032
3085
|
resolve5(listening);
|
|
@@ -3050,7 +3103,7 @@ async function freePorts(count) {
|
|
|
3050
3103
|
}
|
|
3051
3104
|
function spawnLogged(bin, args, log) {
|
|
3052
3105
|
const fd = openSync(log, "a");
|
|
3053
|
-
const child =
|
|
3106
|
+
const child = spawn4(bin, args, { detached: true, stdio: ["ignore", fd, fd] });
|
|
3054
3107
|
closeSync(fd);
|
|
3055
3108
|
child.unref();
|
|
3056
3109
|
let exited = false;
|
|
@@ -3069,7 +3122,7 @@ async function waitForPort(port2, child) {
|
|
|
3069
3122
|
}
|
|
3070
3123
|
function readBanner(port2) {
|
|
3071
3124
|
return new Promise((resolve5) => {
|
|
3072
|
-
const socket =
|
|
3125
|
+
const socket = createConnection2({ host: "127.0.0.1", port: port2 });
|
|
3073
3126
|
let banner = "";
|
|
3074
3127
|
const done = () => {
|
|
3075
3128
|
socket.destroy();
|
|
@@ -3138,7 +3191,7 @@ async function openSession(device, dir, previous) {
|
|
|
3138
3191
|
pids.push(bridge.pid);
|
|
3139
3192
|
if (!await waitForPort(webPort, bridge)) abort(`Could not start the screen viewer: ${readFileSync5(bridgeLog, "utf8").trim() || "timed out"}`);
|
|
3140
3193
|
const session = { webPort, tunnelPort, pids };
|
|
3141
|
-
|
|
3194
|
+
writeFileSync6(join(dir, "session.json"), JSON.stringify(session));
|
|
3142
3195
|
return session;
|
|
3143
3196
|
}
|
|
3144
3197
|
async function computerScreenCommand(device, options) {
|
|
@@ -3168,7 +3221,7 @@ async function showScreen(device, session) {
|
|
|
3168
3221
|
const findPage = async () => (await getChromePages().catch(() => [])).find((page2) => page2.webSocketDebuggerUrl && page2.url?.startsWith(origin));
|
|
3169
3222
|
let page = await findPage();
|
|
3170
3223
|
if (!page) {
|
|
3171
|
-
|
|
3224
|
+
spawn4(CHROME_WRAPPER, ["--new-window", `--app=${origin}?device=${device}&requestFps=0`], { env: withDisplay(), detached: true, stdio: "ignore" }).unref();
|
|
3172
3225
|
const deadline2 = Date.now() + CONNECT_TIMEOUT_MS;
|
|
3173
3226
|
while (!page && Date.now() < deadline2) {
|
|
3174
3227
|
await sleep2(250);
|
|
@@ -3263,7 +3316,7 @@ async function computerStatusCommand() {
|
|
|
3263
3316
|
}
|
|
3264
3317
|
|
|
3265
3318
|
// src/version.ts
|
|
3266
|
-
var COMPUTER_USE_VERSION = "0.3.
|
|
3319
|
+
var COMPUTER_USE_VERSION = "0.3.31";
|
|
3267
3320
|
var COMPUTER_USE_PROTOCOL_VERSION = 1;
|
|
3268
3321
|
|
|
3269
3322
|
// src/program.ts
|
|
@@ -3301,6 +3354,7 @@ function createProgram() {
|
|
|
3301
3354
|
program.command("scroll <direction>").description("Scroll the desktop").option("--amount <n>", "Wheel ticks").option("--x <x>", "Pointer x").option("--y <y>", "Pointer y").action((direction, options) => computerScrollCommand(direction, options));
|
|
3302
3355
|
program.command("drag <fromX> <fromY> <toX> <toY>").description("Drag between desktop coordinates").action((fromX, fromY, toX, toY) => computerDragCommand(fromX, fromY, toX, toY));
|
|
3303
3356
|
program.command("launch <app> [args...]").description("Launch an application on the desktop").action((app, args) => computerLaunchCommand(app, args));
|
|
3357
|
+
program.command("mac <tool> [json]", { hidden: true }).description("Call a Cua Driver tool on the user's Mac, in the background, through the Replicas desktop app").option("--screenshot <path>", "Write the screenshot in the reply to this PNG").option("--session <name>", "Keep element tokens across calls under this session name").action((tool, json, options) => computerMacCommand(tool, json, options));
|
|
3304
3358
|
program.command("screen <device>", { hidden: true }).description("Show a device's screen in a window on this desktop, where the other commands drive it").option("--close", "Close the window and its connection").action((device, options) => computerScreenCommand(device, options));
|
|
3305
3359
|
const record = program.command("record").description("Record the workspace display");
|
|
3306
3360
|
record.command("start <path>").description("Start a recording").option("--fps <n>", "Frame rate. Default 60.").action((path, options) => computerRecordStartCommand(path, options));
|
|
@@ -3355,7 +3409,7 @@ function writeResult(socket, result) {
|
|
|
3355
3409
|
async function hasSocketListener() {
|
|
3356
3410
|
if (!existsSync6(COMPUTER_USE_SOCKET_PATH)) return false;
|
|
3357
3411
|
return await new Promise((resolve5) => {
|
|
3358
|
-
const socket =
|
|
3412
|
+
const socket = createConnection3(COMPUTER_USE_SOCKET_PATH);
|
|
3359
3413
|
socket.setTimeout(500, () => socket.destroy());
|
|
3360
3414
|
socket.once("connect", () => {
|
|
3361
3415
|
socket.destroy();
|
|
@@ -3398,18 +3452,18 @@ async function runComputerDaemon() {
|
|
|
3398
3452
|
if (rejected) return;
|
|
3399
3453
|
socket.setTimeout(0);
|
|
3400
3454
|
queue = queue.then(async () => {
|
|
3401
|
-
let
|
|
3455
|
+
let request2;
|
|
3402
3456
|
try {
|
|
3403
|
-
|
|
3457
|
+
request2 = JSON.parse(input);
|
|
3404
3458
|
} catch {
|
|
3405
3459
|
writeResult(socket, { exitCode: 2, stdout: "", stderr: "Invalid computer-use request JSON\n" });
|
|
3406
3460
|
return;
|
|
3407
3461
|
}
|
|
3408
|
-
if (!isDaemonRequest(
|
|
3462
|
+
if (!isDaemonRequest(request2)) {
|
|
3409
3463
|
writeResult(socket, { exitCode: 2, stdout: "", stderr: "Invalid or incompatible computer-use request\n" });
|
|
3410
3464
|
return;
|
|
3411
3465
|
}
|
|
3412
|
-
writeResult(socket, await runDirectCommand(
|
|
3466
|
+
writeResult(socket, await runDirectCommand(request2.argv, request2.cwd));
|
|
3413
3467
|
}).catch((error) => {
|
|
3414
3468
|
writeResult(socket, { exitCode: 1, stdout: "", stderr: `${error instanceof Error ? error.message : String(error)}
|
|
3415
3469
|
` });
|
|
@@ -3454,7 +3508,7 @@ var ComputerDaemonUnavailableError = class extends Error {
|
|
|
3454
3508
|
};
|
|
3455
3509
|
function sendComputerRequest(argv) {
|
|
3456
3510
|
return new Promise((resolve5, reject) => {
|
|
3457
|
-
const socket =
|
|
3511
|
+
const socket = createConnection4(COMPUTER_USE_SOCKET_PATH);
|
|
3458
3512
|
let response = "";
|
|
3459
3513
|
let settled = false;
|
|
3460
3514
|
const finish = (error, result) => {
|
|
@@ -3505,7 +3559,7 @@ async function runComputerClient(argv) {
|
|
|
3505
3559
|
}
|
|
3506
3560
|
|
|
3507
3561
|
// src/media-server.ts
|
|
3508
|
-
import { spawn as
|
|
3562
|
+
import { spawn as spawn7 } from "child_process";
|
|
3509
3563
|
import { createHash as createHash3 } from "crypto";
|
|
3510
3564
|
import { createReadStream, existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync8, statSync as statSync2 } from "fs";
|
|
3511
3565
|
import { createServer as createServer3 } from "http";
|
|
@@ -3515,7 +3569,7 @@ import { fileURLToPath as fileURLToPath2 } from "url";
|
|
|
3515
3569
|
import WebSocket2, { WebSocketServer } from "ws";
|
|
3516
3570
|
|
|
3517
3571
|
// src/encoder.ts
|
|
3518
|
-
import { spawn as
|
|
3572
|
+
import { spawn as spawn5 } from "child_process";
|
|
3519
3573
|
import { createSocket } from "dgram";
|
|
3520
3574
|
import { createInterface } from "readline";
|
|
3521
3575
|
|
|
@@ -3750,7 +3804,7 @@ function superviseEncoder(options) {
|
|
|
3750
3804
|
const warmupFrames = active ? options.warmupFrames ?? 0 : 0;
|
|
3751
3805
|
const command = options.command?.() ?? "ffmpeg";
|
|
3752
3806
|
const args = options.args(socket.address().port, warmupFrames);
|
|
3753
|
-
const child =
|
|
3807
|
+
const child = spawn5(command, args, { env: options.env, stdio: [options.controllable?.(command) ? "pipe" : "ignore", options.onOutput ? "pipe" : "ignore", "pipe"] });
|
|
3754
3808
|
child.stdin?.on("error", () => {
|
|
3755
3809
|
});
|
|
3756
3810
|
child.stderr?.on("data", (chunk) => process.stderr.write(`[desktop-stream] ${chunk}`));
|
|
@@ -3932,7 +3986,11 @@ function readReplicasRuntimeEnv(readEnv, [namespacedKey, legacyKey]) {
|
|
|
3932
3986
|
return readEnv(namespacedKey) ?? readEnv(legacyKey);
|
|
3933
3987
|
}
|
|
3934
3988
|
function parsePosixEnvFile(content) {
|
|
3989
|
+
return parsePosixEnv(content).values;
|
|
3990
|
+
}
|
|
3991
|
+
function parsePosixEnv(content) {
|
|
3935
3992
|
const result = {};
|
|
3993
|
+
const unsetKeys = [];
|
|
3936
3994
|
let pos = 0;
|
|
3937
3995
|
while (pos < content.length) {
|
|
3938
3996
|
let lineStart;
|
|
@@ -3945,6 +4003,9 @@ function parsePosixEnvFile(content) {
|
|
|
3945
4003
|
}
|
|
3946
4004
|
if (!content.startsWith("export ", lineStart)) {
|
|
3947
4005
|
const nl2 = content.indexOf("\n", lineStart);
|
|
4006
|
+
if (content.startsWith("unset ", lineStart)) {
|
|
4007
|
+
unsetKeys.push(content.slice(lineStart + "unset ".length, nl2 === -1 ? void 0 : nl2).trim());
|
|
4008
|
+
}
|
|
3948
4009
|
if (nl2 === -1) break;
|
|
3949
4010
|
pos = nl2 + 1;
|
|
3950
4011
|
continue;
|
|
@@ -3983,7 +4044,7 @@ function parsePosixEnvFile(content) {
|
|
|
3983
4044
|
const nl = content.indexOf("\n", cursor);
|
|
3984
4045
|
pos = nl === -1 ? content.length : nl + 1;
|
|
3985
4046
|
}
|
|
3986
|
-
return result;
|
|
4047
|
+
return { values: result, unsetKeys };
|
|
3987
4048
|
}
|
|
3988
4049
|
|
|
3989
4050
|
// src/ice-servers.ts
|
|
@@ -4674,7 +4735,7 @@ function superviseVideoEncoder(options) {
|
|
|
4674
4735
|
}
|
|
4675
4736
|
|
|
4676
4737
|
// src/input-bridge.ts
|
|
4677
|
-
import { spawn as
|
|
4738
|
+
import { spawn as spawn6 } from "child_process";
|
|
4678
4739
|
var scrollPixelsPerStep = 80;
|
|
4679
4740
|
var namedKeys = {
|
|
4680
4741
|
AltLeft: "Alt_L",
|
|
@@ -4855,7 +4916,7 @@ var InputBridge = class {
|
|
|
4855
4916
|
}
|
|
4856
4917
|
write(command) {
|
|
4857
4918
|
if (!this.process || this.process.exitCode !== null || this.process.stdin.destroyed) {
|
|
4858
|
-
const child =
|
|
4919
|
+
const child = spawn6("xte", [], {
|
|
4859
4920
|
env: { ...process.env, DISPLAY: this.display },
|
|
4860
4921
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4861
4922
|
});
|
|
@@ -4956,7 +5017,7 @@ function drainDesktopTheme() {
|
|
|
4956
5017
|
if (applied) appliedTheme = theme;
|
|
4957
5018
|
drainDesktopTheme();
|
|
4958
5019
|
};
|
|
4959
|
-
const child =
|
|
5020
|
+
const child = spawn7("bash", [SERVICES_SCRIPT, "--theme", theme], { detached: true, stdio: "ignore" });
|
|
4960
5021
|
child.unref();
|
|
4961
5022
|
child.once("exit", (code) => finish(code === 0));
|
|
4962
5023
|
child.once("error", () => finish(false));
|
|
@@ -5263,8 +5324,8 @@ async function runMediaServer() {
|
|
|
5263
5324
|
for (const viewer of viewers.values()) viewer.rtc?.setBudget(budgetBps());
|
|
5264
5325
|
syncEncoder();
|
|
5265
5326
|
};
|
|
5266
|
-
const resetLadder = (
|
|
5267
|
-
rung =
|
|
5327
|
+
const resetLadder = (request2) => {
|
|
5328
|
+
rung = request2.transport === "webrtc" && request2.adaptive ? webrtcStartupRung : 0;
|
|
5268
5329
|
cleanTransportSeconds = 0;
|
|
5269
5330
|
descents = [];
|
|
5270
5331
|
failedRung = -1;
|
|
@@ -5692,8 +5753,8 @@ async function runMediaServer() {
|
|
|
5692
5753
|
audioSampleRate: 48e3,
|
|
5693
5754
|
audioChannels: 2
|
|
5694
5755
|
};
|
|
5695
|
-
const httpServer = createServer3((
|
|
5696
|
-
const url = new URL(
|
|
5756
|
+
const httpServer = createServer3((request2, response) => {
|
|
5757
|
+
const url = new URL(request2.url || "/", "http://localhost");
|
|
5697
5758
|
if (url.pathname === "/health" || url.pathname === "/telemetry") {
|
|
5698
5759
|
const all = [...viewers.values()];
|
|
5699
5760
|
const memory = process.memoryUsage();
|
|
@@ -5746,7 +5807,7 @@ async function runMediaServer() {
|
|
|
5746
5807
|
return;
|
|
5747
5808
|
}
|
|
5748
5809
|
if (url.pathname === "/snapshot") {
|
|
5749
|
-
const capture =
|
|
5810
|
+
const capture = spawn7("ffmpeg", [
|
|
5750
5811
|
"-hide_banner",
|
|
5751
5812
|
"-loglevel",
|
|
5752
5813
|
"error",
|
|
@@ -5803,20 +5864,20 @@ async function runMediaServer() {
|
|
|
5803
5864
|
createReadStream(path).pipe(response);
|
|
5804
5865
|
});
|
|
5805
5866
|
const webSocketServer = new WebSocketServer({ noServer: true, maxPayload: 64 * 1024 });
|
|
5806
|
-
httpServer.on("upgrade", (
|
|
5807
|
-
const url = new URL(
|
|
5867
|
+
httpServer.on("upgrade", (request2, socket, head) => {
|
|
5868
|
+
const url = new URL(request2.url || "/", "http://localhost");
|
|
5808
5869
|
if (url.pathname !== "/stream") {
|
|
5809
5870
|
socket.destroy();
|
|
5810
5871
|
return;
|
|
5811
5872
|
}
|
|
5812
5873
|
const streamRequest = parseStreamRequest(url.searchParams, defaultRequest);
|
|
5813
|
-
void (webrtc && streamRequest.transport === "webrtc" ? currentIceServers() : Promise.resolve(void 0)).then((ice) => webSocketServer.handleUpgrade(
|
|
5874
|
+
void (webrtc && streamRequest.transport === "webrtc" ? currentIceServers() : Promise.resolve(void 0)).then((ice) => webSocketServer.handleUpgrade(request2, socket, head, (client) => webSocketServer.emit("connection", client, streamRequest, url.searchParams.get("standby") === "1", ice)));
|
|
5814
5875
|
});
|
|
5815
|
-
webSocketServer.on("connection", (socket,
|
|
5876
|
+
webSocketServer.on("connection", (socket, request2, standby, ice) => {
|
|
5816
5877
|
const viewer = {
|
|
5817
5878
|
socket,
|
|
5818
5879
|
input: new InputBridge(display, width, height),
|
|
5819
|
-
request,
|
|
5880
|
+
request: request2,
|
|
5820
5881
|
standby,
|
|
5821
5882
|
fallback: false,
|
|
5822
5883
|
waitingForKeyframe: true,
|
|
@@ -5834,23 +5895,23 @@ async function runMediaServer() {
|
|
|
5834
5895
|
rttFloorMs: 0,
|
|
5835
5896
|
rttSamples: []
|
|
5836
5897
|
};
|
|
5837
|
-
if (viewers.size === 0) resetLadder(
|
|
5838
|
-
if (
|
|
5839
|
-
record("viewer request", `${
|
|
5840
|
-
activeProfile =
|
|
5841
|
-
adaptive =
|
|
5898
|
+
if (viewers.size === 0) resetLadder(request2);
|
|
5899
|
+
if (request2.profile !== activeProfile || request2.adaptive !== adaptive) {
|
|
5900
|
+
record("viewer request", `${request2.profile} profile, ${request2.adaptive ? "adaptive" : "pinned"} quality`);
|
|
5901
|
+
activeProfile = request2.profile;
|
|
5902
|
+
adaptive = request2.adaptive;
|
|
5842
5903
|
if (!adaptive) rung = 0;
|
|
5843
5904
|
}
|
|
5844
5905
|
viewers.set(socket, viewer);
|
|
5845
|
-
record("viewer connected", `${
|
|
5906
|
+
record("viewer connected", `${request2.transport} requested${standby ? " in standby" : ""}, ${viewers.size} connected`);
|
|
5846
5907
|
const reply = (message) => {
|
|
5847
5908
|
if (socket.readyState === WebSocket2.OPEN) socket.send(JSON.stringify(message));
|
|
5848
5909
|
};
|
|
5849
5910
|
const fallbackProfile = profile() === "high444" ? "main" : profile();
|
|
5850
|
-
const configMessage = { ...config, videoCodec: H264_PROFILES[fallbackProfile].codec, iceServers: ice?.browser, request };
|
|
5911
|
+
const configMessage = { ...config, videoCodec: H264_PROFILES[fallbackProfile].codec, iceServers: ice?.browser, request: request2 };
|
|
5851
5912
|
reply(configMessage);
|
|
5852
5913
|
if (ice) openWebRtc(viewer, reply, ice.native);
|
|
5853
|
-
if (!viewer.rtc) enterFallback(viewer,
|
|
5914
|
+
if (!viewer.rtc) enterFallback(viewer, request2.transport === "webrtc" ? "webrtc unavailable" : "websocket requested");
|
|
5854
5915
|
ensureEncoders();
|
|
5855
5916
|
socket.on("message", (raw, binary) => {
|
|
5856
5917
|
if (binary) return;
|
|
@@ -5908,7 +5969,7 @@ async function runMediaServer() {
|
|
|
5908
5969
|
|
|
5909
5970
|
// src/setup.ts
|
|
5910
5971
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
5911
|
-
import { accessSync, chmodSync as chmodSync2, constants, cpSync, existsSync as existsSync9, mkdirSync as mkdirSync7, readdirSync as readdirSync4, writeFileSync as
|
|
5972
|
+
import { accessSync, chmodSync as chmodSync2, constants, cpSync, existsSync as existsSync9, mkdirSync as mkdirSync7, readdirSync as readdirSync4, writeFileSync as writeFileSync7 } from "fs";
|
|
5912
5973
|
import { dirname as dirname8, resolve as resolve4 } from "path";
|
|
5913
5974
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
5914
5975
|
var dependencies = [
|
|
@@ -6053,13 +6114,13 @@ async function setupComputer(options = {}) {
|
|
|
6053
6114
|
chmodSync2("/usr/local/bin/replicas-start-desktop-services", 493);
|
|
6054
6115
|
chmodSync2("/usr/local/bin/replicas-chrome", 493);
|
|
6055
6116
|
chmodSync2("/usr/local/bin/replicas-terminal", 493);
|
|
6056
|
-
|
|
6117
|
+
writeFileSync7("/etc/systemd/system/replicas-computer.service", serviceUnit, { mode: 420 });
|
|
6057
6118
|
if (existsSync9("/run/systemd/system")) {
|
|
6058
6119
|
const reload = spawnSync6("systemctl", ["daemon-reload"], { stdio: "pipe" });
|
|
6059
6120
|
if (reload.status !== 0) throw new Error(`Failed to reload systemd: ${reload.stderr?.toString().trim() || `exit ${reload.status}`}`);
|
|
6060
6121
|
}
|
|
6061
6122
|
mkdirSync7("/usr/local/share/replicas-computer", { recursive: true });
|
|
6062
|
-
|
|
6123
|
+
writeFileSync7("/usr/local/share/replicas-computer/install.json", `${JSON.stringify({
|
|
6063
6124
|
version: COMPUTER_USE_VERSION,
|
|
6064
6125
|
protocolVersion: COMPUTER_USE_PROTOCOL_VERSION,
|
|
6065
6126
|
dependencies: found
|