coderemote 0.3.5 → 0.3.6
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 +19 -4
- package/dist/coderemote.js +541 -226
- package/package.json +4 -8
package/dist/coderemote.js
CHANGED
|
@@ -1,25 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// bin/coderemote.js
|
|
4
|
-
import { execFile as execFile8, spawn as spawn3 } from "node:child_process";
|
|
4
|
+
import { execFile as execFile8, execFileSync, spawn as spawn3 } from "node:child_process";
|
|
5
5
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
6
|
-
import
|
|
6
|
+
import fs14 from "node:fs";
|
|
7
7
|
import net2 from "node:net";
|
|
8
8
|
import os6 from "node:os";
|
|
9
|
-
import
|
|
9
|
+
import path12 from "node:path";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
|
-
import { promisify as
|
|
11
|
+
import { promisify as promisify4 } from "node:util";
|
|
12
12
|
import qrcode from "qrcode-terminal";
|
|
13
13
|
|
|
14
14
|
// ../../packages/agent-core/src/server.js
|
|
15
15
|
import { createReadStream, createWriteStream as createWriteStream2 } from "node:fs";
|
|
16
|
-
import
|
|
16
|
+
import fs8 from "node:fs/promises";
|
|
17
17
|
import http2 from "node:http";
|
|
18
18
|
import os5 from "node:os";
|
|
19
|
-
import
|
|
19
|
+
import path6 from "node:path";
|
|
20
20
|
import { pipeline as pipeline2 } from "node:stream/promises";
|
|
21
21
|
import { randomUUID as randomUUID4, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
22
|
-
import { execFile as execFile5 } from "node:child_process";
|
|
23
22
|
|
|
24
23
|
// ../../packages/protocol/index.js
|
|
25
24
|
var PROTOCOL_VERSION = 4;
|
|
@@ -118,7 +117,7 @@ var HttpError = class extends Error {
|
|
|
118
117
|
var Sandbox = class {
|
|
119
118
|
constructor(root2) {
|
|
120
119
|
this.root = path.resolve(root2);
|
|
121
|
-
this.realRoot = fsSync.realpathSync(this.root);
|
|
120
|
+
this.realRoot = fsSync.realpathSync.native(this.root);
|
|
122
121
|
}
|
|
123
122
|
#containsReal(p) {
|
|
124
123
|
return p === this.realRoot || p.startsWith(this.realRoot + path.sep);
|
|
@@ -1052,12 +1051,100 @@ var LANDING_HTML = `<!doctype html>
|
|
|
1052
1051
|
`;
|
|
1053
1052
|
|
|
1054
1053
|
// ../../packages/agent-core/src/ports.js
|
|
1055
|
-
import { execFile as
|
|
1056
|
-
import
|
|
1054
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
1055
|
+
import fs6 from "node:fs/promises";
|
|
1057
1056
|
import os from "node:os";
|
|
1057
|
+
|
|
1058
|
+
// ../../packages/agent-core/src/platform.js
|
|
1059
|
+
import { execFile as execFile2, spawn } from "node:child_process";
|
|
1060
|
+
import fs5 from "node:fs";
|
|
1061
|
+
import path4 from "node:path";
|
|
1062
|
+
var isWindows = process.platform === "win32";
|
|
1063
|
+
function shellQuote(arg) {
|
|
1064
|
+
return `'${String(arg).replace(/'/g, `'\\''`)}'`;
|
|
1065
|
+
}
|
|
1066
|
+
function runnable(file) {
|
|
1067
|
+
try {
|
|
1068
|
+
if (!isWindows) fs5.accessSync(file, fs5.constants.X_OK);
|
|
1069
|
+
return fs5.statSync(file).isFile();
|
|
1070
|
+
} catch {
|
|
1071
|
+
return false;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
function findExecutable(name, env = process.env) {
|
|
1075
|
+
const exts = isWindows && !path4.extname(name) ? (env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
|
|
1076
|
+
const match = (base2) => exts.map((e) => base2 + e).find(runnable) ?? null;
|
|
1077
|
+
if (name.includes("/") || name.includes(path4.sep)) return match(path4.resolve(name));
|
|
1078
|
+
for (const dir of (env.PATH ?? env.Path ?? "").split(path4.delimiter)) {
|
|
1079
|
+
const found = dir && match(path4.join(dir.replace(/^"(.*)"$/, "$1"), name));
|
|
1080
|
+
if (found) return found;
|
|
1081
|
+
}
|
|
1082
|
+
return null;
|
|
1083
|
+
}
|
|
1084
|
+
function interactiveShell() {
|
|
1085
|
+
if (!isWindows) return { file: process.env.SHELL || "/bin/sh", args: ["-l"] };
|
|
1086
|
+
return { file: findExecutable("pwsh") ?? "powershell.exe", args: ["-NoLogo"] };
|
|
1087
|
+
}
|
|
1088
|
+
function cmdQuote(arg) {
|
|
1089
|
+
const s = String(arg);
|
|
1090
|
+
if (/["%\r\n]/.test(s)) throw new Error(`${JSON.stringify(s)} can't be passed through cmd.exe`);
|
|
1091
|
+
return `"${s.replace(/(\\+)$/, "$1$1")}"`;
|
|
1092
|
+
}
|
|
1093
|
+
function resolveCommand(argv2) {
|
|
1094
|
+
const [name, ...rest2] = argv2;
|
|
1095
|
+
if (!isWindows) return { file: name, args: rest2, verbatim: false };
|
|
1096
|
+
const file = findExecutable(name) ?? name;
|
|
1097
|
+
if (!/\.(bat|cmd)$/i.test(file)) return { file, args: rest2, verbatim: false };
|
|
1098
|
+
const line = [file, ...rest2].map(cmdQuote).join(" ");
|
|
1099
|
+
return { file: process.env.ComSpec || "cmd.exe", args: ["/d", "/v:off", "/s", "/c", `"${line}"`], verbatim: true };
|
|
1100
|
+
}
|
|
1101
|
+
function terminalCommand(argv2) {
|
|
1102
|
+
if (!isWindows) return { file: interactiveShell().file, args: ["-l", "-c", `exec ${argv2.map(shellQuote).join(" ")}`] };
|
|
1103
|
+
const { file, args, verbatim } = resolveCommand(argv2);
|
|
1104
|
+
return { file, args: verbatim ? args.join(" ") : args };
|
|
1105
|
+
}
|
|
1106
|
+
function spawnCommand(argv2, options = {}) {
|
|
1107
|
+
const { file, args, verbatim } = resolveCommand(argv2);
|
|
1108
|
+
return spawn(file, args, { ...options, windowsVerbatimArguments: verbatim });
|
|
1109
|
+
}
|
|
1110
|
+
function execCommand(argv2, options = {}) {
|
|
1111
|
+
return new Promise((resolve, reject) => {
|
|
1112
|
+
const { file, args, verbatim } = resolveCommand(argv2);
|
|
1113
|
+
execFile2(
|
|
1114
|
+
file,
|
|
1115
|
+
args,
|
|
1116
|
+
{ ...options, windowsVerbatimArguments: verbatim },
|
|
1117
|
+
(err, stdout, stderr) => err ? reject(Object.assign(err, { stdout, stderr })) : resolve({ stdout, stderr })
|
|
1118
|
+
);
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
function loginShell(script, timeout) {
|
|
1122
|
+
return new Promise((resolve) => {
|
|
1123
|
+
execFile2(
|
|
1124
|
+
interactiveShell().file,
|
|
1125
|
+
["-l", "-c", script],
|
|
1126
|
+
{ timeout, maxBuffer: 16 * 1024 * 1024 },
|
|
1127
|
+
(err, out) => resolve(err ? null : out)
|
|
1128
|
+
);
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
async function capture(argv2, { timeout = 2e4 } = {}) {
|
|
1132
|
+
if (!isWindows) return loginShell(argv2.map(shellQuote).join(" "), timeout);
|
|
1133
|
+
try {
|
|
1134
|
+
return (await execCommand(argv2, { timeout, maxBuffer: 16 * 1024 * 1024 })).stdout;
|
|
1135
|
+
} catch {
|
|
1136
|
+
return null;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
async function commandExists(command) {
|
|
1140
|
+
if (isWindows) return findExecutable(command) !== null;
|
|
1141
|
+
return Boolean((await loginShell(`command -v ${shellQuote(command)}`, 2e4))?.trim());
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// ../../packages/agent-core/src/ports.js
|
|
1058
1145
|
function run(cmd2, args) {
|
|
1059
1146
|
return new Promise((resolve, reject) => {
|
|
1060
|
-
|
|
1147
|
+
execFile3(cmd2, args, { maxBuffer: 8 * 1024 * 1024 }, (err, stdout) => {
|
|
1061
1148
|
if (err?.code === "ENOENT") reject(err);
|
|
1062
1149
|
else resolve(stdout || "");
|
|
1063
1150
|
});
|
|
@@ -1109,9 +1196,9 @@ async function processTable() {
|
|
|
1109
1196
|
}
|
|
1110
1197
|
async function workingDirs(pids) {
|
|
1111
1198
|
const dirs = /* @__PURE__ */ new Map();
|
|
1112
|
-
if (!pids.length) return dirs;
|
|
1199
|
+
if (!pids.length || isWindows) return dirs;
|
|
1113
1200
|
if (os.platform() === "linux") {
|
|
1114
|
-
await Promise.all(pids.map(async (pid2) => dirs.set(pid2, await
|
|
1201
|
+
await Promise.all(pids.map(async (pid2) => dirs.set(pid2, await fs6.readlink(`/proc/${pid2}/cwd`).catch(() => null))));
|
|
1115
1202
|
return dirs;
|
|
1116
1203
|
}
|
|
1117
1204
|
const out = await run("lsof", ["-a", "-p", pids.join(","), "-d", "cwd", "-F", "pn"]).catch(() => "");
|
|
@@ -1122,9 +1209,39 @@ async function workingDirs(pids) {
|
|
|
1122
1209
|
}
|
|
1123
1210
|
return dirs;
|
|
1124
1211
|
}
|
|
1125
|
-
var
|
|
1212
|
+
var WINDOWS_SHELLS = /* @__PURE__ */ new Set(["cmd.exe", "powershell.exe", "pwsh.exe", "bash.exe", "wsl.exe", "windowsterminal.exe", "openconsole.exe"]);
|
|
1213
|
+
async function windowsListeners() {
|
|
1214
|
+
const query = "Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name,CommandLine | ConvertTo-Json -Compress";
|
|
1215
|
+
const [v4, v6, json] = await Promise.all([
|
|
1216
|
+
run("netstat", ["-ano", "-p", "TCP"]),
|
|
1217
|
+
run("netstat", ["-ano", "-p", "TCPv6"]),
|
|
1218
|
+
run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", query])
|
|
1219
|
+
]);
|
|
1220
|
+
const table = /* @__PURE__ */ new Map();
|
|
1221
|
+
for (const p of [JSON.parse(json || "[]")].flat()) {
|
|
1222
|
+
table.set(p.ProcessId, { ppid: p.ParentProcessId, name: p.Name, tty: null, command: p.CommandLine });
|
|
1223
|
+
}
|
|
1224
|
+
for (const proc of table.values()) {
|
|
1225
|
+
for (let p = proc.ppid, depth = 0; p && depth < 64; p = table.get(p)?.ppid, depth++) {
|
|
1226
|
+
if (WINDOWS_SHELLS.has(table.get(p)?.name?.toLowerCase())) {
|
|
1227
|
+
proc.tty = "console";
|
|
1228
|
+
break;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
const sockets = [];
|
|
1233
|
+
for (const line of `${v4}
|
|
1234
|
+
${v6}`.split(/\r?\n/)) {
|
|
1235
|
+
const m = line.match(/^\s*TCP\s+(\S+)\s+(\S+)\s+\S+\s+(\d+)\s*$/);
|
|
1236
|
+
const hp = m && m[2].endsWith(":0") && splitHostPort(m[1]);
|
|
1237
|
+
const pid = hp && parseInt(m[3], 10);
|
|
1238
|
+
if (hp && table.get(pid)?.command) sockets.push({ pid, name: table.get(pid).name, ...hp });
|
|
1239
|
+
}
|
|
1240
|
+
return [sockets, table];
|
|
1241
|
+
}
|
|
1242
|
+
var AGENT_COMMAND = /(^|[\/\\\s])coderemote(\.js)?"?\s+up(\s|$)/;
|
|
1126
1243
|
async function listPorts(shells = /* @__PURE__ */ new Map()) {
|
|
1127
|
-
const [sockets, table] = await Promise.all([listeningSockets(), processTable()]);
|
|
1244
|
+
const [sockets, table] = isWindows ? await windowsListeners() : await Promise.all([listeningSockets(), processTable()]);
|
|
1128
1245
|
const byKey = /* @__PURE__ */ new Map();
|
|
1129
1246
|
for (const s of sockets) {
|
|
1130
1247
|
const key = `${s.pid}:${s.port}`;
|
|
@@ -1172,11 +1289,11 @@ async function killListener(pid, { force = false, shells } = {}) {
|
|
|
1172
1289
|
}
|
|
1173
1290
|
|
|
1174
1291
|
// ../../packages/agent-core/src/push.js
|
|
1175
|
-
import { execFile as
|
|
1292
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
1176
1293
|
import { createCipheriv, randomBytes } from "node:crypto";
|
|
1177
1294
|
import os2 from "node:os";
|
|
1178
1295
|
import { promisify } from "node:util";
|
|
1179
|
-
var run2 = promisify(
|
|
1296
|
+
var run2 = promisify(execFile4);
|
|
1180
1297
|
var FINISHED_AFTER = 60 * 1e3;
|
|
1181
1298
|
var ACTIVE_WITHIN = 2 * 60;
|
|
1182
1299
|
async function macIdleSeconds() {
|
|
@@ -1568,17 +1685,25 @@ function detail(e) {
|
|
|
1568
1685
|
}
|
|
1569
1686
|
|
|
1570
1687
|
// ../../packages/agent-core/src/sessions.js
|
|
1688
|
+
import { execFile as execFile5 } from "node:child_process";
|
|
1571
1689
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1572
1690
|
import os3 from "node:os";
|
|
1573
1691
|
import pty from "node-pty";
|
|
1574
1692
|
var SCROLLBACK_LIMIT = 256 * 1024;
|
|
1575
|
-
function
|
|
1576
|
-
|
|
1693
|
+
function guardConpty(term) {
|
|
1694
|
+
term._agent?._inSocket?.on?.("error", () => {
|
|
1695
|
+
});
|
|
1696
|
+
term._agent?._conoutSocketWorker?._worker?.on?.("error", () => {
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1699
|
+
function releaseConpty(term) {
|
|
1700
|
+
term._agent?._conoutSocketWorker?.dispose?.();
|
|
1701
|
+
term._agent?._inSocket?.destroy?.();
|
|
1577
1702
|
}
|
|
1578
1703
|
var TerminalSession = class {
|
|
1579
1704
|
/**
|
|
1580
|
-
* `command` (an argv array) runs that program
|
|
1581
|
-
*
|
|
1705
|
+
* `command` (an argv array) runs that program instead of an interactive shell, started as
|
|
1706
|
+
* terminalCommand says; the session ends when it exits.
|
|
1582
1707
|
*/
|
|
1583
1708
|
constructor({ cols, rows, cwd, env = {}, command = null }) {
|
|
1584
1709
|
this.id = randomUUID2();
|
|
@@ -1587,11 +1712,10 @@ var TerminalSession = class {
|
|
|
1587
1712
|
this.bufferedBytes = 0;
|
|
1588
1713
|
this.clients = /* @__PURE__ */ new Set();
|
|
1589
1714
|
this.exitCode = null;
|
|
1590
|
-
const
|
|
1591
|
-
this.shell =
|
|
1715
|
+
const { file, args } = command ? terminalCommand(command) : interactiveShell();
|
|
1716
|
+
this.shell = command ? interactiveShell().file : file;
|
|
1592
1717
|
this.command = command;
|
|
1593
|
-
|
|
1594
|
-
this.pty = pty.spawn(shell, args, {
|
|
1718
|
+
this.pty = pty.spawn(file, args, {
|
|
1595
1719
|
name: "xterm-256color",
|
|
1596
1720
|
cols,
|
|
1597
1721
|
rows,
|
|
@@ -1599,9 +1723,11 @@ var TerminalSession = class {
|
|
|
1599
1723
|
// CODEREMOTE_TERM_ID lets `coderemote hook` tell which terminal a coding agent runs in.
|
|
1600
1724
|
env: { ...process.env, ...env, CODEREMOTE_TERM_ID: this.id, TERM: "xterm-256color", COLORTERM: "truecolor" }
|
|
1601
1725
|
});
|
|
1726
|
+
if (isWindows) guardConpty(this.pty);
|
|
1602
1727
|
this.pty.onData((data) => this.#output(Buffer.from(data, "utf8")));
|
|
1603
1728
|
this.pty.onExit(({ exitCode }) => {
|
|
1604
1729
|
this.exitCode = exitCode;
|
|
1730
|
+
if (isWindows) releaseConpty(this.pty);
|
|
1605
1731
|
for (const ws of this.clients) {
|
|
1606
1732
|
ws.send(JSON.stringify({ type: "exit", code: exitCode }));
|
|
1607
1733
|
ws.close(1e3, "session exited");
|
|
@@ -1624,7 +1750,7 @@ var TerminalSession = class {
|
|
|
1624
1750
|
if (replay.length) ws.send(replay, { binary: true });
|
|
1625
1751
|
ws.on("message", (data, isBinary) => {
|
|
1626
1752
|
if (isBinary) {
|
|
1627
|
-
this.
|
|
1753
|
+
this.write(data.toString("utf8"));
|
|
1628
1754
|
return;
|
|
1629
1755
|
}
|
|
1630
1756
|
let msg;
|
|
@@ -1633,22 +1759,25 @@ var TerminalSession = class {
|
|
|
1633
1759
|
} catch {
|
|
1634
1760
|
return;
|
|
1635
1761
|
}
|
|
1636
|
-
if (msg.type === "input" && typeof msg.data === "string") this.
|
|
1762
|
+
if (msg.type === "input" && typeof msg.data === "string") this.write(msg.data);
|
|
1637
1763
|
else if (msg.type === "resize") this.resize(msg.cols, msg.rows);
|
|
1638
1764
|
});
|
|
1639
1765
|
ws.on("close", () => this.clients.delete(ws));
|
|
1640
1766
|
}
|
|
1641
1767
|
resize(cols, rows) {
|
|
1642
|
-
if (Number.isInteger(cols) && Number.isInteger(rows) && cols > 0 && rows > 0) {
|
|
1768
|
+
if (this.exitCode === null && Number.isInteger(cols) && Number.isInteger(rows) && cols > 0 && rows > 0) {
|
|
1643
1769
|
this.pty.resize(cols, rows);
|
|
1644
1770
|
}
|
|
1645
1771
|
}
|
|
1772
|
+
/** Ends the session; on Windows, which has no signals, always forcibly (see releaseConpty). */
|
|
1646
1773
|
kill(signal) {
|
|
1647
|
-
this.pty.kill(signal);
|
|
1774
|
+
if (!isWindows) this.pty.kill(signal);
|
|
1775
|
+
else if (this.exitCode === null) execFile5("taskkill", ["/T", "/F", "/PID", String(this.pty.pid)], () => {
|
|
1776
|
+
});
|
|
1648
1777
|
}
|
|
1649
|
-
/** Types into the session, as a client's keyboard would. */
|
|
1778
|
+
/** Types into the session, as a client's keyboard would; nothing once it has exited. */
|
|
1650
1779
|
write(data) {
|
|
1651
|
-
this.pty.write(data);
|
|
1780
|
+
if (this.exitCode === null) this.pty.write(data);
|
|
1652
1781
|
}
|
|
1653
1782
|
toJSON() {
|
|
1654
1783
|
return {
|
|
@@ -1682,28 +1811,42 @@ var SessionManager = class {
|
|
|
1682
1811
|
list() {
|
|
1683
1812
|
return [...this.#sessions.values()].map((s) => s.toJSON());
|
|
1684
1813
|
}
|
|
1685
|
-
|
|
1686
|
-
|
|
1814
|
+
/**
|
|
1815
|
+
* Ends every session; resolves once they have all exited, or after `timeout` ms. Wait for it
|
|
1816
|
+
* before exiting: on Windows terminals end asynchronously (see releaseConpty), and exiting
|
|
1817
|
+
* while node-pty still holds a console open can crash the process.
|
|
1818
|
+
*/
|
|
1819
|
+
killAll(timeout = 3e3) {
|
|
1820
|
+
const exited = [...this.#sessions.values()].map((s) => {
|
|
1821
|
+
const done = new Promise((resolve) => s.pty.onExit(resolve));
|
|
1822
|
+
s.kill();
|
|
1823
|
+
return done;
|
|
1824
|
+
});
|
|
1825
|
+
let timer;
|
|
1826
|
+
const late = new Promise((resolve) => {
|
|
1827
|
+
timer = setTimeout(resolve, timeout);
|
|
1828
|
+
});
|
|
1829
|
+
return Promise.race([Promise.all(exited), late]).finally(() => clearTimeout(timer));
|
|
1687
1830
|
}
|
|
1688
1831
|
};
|
|
1689
1832
|
|
|
1690
1833
|
// ../../packages/agent-core/src/uploads.js
|
|
1691
|
-
import { execFile as
|
|
1834
|
+
import { execFile as execFile6 } from "node:child_process";
|
|
1692
1835
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
1693
1836
|
import { createWriteStream } from "node:fs";
|
|
1694
|
-
import
|
|
1837
|
+
import fs7 from "node:fs/promises";
|
|
1695
1838
|
import os4 from "node:os";
|
|
1696
|
-
import
|
|
1839
|
+
import path5 from "node:path";
|
|
1697
1840
|
import { Transform } from "node:stream";
|
|
1698
1841
|
import { pipeline } from "node:stream/promises";
|
|
1699
1842
|
import { promisify as promisify2 } from "node:util";
|
|
1700
|
-
var run3 = promisify2(
|
|
1843
|
+
var run3 = promisify2(execFile6);
|
|
1701
1844
|
var UPLOAD_LIMIT = 100 * 1024 * 1024;
|
|
1702
1845
|
var KEEP = 7 * 24 * 60 * 60 * 1e3;
|
|
1703
1846
|
var ID = /^[0-9a-f-]{36}$/;
|
|
1704
1847
|
function safeName(name) {
|
|
1705
|
-
const
|
|
1706
|
-
const clean =
|
|
1848
|
+
const base2 = path5.basename(String(name ?? "").replace(/\\/g, "/"));
|
|
1849
|
+
const clean = base2.replace(/[^\p{L}\p{N}._ -]/gu, "_").replace(/^[.\s]+/, "").slice(-80).trim();
|
|
1707
1850
|
return clean || "file";
|
|
1708
1851
|
}
|
|
1709
1852
|
var UploadStore = class {
|
|
@@ -1716,8 +1859,8 @@ var UploadStore = class {
|
|
|
1716
1859
|
if (Number(length) > limit) throw new HttpError(413, `files are limited to ${limit / 1024 / 1024} MB`);
|
|
1717
1860
|
const id = randomUUID3();
|
|
1718
1861
|
const day = new Date(this.now()).toISOString().slice(0, 10);
|
|
1719
|
-
await
|
|
1720
|
-
let file =
|
|
1862
|
+
await fs7.mkdir(path5.join(this.dir, day), { recursive: true, mode: 448 });
|
|
1863
|
+
let file = path5.join(this.dir, day, `${id}-${safeName(name)}`);
|
|
1721
1864
|
let size = 0;
|
|
1722
1865
|
const counter = new Transform({
|
|
1723
1866
|
transform(chunk, _enc, done) {
|
|
@@ -1728,7 +1871,7 @@ var UploadStore = class {
|
|
|
1728
1871
|
try {
|
|
1729
1872
|
await pipeline(body, counter, createWriteStream(file, { mode: 384 }));
|
|
1730
1873
|
} catch (e) {
|
|
1731
|
-
await
|
|
1874
|
+
await fs7.rm(file, { force: true });
|
|
1732
1875
|
throw e;
|
|
1733
1876
|
}
|
|
1734
1877
|
let kind = type || "application/octet-stream";
|
|
@@ -1736,31 +1879,31 @@ var UploadStore = class {
|
|
|
1736
1879
|
const jpeg = file.replace(/(\.heic)?$/i, ".jpg");
|
|
1737
1880
|
try {
|
|
1738
1881
|
await run3("sips", ["-s", "format", "jpeg", file, "--out", jpeg], { timeout: 3e4 });
|
|
1739
|
-
await
|
|
1882
|
+
await fs7.rm(file);
|
|
1740
1883
|
file = jpeg;
|
|
1741
1884
|
kind = "image/jpeg";
|
|
1742
|
-
size = (await
|
|
1885
|
+
size = (await fs7.stat(file)).size;
|
|
1743
1886
|
} catch {
|
|
1744
1887
|
}
|
|
1745
1888
|
}
|
|
1746
|
-
return { id, name:
|
|
1889
|
+
return { id, name: path5.basename(file).slice(id.length + 1), type: kind, size, path: file };
|
|
1747
1890
|
}
|
|
1748
1891
|
/** The file of an upload; 404 once it's gone (swept, or never existed). */
|
|
1749
1892
|
async path(id) {
|
|
1750
1893
|
if (typeof id !== "string" || !ID.test(id)) throw new HttpError(400, "invalid upload id");
|
|
1751
1894
|
let days = [];
|
|
1752
1895
|
try {
|
|
1753
|
-
days = (await
|
|
1896
|
+
days = (await fs7.readdir(this.dir)).sort().reverse();
|
|
1754
1897
|
} catch {
|
|
1755
1898
|
}
|
|
1756
1899
|
for (const day of days) {
|
|
1757
1900
|
let names = [];
|
|
1758
1901
|
try {
|
|
1759
|
-
names = await
|
|
1902
|
+
names = await fs7.readdir(path5.join(this.dir, day));
|
|
1760
1903
|
} catch {
|
|
1761
1904
|
}
|
|
1762
1905
|
const hit = names.find((n) => n.startsWith(`${id}-`));
|
|
1763
|
-
if (hit) return
|
|
1906
|
+
if (hit) return path5.join(this.dir, day, hit);
|
|
1764
1907
|
}
|
|
1765
1908
|
throw new HttpError(404, "no such upload (they are kept for 7 days)");
|
|
1766
1909
|
}
|
|
@@ -1768,25 +1911,25 @@ var UploadStore = class {
|
|
|
1768
1911
|
async sweep() {
|
|
1769
1912
|
let days = [];
|
|
1770
1913
|
try {
|
|
1771
|
-
days = await
|
|
1914
|
+
days = await fs7.readdir(this.dir);
|
|
1772
1915
|
} catch {
|
|
1773
1916
|
return;
|
|
1774
1917
|
}
|
|
1775
1918
|
const cutoff = this.now() - KEEP;
|
|
1776
1919
|
for (const day of days) {
|
|
1777
|
-
const dir =
|
|
1920
|
+
const dir = path5.join(this.dir, day);
|
|
1778
1921
|
let names = [];
|
|
1779
1922
|
try {
|
|
1780
|
-
names = await
|
|
1923
|
+
names = await fs7.readdir(dir);
|
|
1781
1924
|
} catch {
|
|
1782
1925
|
continue;
|
|
1783
1926
|
}
|
|
1784
1927
|
for (const n of names) {
|
|
1785
|
-
const file =
|
|
1786
|
-
const stat = await
|
|
1787
|
-
if (stat && stat.mtimeMs < cutoff) await
|
|
1928
|
+
const file = path5.join(dir, n);
|
|
1929
|
+
const stat = await fs7.stat(file).catch(() => null);
|
|
1930
|
+
if (stat && stat.mtimeMs < cutoff) await fs7.rm(file, { force: true });
|
|
1788
1931
|
}
|
|
1789
|
-
if (!(await
|
|
1932
|
+
if (!(await fs7.readdir(dir).catch(() => [])).length) await fs7.rm(dir, { recursive: true, force: true });
|
|
1790
1933
|
}
|
|
1791
1934
|
}
|
|
1792
1935
|
};
|
|
@@ -1799,11 +1942,11 @@ function kindOf(stat) {
|
|
|
1799
1942
|
return "other";
|
|
1800
1943
|
}
|
|
1801
1944
|
async function describe(fullPath, name) {
|
|
1802
|
-
const lst = await
|
|
1945
|
+
const lst = await fs8.lstat(fullPath);
|
|
1803
1946
|
const entry2 = { name, type: kindOf(lst), size: lst.size, mtime: lst.mtimeMs };
|
|
1804
1947
|
if (lst.isSymbolicLink()) {
|
|
1805
1948
|
try {
|
|
1806
|
-
entry2.target = kindOf(await
|
|
1949
|
+
entry2.target = kindOf(await fs8.stat(fullPath));
|
|
1807
1950
|
} catch {
|
|
1808
1951
|
entry2.target = "broken";
|
|
1809
1952
|
}
|
|
@@ -1843,7 +1986,7 @@ function createAgentServer({
|
|
|
1843
1986
|
uploadsDir = null,
|
|
1844
1987
|
claudeCommand = "claude",
|
|
1845
1988
|
codexCommand = "codex",
|
|
1846
|
-
codexHome = process.env.CODEX_HOME ||
|
|
1989
|
+
codexHome = process.env.CODEX_HOME || path6.join(os5.homedir(), ".codex"),
|
|
1847
1990
|
pluginInstalled = null,
|
|
1848
1991
|
agents = new AgentHub(),
|
|
1849
1992
|
pushStore: pushStore2 = null,
|
|
@@ -1898,17 +2041,6 @@ function createAgentServer({
|
|
|
1898
2041
|
if (!Array.isArray(ids) || ids.some((a) => typeof a !== "string")) throw new HttpError(400, "attachments must be upload ids");
|
|
1899
2042
|
return Promise.all(ids.map((a) => uploadPath(a)));
|
|
1900
2043
|
}
|
|
1901
|
-
function loginShell(script) {
|
|
1902
|
-
const shell = process.env.SHELL || "/bin/sh";
|
|
1903
|
-
return new Promise((resolve) => {
|
|
1904
|
-
execFile5(
|
|
1905
|
-
shell,
|
|
1906
|
-
["-l", "-c", script],
|
|
1907
|
-
{ timeout: 2e4, maxBuffer: 16 * 1024 * 1024 },
|
|
1908
|
-
(err, out) => resolve(err ? null : out)
|
|
1909
|
-
);
|
|
1910
|
-
});
|
|
1911
|
-
}
|
|
1912
2044
|
const launchers = {
|
|
1913
2045
|
claude: {
|
|
1914
2046
|
command: claudeCommand,
|
|
@@ -1923,7 +2055,7 @@ function createAgentServer({
|
|
|
1923
2055
|
// terminal's queries; the classic renderer doesn't need them.
|
|
1924
2056
|
env: { CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN: "1" },
|
|
1925
2057
|
async plugin() {
|
|
1926
|
-
const list = JSON.parse(await
|
|
2058
|
+
const list = JSON.parse(await capture([claudeCommand, "plugin", "list", "--json"]) ?? "[]");
|
|
1927
2059
|
return list.some((p) => p.id === "coderemote@coderemote" && p.enabled !== false);
|
|
1928
2060
|
},
|
|
1929
2061
|
missingPlugin: "Claude Code needs the Code Remote plugin first: run `coderemote setup claude` once on the computer."
|
|
@@ -1941,14 +2073,14 @@ function createAgentServer({
|
|
|
1941
2073
|
],
|
|
1942
2074
|
env: {},
|
|
1943
2075
|
async plugin() {
|
|
1944
|
-
const list = JSON.parse(await
|
|
2076
|
+
const list = JSON.parse(await capture([codexCommand, "plugin", "list", "--marketplace", "coderemote", "--json"]) ?? "{}");
|
|
1945
2077
|
return (list.installed ?? []).some((p) => p.pluginId === "coderemote@coderemote" && p.enabled !== false);
|
|
1946
2078
|
},
|
|
1947
2079
|
missingPlugin: "Codex needs the Code Remote plugin first: run `coderemote setup codex` once on the computer, then trust its hooks with `/hooks` in Codex."
|
|
1948
2080
|
}
|
|
1949
2081
|
};
|
|
1950
|
-
|
|
1951
|
-
return
|
|
2082
|
+
function installed(agent) {
|
|
2083
|
+
return commandExists(launchers[agent].command);
|
|
1952
2084
|
}
|
|
1953
2085
|
const plugins = {};
|
|
1954
2086
|
async function hasPlugin(agent) {
|
|
@@ -1964,7 +2096,7 @@ function createAgentServer({
|
|
|
1964
2096
|
}
|
|
1965
2097
|
async function codexModels() {
|
|
1966
2098
|
try {
|
|
1967
|
-
const { models } = JSON.parse(await
|
|
2099
|
+
const { models } = JSON.parse(await fs8.readFile(path6.join(codexHome, "models_cache.json"), "utf8"));
|
|
1968
2100
|
return models.filter((m) => m.visibility === "list" && typeof m.slug === "string").sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0)).map((m) => ({ id: m.slug, name: m.display_name || m.slug, description: m.description ?? "" }));
|
|
1969
2101
|
} catch {
|
|
1970
2102
|
return [];
|
|
@@ -2014,20 +2146,20 @@ function createAgentServer({
|
|
|
2014
2146
|
async function repoOf(p) {
|
|
2015
2147
|
const repo = await sandbox.resolve(p);
|
|
2016
2148
|
try {
|
|
2017
|
-
await
|
|
2149
|
+
await fs8.access(path6.join(repo, ".git"));
|
|
2018
2150
|
} catch {
|
|
2019
2151
|
throw new HttpError(400, "not a git repository");
|
|
2020
2152
|
}
|
|
2021
2153
|
return repo;
|
|
2022
2154
|
}
|
|
2023
2155
|
async function filesIn(repo, files) {
|
|
2024
|
-
if (!Array.isArray(files) || !files.length || files.some((f) => typeof f !== "string" || !f ||
|
|
2156
|
+
if (!Array.isArray(files) || !files.length || files.some((f) => typeof f !== "string" || !f || path6.isAbsolute(f))) {
|
|
2025
2157
|
throw new HttpError(400, "files must be a non-empty list of paths relative to the repository");
|
|
2026
2158
|
}
|
|
2027
2159
|
return Promise.all(
|
|
2028
2160
|
files.map(async (f) => {
|
|
2029
|
-
const abs = await sandbox.resolve(
|
|
2030
|
-
const rel =
|
|
2161
|
+
const abs = await sandbox.resolve(path6.join(repo, f), { allowMissing: true });
|
|
2162
|
+
const rel = path6.relative(repo, abs);
|
|
2031
2163
|
if (!rel || rel.startsWith("..")) throw new HttpError(403, "path is outside the repository");
|
|
2032
2164
|
return rel;
|
|
2033
2165
|
})
|
|
@@ -2040,21 +2172,22 @@ function createAgentServer({
|
|
|
2040
2172
|
platform: os5.platform(),
|
|
2041
2173
|
root: sandbox.root,
|
|
2042
2174
|
user: os5.userInfo().username,
|
|
2043
|
-
|
|
2175
|
+
// On Windows, terminals run PowerShell whatever $SHELL (Git Bash sets one) says.
|
|
2176
|
+
shell: isWindows ? interactiveShell().file : process.env.SHELL || null
|
|
2044
2177
|
}),
|
|
2045
2178
|
[Routes.fsList]: async (url) => {
|
|
2046
2179
|
const dir = await sandbox.resolve(url.searchParams.get("path"));
|
|
2047
|
-
const names = await
|
|
2048
|
-
const entries = (await Promise.all(names.map((n) => describe(
|
|
2180
|
+
const names = await fs8.readdir(dir);
|
|
2181
|
+
const entries = (await Promise.all(names.map((n) => describe(path6.join(dir, n), n).catch(() => null)))).filter(Boolean);
|
|
2049
2182
|
return { path: dir, entries };
|
|
2050
2183
|
},
|
|
2051
2184
|
[Routes.fsStat]: async (url) => {
|
|
2052
2185
|
const p = await sandbox.resolve(url.searchParams.get("path"));
|
|
2053
|
-
return { path: p, ...await describe(p,
|
|
2186
|
+
return { path: p, ...await describe(p, path6.basename(p)) };
|
|
2054
2187
|
},
|
|
2055
2188
|
[Routes.fsRead]: async (url, req, res) => {
|
|
2056
2189
|
const p = await sandbox.resolve(url.searchParams.get("path"));
|
|
2057
|
-
const stat = await
|
|
2190
|
+
const stat = await fs8.stat(p);
|
|
2058
2191
|
if (!stat.isFile()) throw new HttpError(400, "not a file");
|
|
2059
2192
|
res.writeHead(200, { "content-type": "application/octet-stream", "content-length": stat.size });
|
|
2060
2193
|
await pipeline2(createReadStream(p), res);
|
|
@@ -2064,24 +2197,24 @@ function createAgentServer({
|
|
|
2064
2197
|
const tmp = `${p}.vsremote-${process.pid}-${Date.now()}.tmp`;
|
|
2065
2198
|
try {
|
|
2066
2199
|
await pipeline2(req, createWriteStream2(tmp));
|
|
2067
|
-
await
|
|
2200
|
+
await fs8.rename(tmp, p);
|
|
2068
2201
|
} catch (e) {
|
|
2069
|
-
await
|
|
2202
|
+
await fs8.rm(tmp, { force: true });
|
|
2070
2203
|
throw e;
|
|
2071
2204
|
}
|
|
2072
|
-
return describe(p,
|
|
2205
|
+
return describe(p, path6.basename(p));
|
|
2073
2206
|
},
|
|
2074
2207
|
[Routes.fsMkdir]: async (url) => {
|
|
2075
|
-
await
|
|
2208
|
+
await fs8.mkdir(await sandbox.resolve(url.searchParams.get("path"), { allowMissing: true }), { recursive: true });
|
|
2076
2209
|
return {};
|
|
2077
2210
|
},
|
|
2078
2211
|
[Routes.fsRename]: async (url, req) => {
|
|
2079
2212
|
const { from, to } = await readJson(req);
|
|
2080
|
-
await
|
|
2213
|
+
await fs8.rename(await sandbox.resolveChild(from), await sandbox.resolveChild(to, { allowMissing: true }));
|
|
2081
2214
|
return {};
|
|
2082
2215
|
},
|
|
2083
2216
|
[Routes.fsDelete]: async (url) => {
|
|
2084
|
-
await
|
|
2217
|
+
await fs8.rm(await sandbox.resolveChild(url.searchParams.get("path")), { recursive: true });
|
|
2085
2218
|
return {};
|
|
2086
2219
|
},
|
|
2087
2220
|
[Routes.gitRepos]: async () => ({ repos: await findRepos(sandbox.root) }),
|
|
@@ -2216,7 +2349,7 @@ function createAgentServer({
|
|
|
2216
2349
|
} catch (e) {
|
|
2217
2350
|
throw new HttpError(400, e.message);
|
|
2218
2351
|
}
|
|
2219
|
-
if (!(await
|
|
2352
|
+
if (!(await fs8.stat(dir)).isDirectory()) throw new HttpError(400, "cwd must be a folder");
|
|
2220
2353
|
if (model !== void 0 && (typeof model !== "string" || !/^[A-Za-z0-9._[\]-]{1,80}$/.test(model))) {
|
|
2221
2354
|
throw new HttpError(400, "invalid model");
|
|
2222
2355
|
}
|
|
@@ -2238,7 +2371,7 @@ function createAgentServer({
|
|
|
2238
2371
|
throw Object.assign(new HttpError(400, launcher.missingPlugin), { code: "ENOPLUGIN" });
|
|
2239
2372
|
}
|
|
2240
2373
|
const id = resume ?? randomUUID4();
|
|
2241
|
-
if (uploadsDir) await
|
|
2374
|
+
if (uploadsDir) await fs8.mkdir(uploadsDir, { recursive: true, mode: 448 });
|
|
2242
2375
|
const argv2 = [
|
|
2243
2376
|
launcher.command,
|
|
2244
2377
|
...launcher.args({
|
|
@@ -2475,42 +2608,33 @@ function createAgentServer({
|
|
|
2475
2608
|
}
|
|
2476
2609
|
|
|
2477
2610
|
// src/devtunnel.js
|
|
2478
|
-
import { execFile as
|
|
2611
|
+
import { execFile as execFile7 } from "node:child_process";
|
|
2479
2612
|
import { createHash } from "node:crypto";
|
|
2480
|
-
import
|
|
2481
|
-
import
|
|
2613
|
+
import fs9 from "node:fs";
|
|
2614
|
+
import path7 from "node:path";
|
|
2482
2615
|
import { promisify as promisify3 } from "node:util";
|
|
2483
|
-
var run4 = promisify3(
|
|
2616
|
+
var run4 = promisify3(execFile7);
|
|
2484
2617
|
var DOWNLOAD_BASE = "https://tunnelsassetsprod.blob.core.windows.net/cli";
|
|
2485
2618
|
var BUILDS = {
|
|
2486
2619
|
"darwin-arm64": "osx-arm64-devtunnel",
|
|
2487
2620
|
"darwin-x64": "osx-x64-devtunnel",
|
|
2488
2621
|
"linux-arm64": "linux-arm64-devtunnel",
|
|
2489
|
-
"linux-x64": "linux-x64-devtunnel"
|
|
2622
|
+
"linux-x64": "linux-x64-devtunnel",
|
|
2623
|
+
"win32-x64": "devtunnel.exe",
|
|
2624
|
+
// There's no Arm build for Windows; the x64 one runs there under emulation.
|
|
2625
|
+
"win32-arm64": "devtunnel.exe"
|
|
2490
2626
|
};
|
|
2491
2627
|
var MICROSOFT_TEAM_ID = "UBF8T346G9";
|
|
2628
|
+
var MICROSOFT_SIGNER = /^CN=Microsoft Corporation, O=Microsoft Corporation,/;
|
|
2492
2629
|
var REFRESH_INTERVAL_MS = 24 * 36e5;
|
|
2493
|
-
function executable(file) {
|
|
2494
|
-
try {
|
|
2495
|
-
fs8.accessSync(file, fs8.constants.X_OK);
|
|
2496
|
-
return fs8.statSync(file).isFile();
|
|
2497
|
-
} catch {
|
|
2498
|
-
return false;
|
|
2499
|
-
}
|
|
2500
|
-
}
|
|
2501
|
-
function onPath(name) {
|
|
2502
|
-
for (const dir of (process.env.PATH || "").split(path6.delimiter)) {
|
|
2503
|
-
if (dir && executable(path6.join(dir, name))) return path6.join(dir, name);
|
|
2504
|
-
}
|
|
2505
|
-
return null;
|
|
2506
|
-
}
|
|
2507
2630
|
async function ensureDevtunnel({ dir, progress }) {
|
|
2508
2631
|
const override = process.env.CODEREMOTE_DEVTUNNEL;
|
|
2509
2632
|
if (override) {
|
|
2510
|
-
|
|
2633
|
+
const found = findExecutable(override);
|
|
2634
|
+
if (found) return found;
|
|
2511
2635
|
throw new Error(`CODEREMOTE_DEVTUNNEL=${override} isn't an executable file.`);
|
|
2512
2636
|
}
|
|
2513
|
-
const fromPath =
|
|
2637
|
+
const fromPath = findExecutable("devtunnel");
|
|
2514
2638
|
if (fromPath) return fromPath;
|
|
2515
2639
|
const build = BUILDS[`${process.platform}-${process.arch}`];
|
|
2516
2640
|
if (!build) {
|
|
@@ -2518,14 +2642,14 @@ async function ensureDevtunnel({ dir, progress }) {
|
|
|
2518
2642
|
`No devtunnel build for ${process.platform}-${process.arch}. Install it yourself: https://learn.microsoft.com/azure/developer/dev-tunnels/get-started`
|
|
2519
2643
|
);
|
|
2520
2644
|
}
|
|
2521
|
-
const bin =
|
|
2522
|
-
const metaFile =
|
|
2645
|
+
const bin = path7.join(dir, isWindows ? "devtunnel.exe" : "devtunnel");
|
|
2646
|
+
const metaFile = path7.join(dir, "devtunnel.json");
|
|
2523
2647
|
let meta = {};
|
|
2524
2648
|
try {
|
|
2525
|
-
meta = JSON.parse(
|
|
2649
|
+
meta = JSON.parse(fs9.readFileSync(metaFile, "utf8"));
|
|
2526
2650
|
} catch {
|
|
2527
2651
|
}
|
|
2528
|
-
const installed =
|
|
2652
|
+
const installed = findExecutable(bin) !== null;
|
|
2529
2653
|
if (installed && Date.now() - (meta.checkedAt ?? 0) < REFRESH_INTERVAL_MS) return bin;
|
|
2530
2654
|
const url = `${DOWNLOAD_BASE}/${build}`;
|
|
2531
2655
|
if (installed) {
|
|
@@ -2533,23 +2657,27 @@ async function ensureDevtunnel({ dir, progress }) {
|
|
|
2533
2657
|
const head = await fetch(url, { method: "HEAD", signal: AbortSignal.timeout(3e3) });
|
|
2534
2658
|
const lastModified = head.headers.get("last-modified");
|
|
2535
2659
|
if (head.ok && lastModified && lastModified === meta.lastModified) {
|
|
2536
|
-
|
|
2660
|
+
fs9.writeFileSync(metaFile, JSON.stringify({ ...meta, checkedAt: Date.now() }));
|
|
2537
2661
|
return bin;
|
|
2538
2662
|
}
|
|
2539
2663
|
} catch {
|
|
2540
2664
|
return bin;
|
|
2541
2665
|
}
|
|
2542
2666
|
}
|
|
2543
|
-
|
|
2667
|
+
try {
|
|
2668
|
+
await download(url, bin, metaFile, progress);
|
|
2669
|
+
} catch (e) {
|
|
2670
|
+
if (!installed) throw e;
|
|
2671
|
+
}
|
|
2544
2672
|
return bin;
|
|
2545
2673
|
}
|
|
2546
2674
|
async function download(url, bin, metaFile, progress) {
|
|
2547
|
-
|
|
2675
|
+
fs9.mkdirSync(path7.dirname(bin), { recursive: true, mode: 448 });
|
|
2548
2676
|
const res = await fetch(url, { signal: AbortSignal.timeout(12e4) });
|
|
2549
2677
|
if (!res.ok || !res.body) throw new Error(`Downloading devtunnel failed: HTTP ${res.status} from ${url}`);
|
|
2550
2678
|
const total = Number(res.headers.get("content-length")) || 0;
|
|
2551
2679
|
const tmp = `${bin}.${process.pid}.download`;
|
|
2552
|
-
const out =
|
|
2680
|
+
const out = fs9.createWriteStream(tmp, { mode: 493 });
|
|
2553
2681
|
const md5 = createHash("md5");
|
|
2554
2682
|
let received = 0;
|
|
2555
2683
|
try {
|
|
@@ -2562,17 +2690,18 @@ async function download(url, bin, metaFile, progress) {
|
|
|
2562
2690
|
await new Promise((resolve, reject) => out.end((e) => e ? reject(e) : resolve()));
|
|
2563
2691
|
if (total && received !== total) throw new Error(`devtunnel download was cut short (${received} of ${total} bytes)`);
|
|
2564
2692
|
await verify(tmp, md5.digest("base64"), res.headers.get("content-md5"));
|
|
2565
|
-
|
|
2566
|
-
|
|
2693
|
+
fs9.chmodSync(tmp, 493);
|
|
2694
|
+
fs9.renameSync(tmp, bin);
|
|
2567
2695
|
} catch (e) {
|
|
2568
2696
|
out.destroy();
|
|
2569
|
-
|
|
2697
|
+
fs9.rmSync(tmp, { force: true });
|
|
2570
2698
|
throw e;
|
|
2571
2699
|
}
|
|
2572
|
-
|
|
2700
|
+
fs9.writeFileSync(metaFile, JSON.stringify({ lastModified: res.headers.get("last-modified"), checkedAt: Date.now() }));
|
|
2573
2701
|
}
|
|
2574
2702
|
async function verify(file, md5, expectedMd5) {
|
|
2575
2703
|
if (expectedMd5 && md5 !== expectedMd5) throw new Error("devtunnel download is corrupt (MD5 mismatch)");
|
|
2704
|
+
if (isWindows) return verifyAuthenticode(file);
|
|
2576
2705
|
if (process.platform !== "darwin") return;
|
|
2577
2706
|
try {
|
|
2578
2707
|
await run4("codesign", ["--verify", "--strict", file]);
|
|
@@ -2582,52 +2711,69 @@ async function verify(file, md5, expectedMd5) {
|
|
|
2582
2711
|
throw new Error(`devtunnel download failed its signature check: ${e.message}`);
|
|
2583
2712
|
}
|
|
2584
2713
|
}
|
|
2714
|
+
async function verifyAuthenticode(file) {
|
|
2715
|
+
const script = '$s = Get-AuthenticodeSignature -LiteralPath $env:CODEREMOTE_VERIFY; "$($s.Status)|$($s.SignerCertificate.Subject)"';
|
|
2716
|
+
let status2;
|
|
2717
|
+
let signer;
|
|
2718
|
+
try {
|
|
2719
|
+
const { stdout } = await run4("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
|
|
2720
|
+
env: { ...process.env, CODEREMOTE_VERIFY: file }
|
|
2721
|
+
});
|
|
2722
|
+
[status2, signer] = stdout.trim().split("|");
|
|
2723
|
+
} catch (e) {
|
|
2724
|
+
throw new Error(`devtunnel download failed its signature check: ${e.message}`);
|
|
2725
|
+
}
|
|
2726
|
+
if (status2 !== "Valid" || !MICROSOFT_SIGNER.test(signer ?? "")) {
|
|
2727
|
+
throw new Error(`devtunnel download failed its signature check: ${status2}, signed by ${signer || "nobody"}`);
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2585
2730
|
|
|
2586
2731
|
// src/hook.js
|
|
2587
2732
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
2588
|
-
import
|
|
2733
|
+
import fs10 from "node:fs";
|
|
2589
2734
|
import http3 from "node:http";
|
|
2590
|
-
import
|
|
2735
|
+
import path8 from "node:path";
|
|
2591
2736
|
var CONNECT_TIMEOUT = 1e3;
|
|
2592
2737
|
var EVENT_TIMEOUT = 5e3;
|
|
2593
2738
|
function ensureHookSecret(dir) {
|
|
2594
|
-
const file =
|
|
2739
|
+
const file = path8.join(dir, "hook-secret");
|
|
2595
2740
|
try {
|
|
2596
|
-
const secret2 =
|
|
2741
|
+
const secret2 = fs10.readFileSync(file, "utf8").trim();
|
|
2597
2742
|
if (secret2) return secret2;
|
|
2598
2743
|
} catch {
|
|
2599
2744
|
}
|
|
2600
|
-
|
|
2745
|
+
fs10.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
2601
2746
|
const secret = randomBytes2(32).toString("base64url");
|
|
2602
|
-
|
|
2747
|
+
fs10.writeFileSync(file, secret, { mode: 384 });
|
|
2603
2748
|
return secret;
|
|
2604
2749
|
}
|
|
2605
2750
|
function writeHookLauncher(dir, { node = process.execPath, script }) {
|
|
2606
|
-
const bin =
|
|
2607
|
-
|
|
2751
|
+
const bin = path8.join(dir, "bin");
|
|
2752
|
+
fs10.mkdirSync(bin, { recursive: true, mode: 448 });
|
|
2608
2753
|
const quote = (s) => `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
2609
|
-
const file =
|
|
2610
|
-
|
|
2754
|
+
const file = path8.join(bin, "hook");
|
|
2755
|
+
fs10.writeFileSync(file, `#!/bin/sh
|
|
2611
2756
|
exec ${quote(node)} ${quote(script)} hook "$@"
|
|
2612
2757
|
`, { mode: 493 });
|
|
2613
|
-
|
|
2758
|
+
fs10.chmodSync(file, 493);
|
|
2759
|
+
fs10.writeFileSync(path8.join(bin, "hook.json"), JSON.stringify({ node, script }), { mode: 384 });
|
|
2614
2760
|
return file;
|
|
2615
2761
|
}
|
|
2616
2762
|
function readHookSecret(dir) {
|
|
2617
2763
|
try {
|
|
2618
|
-
return
|
|
2764
|
+
return fs10.readFileSync(path8.join(dir, "hook-secret"), "utf8").trim() || null;
|
|
2619
2765
|
} catch {
|
|
2620
2766
|
return null;
|
|
2621
2767
|
}
|
|
2622
2768
|
}
|
|
2623
2769
|
function real(p) {
|
|
2624
2770
|
try {
|
|
2625
|
-
return
|
|
2771
|
+
return fs10.realpathSync.native(p);
|
|
2626
2772
|
} catch {
|
|
2627
|
-
return
|
|
2773
|
+
return path8.resolve(p);
|
|
2628
2774
|
}
|
|
2629
2775
|
}
|
|
2630
|
-
var within = (dir, root2) => dir === root2 || dir.startsWith(root2.endsWith(
|
|
2776
|
+
var within = (dir, root2) => dir === root2 || dir.startsWith(root2.endsWith(path8.sep) ? root2 : root2 + path8.sep);
|
|
2631
2777
|
function hookTarget(agents, cwd, env = process.env) {
|
|
2632
2778
|
const port2 = parseInt(env.CODEREMOTE_AGENT_PORT, 10);
|
|
2633
2779
|
if (port2 && agents.some((a) => a.port === port2)) return { port: port2, terminal: env.CODEREMOTE_TERM_ID || null };
|
|
@@ -2683,8 +2829,80 @@ async function runHook({ input, agents, secret, agent = "claude", env = process.
|
|
|
2683
2829
|
}
|
|
2684
2830
|
|
|
2685
2831
|
// src/preview.js
|
|
2686
|
-
import { spawn } from "node:child_process";
|
|
2832
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
2833
|
+
|
|
2834
|
+
// src/tunnels.js
|
|
2835
|
+
var TUNNEL_LIMIT = 10;
|
|
2836
|
+
var PREVIEW_LABEL = "coderemote-preview";
|
|
2837
|
+
var base = (id) => String(id).split(".")[0];
|
|
2838
|
+
async function listTunnels(devtunnel2) {
|
|
2839
|
+
const res = await devtunnel2("list", "--limit", "100", "--json");
|
|
2840
|
+
if (!res.ok) return { error: res.out.trim() };
|
|
2841
|
+
try {
|
|
2842
|
+
const { tunnels } = JSON.parse(res.out.slice(res.out.indexOf("{")));
|
|
2843
|
+
if (Array.isArray(tunnels)) return { tunnels };
|
|
2844
|
+
} catch {
|
|
2845
|
+
}
|
|
2846
|
+
return { error: `unexpected devtunnel list output:
|
|
2847
|
+
${res.out}` };
|
|
2848
|
+
}
|
|
2849
|
+
async function tunnelsAtLimit(devtunnel2) {
|
|
2850
|
+
const { tunnels } = await listTunnels(devtunnel2);
|
|
2851
|
+
return tunnels && tunnels.length >= TUNNEL_LIMIT ? tunnels : null;
|
|
2852
|
+
}
|
|
2853
|
+
async function makeRoom(devtunnel2) {
|
|
2854
|
+
const tunnels = await tunnelsAtLimit(devtunnel2);
|
|
2855
|
+
if (!tunnels) return null;
|
|
2856
|
+
const idle = tunnels.filter((t) => t.labels?.includes(PREVIEW_LABEL) && !(t.hostConnections > 0));
|
|
2857
|
+
const deleted = await Promise.all(idle.map((t) => devtunnel2("delete", t.tunnelId, "-f")));
|
|
2858
|
+
const freed = idle.filter((_, i) => deleted[i].ok).map((t) => t.tunnelId);
|
|
2859
|
+
return freed.length ? { freed } : { full: tunnels };
|
|
2860
|
+
}
|
|
2861
|
+
function ownUse(cfg2, id) {
|
|
2862
|
+
for (const [port2, t] of Object.entries(cfg2.tunnels ?? {})) if (base(t) === base(id)) return `agent on port ${port2}`;
|
|
2863
|
+
if (cfg2.tunnelId && base(cfg2.tunnelId) === base(id)) return "agent";
|
|
2864
|
+
for (const [port2, t] of Object.entries(cfg2.previewTunnels ?? {})) if (base(t) === base(id)) return `previews on port ${port2}`;
|
|
2865
|
+
return null;
|
|
2866
|
+
}
|
|
2867
|
+
function forgetTunnel(cfg2, id) {
|
|
2868
|
+
let changed = false;
|
|
2869
|
+
for (const key of ["tunnels", "previewTunnels"]) {
|
|
2870
|
+
for (const [port2, t] of Object.entries(cfg2[key] ?? {})) {
|
|
2871
|
+
if (base(t) !== base(id)) continue;
|
|
2872
|
+
delete cfg2[key][port2];
|
|
2873
|
+
changed = true;
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
if (cfg2.tunnelId && base(cfg2.tunnelId) === base(id)) {
|
|
2877
|
+
delete cfg2.tunnelId;
|
|
2878
|
+
changed = true;
|
|
2879
|
+
}
|
|
2880
|
+
return changed;
|
|
2881
|
+
}
|
|
2882
|
+
function findTunnel(tunnels, id) {
|
|
2883
|
+
return tunnels.find((t) => t.tunnelId === id || base(t.tunnelId) === base(id)) ?? null;
|
|
2884
|
+
}
|
|
2885
|
+
function formatTunnels(tunnels, cfg2) {
|
|
2886
|
+
const width = Math.max(...tunnels.map((t) => t.tunnelId.length));
|
|
2887
|
+
return tunnels.map((t) => {
|
|
2888
|
+
const notes = [t.hostConnections > 0 ? "online" : null, ownUse(cfg2, t.tunnelId) && `this computer's ${ownUse(cfg2, t.tunnelId)}`];
|
|
2889
|
+
const about = t.description || t.labels?.join(", ") || "(no description)";
|
|
2890
|
+
return ` ${t.tunnelId.padEnd(width)} ${[...notes.filter(Boolean), about].join(" \xB7 ")}`;
|
|
2891
|
+
});
|
|
2892
|
+
}
|
|
2893
|
+
function tunnelLimitHelp(tunnels, cfg2 = {}) {
|
|
2894
|
+
return [
|
|
2895
|
+
`Your Dev Tunnels account already has ${tunnels.length} tunnels, the most it may keep:`,
|
|
2896
|
+
...formatTunnels(tunnels, cfg2),
|
|
2897
|
+
"Delete the ones nothing uses any more, then try again. Deleting a tunnel an agent still uses",
|
|
2898
|
+
"changes its URL and unpairs its phones.",
|
|
2899
|
+
" coderemote tunnels delete <tunnel id>..."
|
|
2900
|
+
].join("\n");
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
// src/preview.js
|
|
2687
2904
|
var HOST_READY_TIMEOUT_MS = 3e4;
|
|
2905
|
+
var CLOSE_TIMEOUT_MS = 5e3;
|
|
2688
2906
|
var TOKEN_REUSE_MS = 12 * 36e5;
|
|
2689
2907
|
var PreviewTunnel = class {
|
|
2690
2908
|
#queue = Promise.resolve();
|
|
@@ -2764,8 +2982,13 @@ var PreviewTunnel = class {
|
|
|
2764
2982
|
return this.#serial(async () => {
|
|
2765
2983
|
const url = this.#host?.urls.get(port2);
|
|
2766
2984
|
if (url) return { port: port2, url };
|
|
2767
|
-
|
|
2768
|
-
|
|
2985
|
+
let id = await this.#ensureTunnel();
|
|
2986
|
+
let res = await this.devtunnel("port", "create", id, "-p", String(port2), "--protocol", "http");
|
|
2987
|
+
if (!res.ok && /not found/i.test(res.out)) {
|
|
2988
|
+
this.#forgetTunnel();
|
|
2989
|
+
id = await this.#ensureTunnel();
|
|
2990
|
+
res = await this.devtunnel("port", "create", id, "-p", String(port2), "--protocol", "http");
|
|
2991
|
+
}
|
|
2769
2992
|
if (!res.ok && !/already exists|conflict/i.test(res.out)) throw new Error(`devtunnel port create failed: ${res.out.trim()}`);
|
|
2770
2993
|
this.#ports.add(port2);
|
|
2771
2994
|
try {
|
|
@@ -2800,20 +3023,42 @@ var PreviewTunnel = class {
|
|
|
2800
3023
|
stop() {
|
|
2801
3024
|
this.#stopHost();
|
|
2802
3025
|
}
|
|
3026
|
+
/**
|
|
3027
|
+
* Stops hosting and deletes the tunnel (or the last run's), which nothing reuses: each run
|
|
3028
|
+
* starts a new one. If that fails or takes too long, the next run deletes it.
|
|
3029
|
+
*/
|
|
3030
|
+
async close() {
|
|
3031
|
+
this.#stopHost();
|
|
3032
|
+
const id = this.#tunnelId ?? this.leftover;
|
|
3033
|
+
if (!id) return;
|
|
3034
|
+
const res = await Promise.race([
|
|
3035
|
+
this.devtunnel("delete", id, "-f"),
|
|
3036
|
+
new Promise((resolve) => setTimeout(resolve, CLOSE_TIMEOUT_MS, { ok: false, out: "timed out" }))
|
|
3037
|
+
]);
|
|
3038
|
+
if (res.ok || /not found/i.test(res.out)) this.remember(null);
|
|
3039
|
+
}
|
|
3040
|
+
#forgetTunnel() {
|
|
3041
|
+
this.#stopHost();
|
|
3042
|
+
this.#tunnelId = null;
|
|
3043
|
+
this.#token = null;
|
|
3044
|
+
this.#ports.clear();
|
|
3045
|
+
this.#shares.clear();
|
|
3046
|
+
}
|
|
2803
3047
|
async #ensureTunnel() {
|
|
2804
3048
|
if (this.#tunnelId) return this.#tunnelId;
|
|
2805
3049
|
if (this.leftover) await this.devtunnel("delete", this.leftover, "-f");
|
|
2806
|
-
const
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
3050
|
+
const create = () => this.devtunnel("create", "--expiration", "30d", "-d", this.description, "--labels", PREVIEW_LABEL, "--json");
|
|
3051
|
+
let created = await create();
|
|
3052
|
+
if (!created.ok) {
|
|
3053
|
+
const room = await makeRoom(this.devtunnel);
|
|
3054
|
+
if (room?.freed) created = await create();
|
|
3055
|
+
if (room?.full) {
|
|
3056
|
+
throw new Error(
|
|
3057
|
+
`Your Dev Tunnels account already has ${TUNNEL_LIMIT} tunnels, the most it may keep. Delete ones nothing uses on the computer (coderemote tunnels, coderemote tunnels delete <id>), then try again.`
|
|
3058
|
+
);
|
|
3059
|
+
}
|
|
3060
|
+
if (!created.ok) throw new Error(`devtunnel create failed: ${created.out.trim()}`);
|
|
3061
|
+
}
|
|
2817
3062
|
const id = JSON.parse(created.out.slice(created.out.indexOf("{"))).tunnel?.tunnelId;
|
|
2818
3063
|
if (!id) throw new Error(`could not parse tunnel id from:
|
|
2819
3064
|
${created.out}`);
|
|
@@ -2831,7 +3076,7 @@ ${created.out}`);
|
|
|
2831
3076
|
}
|
|
2832
3077
|
#startHost() {
|
|
2833
3078
|
return new Promise((resolve, reject) => {
|
|
2834
|
-
const child =
|
|
3079
|
+
const child = spawn2(this.command(), ["host", this.#tunnelId], { stdio: ["ignore", "pipe", "pipe"] });
|
|
2835
3080
|
const urls = /* @__PURE__ */ new Map();
|
|
2836
3081
|
let log2 = "";
|
|
2837
3082
|
const timer = setTimeout(() => {
|
|
@@ -2866,40 +3111,40 @@ ${log2}`));
|
|
|
2866
3111
|
};
|
|
2867
3112
|
|
|
2868
3113
|
// src/push-store.js
|
|
2869
|
-
import
|
|
2870
|
-
import
|
|
3114
|
+
import fs11 from "node:fs";
|
|
3115
|
+
import path9 from "node:path";
|
|
2871
3116
|
function pushStore(dir, port2) {
|
|
2872
|
-
const file =
|
|
3117
|
+
const file = path9.join(dir, `${port2}.json`);
|
|
2873
3118
|
return {
|
|
2874
3119
|
load() {
|
|
2875
3120
|
try {
|
|
2876
|
-
const devices = JSON.parse(
|
|
3121
|
+
const devices = JSON.parse(fs11.readFileSync(file, "utf8"));
|
|
2877
3122
|
return Array.isArray(devices) ? devices : [];
|
|
2878
3123
|
} catch {
|
|
2879
3124
|
return [];
|
|
2880
3125
|
}
|
|
2881
3126
|
},
|
|
2882
3127
|
save(devices) {
|
|
2883
|
-
|
|
2884
|
-
|
|
3128
|
+
fs11.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
3129
|
+
fs11.writeFileSync(file, JSON.stringify(devices, null, 2), { mode: 384 });
|
|
2885
3130
|
}
|
|
2886
3131
|
};
|
|
2887
3132
|
}
|
|
2888
3133
|
|
|
2889
3134
|
// src/registry.js
|
|
2890
|
-
import
|
|
2891
|
-
import
|
|
3135
|
+
import fs12 from "node:fs";
|
|
3136
|
+
import path10 from "node:path";
|
|
2892
3137
|
var AgentRegistry = class {
|
|
2893
3138
|
constructor(dir) {
|
|
2894
3139
|
this.dir = dir;
|
|
2895
3140
|
}
|
|
2896
3141
|
register({ port: port2, root: root2, url }) {
|
|
2897
|
-
|
|
3142
|
+
fs12.mkdirSync(this.dir, { recursive: true, mode: 448 });
|
|
2898
3143
|
const file = this.#file(port2);
|
|
2899
|
-
|
|
3144
|
+
fs12.writeFileSync(file, JSON.stringify({ pid: process.pid, port: port2, root: root2, url, startedAt: Date.now() }), { mode: 384 });
|
|
2900
3145
|
const cleanup = () => {
|
|
2901
3146
|
try {
|
|
2902
|
-
if (JSON.parse(
|
|
3147
|
+
if (JSON.parse(fs12.readFileSync(file, "utf8")).pid === process.pid) fs12.rmSync(file);
|
|
2903
3148
|
} catch {
|
|
2904
3149
|
}
|
|
2905
3150
|
};
|
|
@@ -2910,25 +3155,25 @@ var AgentRegistry = class {
|
|
|
2910
3155
|
list() {
|
|
2911
3156
|
let names = [];
|
|
2912
3157
|
try {
|
|
2913
|
-
names =
|
|
3158
|
+
names = fs12.readdirSync(this.dir).filter((n) => n.endsWith(".json"));
|
|
2914
3159
|
} catch {
|
|
2915
3160
|
return [];
|
|
2916
3161
|
}
|
|
2917
3162
|
const agents = [];
|
|
2918
3163
|
for (const name of names) {
|
|
2919
|
-
const file =
|
|
3164
|
+
const file = path10.join(this.dir, name);
|
|
2920
3165
|
try {
|
|
2921
|
-
const agent = JSON.parse(
|
|
3166
|
+
const agent = JSON.parse(fs12.readFileSync(file, "utf8"));
|
|
2922
3167
|
process.kill(agent.pid, 0);
|
|
2923
3168
|
agents.push(agent);
|
|
2924
3169
|
} catch {
|
|
2925
|
-
|
|
3170
|
+
fs12.rmSync(file, { force: true });
|
|
2926
3171
|
}
|
|
2927
3172
|
}
|
|
2928
3173
|
return agents.sort((a, b) => a.port - b.port);
|
|
2929
3174
|
}
|
|
2930
3175
|
#file(port2) {
|
|
2931
|
-
return
|
|
3176
|
+
return path10.join(this.dir, `${port2}.json`);
|
|
2932
3177
|
}
|
|
2933
3178
|
};
|
|
2934
3179
|
|
|
@@ -2992,8 +3237,8 @@ function tunnelStatus({ devtunnel: devtunnel2, tunnelId, account, anonymous: ano
|
|
|
2992
3237
|
}
|
|
2993
3238
|
|
|
2994
3239
|
// src/update.js
|
|
2995
|
-
import
|
|
2996
|
-
import
|
|
3240
|
+
import fs13 from "node:fs";
|
|
3241
|
+
import path11 from "node:path";
|
|
2997
3242
|
var LATEST_URL = "https://registry.npmjs.org/coderemote/latest";
|
|
2998
3243
|
var CHECK_INTERVAL_MS = 24 * 36e5;
|
|
2999
3244
|
var SEMVER = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
|
|
@@ -3017,10 +3262,10 @@ async function fetchLatest({ fetch: fetch2 = globalThis.fetch, timeoutMs = 3e3 }
|
|
|
3017
3262
|
}
|
|
3018
3263
|
}
|
|
3019
3264
|
async function availableUpdate({ current, dir, fetch: fetch2, now = Date.now() }) {
|
|
3020
|
-
const file =
|
|
3265
|
+
const file = path11.join(dir, "update.json");
|
|
3021
3266
|
let cache = {};
|
|
3022
3267
|
try {
|
|
3023
|
-
cache = JSON.parse(
|
|
3268
|
+
cache = JSON.parse(fs13.readFileSync(file, "utf8"));
|
|
3024
3269
|
} catch {
|
|
3025
3270
|
}
|
|
3026
3271
|
let latest = cache.latest;
|
|
@@ -3029,8 +3274,8 @@ async function availableUpdate({ current, dir, fetch: fetch2, now = Date.now() }
|
|
|
3029
3274
|
if (fetched) {
|
|
3030
3275
|
latest = fetched;
|
|
3031
3276
|
try {
|
|
3032
|
-
|
|
3033
|
-
|
|
3277
|
+
fs13.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
3278
|
+
fs13.writeFileSync(file, JSON.stringify({ latest, checkedAt: now }));
|
|
3034
3279
|
} catch {
|
|
3035
3280
|
}
|
|
3036
3281
|
}
|
|
@@ -3038,7 +3283,14 @@ async function availableUpdate({ current, dir, fetch: fetch2, now = Date.now() }
|
|
|
3038
3283
|
return isNewer(latest, current) ? latest : null;
|
|
3039
3284
|
}
|
|
3040
3285
|
function installCommand(installedAt) {
|
|
3041
|
-
const p = installedAt.split(
|
|
3286
|
+
const p = installedAt.split(path11.sep).join("/");
|
|
3287
|
+
const runtime = p.match(/^(.*\/runtime\/node)\/(lib\/)?node_modules\/coderemote\//);
|
|
3288
|
+
if (runtime) {
|
|
3289
|
+
const prefix = installedAt.slice(0, runtime[1].length);
|
|
3290
|
+
const [node, modules] = runtime[2] ? [path11.join(prefix, "bin", "node"), path11.join(prefix, "lib", "node_modules")] : [path11.join(prefix, "node.exe"), path11.join(prefix, "node_modules")];
|
|
3291
|
+
return [node, [path11.join(modules, "npm", "bin", "npm-cli.js"), "install", "-g", "--prefix", prefix, "coderemote@latest"]];
|
|
3292
|
+
}
|
|
3293
|
+
if (p.includes("/Cellar/coderemote/")) return ["brew", ["upgrade", "coderemote"]];
|
|
3042
3294
|
if (p.includes("/pnpm/") || p.includes("/.pnpm/")) return ["pnpm", ["add", "-g", "coderemote@latest"]];
|
|
3043
3295
|
if (p.includes("/.bun/")) return ["bun", ["add", "-g", "coderemote@latest"]];
|
|
3044
3296
|
if (p.includes("/.yarn/") || p.includes("/yarn/global/")) return ["yarn", ["global", "add", "coderemote@latest"]];
|
|
@@ -3048,7 +3300,7 @@ function installCommand(installedAt) {
|
|
|
3048
3300
|
// package.json
|
|
3049
3301
|
var package_default = {
|
|
3050
3302
|
name: "coderemote",
|
|
3051
|
-
version: "0.3.
|
|
3303
|
+
version: "0.3.6",
|
|
3052
3304
|
description: "Use your computer from your iPhone: files, git and a real terminal over a secure Microsoft Dev Tunnel. No SSH, no port forwarding, no VPN.",
|
|
3053
3305
|
keywords: [
|
|
3054
3306
|
"remote",
|
|
@@ -3091,17 +3343,13 @@ var package_default = {
|
|
|
3091
3343
|
engines: {
|
|
3092
3344
|
node: ">=20"
|
|
3093
3345
|
},
|
|
3094
|
-
os: [
|
|
3095
|
-
"darwin",
|
|
3096
|
-
"linux"
|
|
3097
|
-
],
|
|
3098
3346
|
scripts: {
|
|
3099
3347
|
build: "node scripts/build.js",
|
|
3100
3348
|
prepare: "node scripts/build.js",
|
|
3101
|
-
prepack:
|
|
3102
|
-
postpack:
|
|
3349
|
+
prepack: `node scripts/build.js && node -e "require('fs').copyFileSync('../../LICENSE', 'LICENSE')"`,
|
|
3350
|
+
postpack: `node -e "require('fs').rmSync('LICENSE', { force: true })"`,
|
|
3103
3351
|
postinstall: "node scripts/fix-node-pty.js",
|
|
3104
|
-
test:
|
|
3352
|
+
test: 'node --test "test/*.test.js"'
|
|
3105
3353
|
},
|
|
3106
3354
|
dependencies: {
|
|
3107
3355
|
"node-pty": "^1.1.0",
|
|
@@ -3327,15 +3575,12 @@ var Spinner = class {
|
|
|
3327
3575
|
};
|
|
3328
3576
|
|
|
3329
3577
|
// src/vscode-tunnel.js
|
|
3330
|
-
import { execFile as execFile7, spawn as spawn2 } from "node:child_process";
|
|
3331
|
-
import { promisify as promisify4 } from "node:util";
|
|
3332
|
-
var run5 = promisify4(execFile7);
|
|
3333
3578
|
function findCode() {
|
|
3334
|
-
return process.env.CODEREMOTE_CODE ||
|
|
3579
|
+
return process.env.CODEREMOTE_CODE || findExecutable("code");
|
|
3335
3580
|
}
|
|
3336
3581
|
async function codeTunnelSignedIn(code) {
|
|
3337
3582
|
try {
|
|
3338
|
-
const { stdout } = await
|
|
3583
|
+
const { stdout } = await execCommand([code, "tunnel", "user", "show"]);
|
|
3339
3584
|
return /^logged in/im.test(stdout);
|
|
3340
3585
|
} catch {
|
|
3341
3586
|
return false;
|
|
@@ -3343,12 +3588,12 @@ async function codeTunnelSignedIn(code) {
|
|
|
3343
3588
|
}
|
|
3344
3589
|
function codeTunnelLogin(code) {
|
|
3345
3590
|
return new Promise((resolve) => {
|
|
3346
|
-
|
|
3591
|
+
spawnCommand([code, "tunnel", "user", "login", "--provider", "github"], { stdio: "inherit" }).on("exit", resolve);
|
|
3347
3592
|
});
|
|
3348
3593
|
}
|
|
3349
3594
|
function hostCodeTunnel(code, root2, timeoutMs = 6e4) {
|
|
3350
3595
|
return new Promise((resolve, reject) => {
|
|
3351
|
-
const child =
|
|
3596
|
+
const child = spawnCommand([code, "tunnel"], { cwd: root2, stdio: ["ignore", "pipe", "pipe"] });
|
|
3352
3597
|
let log2 = "";
|
|
3353
3598
|
const timer = setTimeout(() => {
|
|
3354
3599
|
child.kill();
|
|
@@ -3360,7 +3605,8 @@ ${log2}`));
|
|
|
3360
3605
|
const m = log2.match(/https:\/\/vscode\.dev\/tunnel\/([^/\s]+)/);
|
|
3361
3606
|
if (!m) return;
|
|
3362
3607
|
clearTimeout(timer);
|
|
3363
|
-
|
|
3608
|
+
const folder = isWindows ? `/${root2.replaceAll("\\", "/")}` : root2;
|
|
3609
|
+
resolve({ child, url: `https://vscode.dev/tunnel/${m[1]}${encodeURI(folder)}` });
|
|
3364
3610
|
};
|
|
3365
3611
|
child.stdout.on("data", onData);
|
|
3366
3612
|
child.stderr.on("data", onData);
|
|
@@ -3377,28 +3623,28 @@ ${log2}`));
|
|
|
3377
3623
|
}
|
|
3378
3624
|
|
|
3379
3625
|
// bin/coderemote.js
|
|
3380
|
-
var
|
|
3381
|
-
var CONFIG_DIR =
|
|
3382
|
-
var LEGACY_CONFIG_DIR =
|
|
3383
|
-
if (!
|
|
3384
|
-
var CONFIG_FILE =
|
|
3385
|
-
var PUSH_DIR =
|
|
3386
|
-
var UPLOADS_DIR =
|
|
3626
|
+
var run5 = promisify4(execFile8);
|
|
3627
|
+
var CONFIG_DIR = path12.join(os6.homedir(), ".coderemote");
|
|
3628
|
+
var LEGACY_CONFIG_DIR = path12.join(os6.homedir(), ".vsremote");
|
|
3629
|
+
if (!fs14.existsSync(CONFIG_DIR) && fs14.existsSync(LEGACY_CONFIG_DIR)) fs14.renameSync(LEGACY_CONFIG_DIR, CONFIG_DIR);
|
|
3630
|
+
var CONFIG_FILE = path12.join(CONFIG_DIR, "config.json");
|
|
3631
|
+
var PUSH_DIR = path12.join(CONFIG_DIR, "push");
|
|
3632
|
+
var UPLOADS_DIR = path12.join(CONFIG_DIR, "uploads");
|
|
3387
3633
|
var DEFAULT_PORT = 31580;
|
|
3388
3634
|
var VERSION = package_default.version;
|
|
3389
|
-
var PACKAGED =
|
|
3635
|
+
var PACKAGED = path12.basename(path12.dirname(fileURLToPath(import.meta.url))) === "dist";
|
|
3390
3636
|
var spinner = new Spinner();
|
|
3391
3637
|
function loadConfig() {
|
|
3392
3638
|
let cfg2 = {};
|
|
3393
3639
|
try {
|
|
3394
|
-
cfg2 = JSON.parse(
|
|
3640
|
+
cfg2 = JSON.parse(fs14.readFileSync(CONFIG_FILE, "utf8"));
|
|
3395
3641
|
} catch {
|
|
3396
3642
|
}
|
|
3397
3643
|
if (!cfg2.token) {
|
|
3398
3644
|
cfg2.token = randomBytes3(32).toString("base64url");
|
|
3399
3645
|
saveConfig(cfg2);
|
|
3400
|
-
|
|
3401
|
-
|
|
3646
|
+
fs14.rmSync(PUSH_DIR, { recursive: true, force: true });
|
|
3647
|
+
fs14.rmSync(UPLOADS_DIR, { recursive: true, force: true });
|
|
3402
3648
|
}
|
|
3403
3649
|
if (cfg2.tunnelId && !cfg2.tunnels) {
|
|
3404
3650
|
cfg2.tunnels = { [DEFAULT_PORT]: cfg2.tunnelId };
|
|
@@ -3406,9 +3652,16 @@ function loadConfig() {
|
|
|
3406
3652
|
}
|
|
3407
3653
|
return cfg2;
|
|
3408
3654
|
}
|
|
3655
|
+
function stopAgent(pid) {
|
|
3656
|
+
if (!isWindows) return process.kill(pid, "SIGTERM");
|
|
3657
|
+
try {
|
|
3658
|
+
execFileSync("taskkill", ["/T", "/F", "/PID", String(pid)], { stdio: "ignore" });
|
|
3659
|
+
} catch {
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3409
3662
|
function saveConfig(cfg2) {
|
|
3410
|
-
|
|
3411
|
-
|
|
3663
|
+
fs14.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
3664
|
+
fs14.writeFileSync(CONFIG_FILE, JSON.stringify(cfg2, null, 2), { mode: 384 });
|
|
3412
3665
|
}
|
|
3413
3666
|
function portFree(port2) {
|
|
3414
3667
|
return new Promise((resolve) => {
|
|
@@ -3419,7 +3672,7 @@ function portFree(port2) {
|
|
|
3419
3672
|
}
|
|
3420
3673
|
function listen(token, port2, root2, tunnelToken, preview, status2) {
|
|
3421
3674
|
const hookSecret = ensureHookSecret(CONFIG_DIR);
|
|
3422
|
-
writeHookLauncher(CONFIG_DIR, { script:
|
|
3675
|
+
writeHookLauncher(CONFIG_DIR, { script: fs14.realpathSync(process.argv[1]) });
|
|
3423
3676
|
const server = createAgentServer({
|
|
3424
3677
|
token,
|
|
3425
3678
|
root: root2,
|
|
@@ -3428,7 +3681,7 @@ function listen(token, port2, root2, tunnelToken, preview, status2) {
|
|
|
3428
3681
|
tunnelStatus: status2,
|
|
3429
3682
|
hookSecret,
|
|
3430
3683
|
// This very CLI, so coding agents in the app's terminals report through the same version.
|
|
3431
|
-
hookBin:
|
|
3684
|
+
hookBin: fs14.realpathSync(process.argv[1]),
|
|
3432
3685
|
pushStore: pushStore(PUSH_DIR, port2),
|
|
3433
3686
|
uploadsDir: UPLOADS_DIR
|
|
3434
3687
|
});
|
|
@@ -3441,7 +3694,7 @@ var DEVTUNNEL = "devtunnel";
|
|
|
3441
3694
|
async function resolveDevtunnel() {
|
|
3442
3695
|
let installing = false;
|
|
3443
3696
|
DEVTUNNEL = await ensureDevtunnel({
|
|
3444
|
-
dir:
|
|
3697
|
+
dir: path12.join(CONFIG_DIR, "bin"),
|
|
3445
3698
|
progress: (detail2) => {
|
|
3446
3699
|
if (!installing) {
|
|
3447
3700
|
installing = true;
|
|
@@ -3454,7 +3707,7 @@ async function resolveDevtunnel() {
|
|
|
3454
3707
|
}
|
|
3455
3708
|
async function devtunnel(...args) {
|
|
3456
3709
|
try {
|
|
3457
|
-
const { stdout } = await
|
|
3710
|
+
const { stdout } = await run5(DEVTUNNEL, args);
|
|
3458
3711
|
return { ok: true, out: stdout };
|
|
3459
3712
|
} catch (e) {
|
|
3460
3713
|
return { ok: false, out: `${e.stdout || ""}${e.stderr || ""}` || e.message };
|
|
@@ -3515,9 +3768,23 @@ ${updated.out}`);
|
|
|
3515
3768
|
const name = port2 === DEFAULT_PORT ? os6.hostname() : `${os6.hostname()} :${port2}`;
|
|
3516
3769
|
const createArgs = ["create", "--expiration", "30d", "-d", `coderemote ${name}`, "--labels", MACHINE_LABEL, "--json"];
|
|
3517
3770
|
if (anonymous2) createArgs.push("--allow-anonymous");
|
|
3518
|
-
|
|
3519
|
-
if (!created.ok)
|
|
3771
|
+
let created = await devtunnel(...createArgs);
|
|
3772
|
+
if (!created.ok) {
|
|
3773
|
+
const room = await makeRoom(devtunnel);
|
|
3774
|
+
if (room?.full) {
|
|
3775
|
+
spinner.fail();
|
|
3776
|
+
console.error(`
|
|
3777
|
+
${tunnelLimitHelp(room.full, cfg2)}`);
|
|
3778
|
+
process.exit(1);
|
|
3779
|
+
}
|
|
3780
|
+
if (room?.freed) {
|
|
3781
|
+
spinner.update(`(deleted ${room.freed.length} idle preview tunnel${room.freed.length > 1 ? "s" : ""} to make room)`);
|
|
3782
|
+
if (room.freed.filter((id) => forgetTunnel(cfg2, id)).length) saveConfig(cfg2);
|
|
3783
|
+
created = await devtunnel(...createArgs);
|
|
3784
|
+
}
|
|
3785
|
+
if (!created.ok) throw new Error(`devtunnel create failed:
|
|
3520
3786
|
${created.out}`);
|
|
3787
|
+
}
|
|
3521
3788
|
const tunnelId = JSON.parse(created.out.slice(created.out.indexOf("{"))).tunnel?.tunnelId;
|
|
3522
3789
|
if (!tunnelId) throw new Error(`could not parse tunnel id from:
|
|
3523
3790
|
${created.out}`);
|
|
@@ -3610,6 +3877,9 @@ var USAGE = `Usage: coderemote <command> [options]
|
|
|
3610
3877
|
kill <id>... | --all kill terminal sessions (an id prefix is enough)
|
|
3611
3878
|
stop [--all] stop the agent for this folder (or every agent)
|
|
3612
3879
|
rotate-token revoke every paired device and stop running agents
|
|
3880
|
+
tunnels list your dev tunnels account's tunnels (it may keep 10)
|
|
3881
|
+
tunnels delete <id>... delete tunnels nothing uses; one that is online or is this
|
|
3882
|
+
computer's needs --force (its URL changes, its phones unpair)
|
|
3613
3883
|
update install the latest coderemote (up says when there's one)
|
|
3614
3884
|
requests [PORT] list the requests that reached a previewed port, oldest first;
|
|
3615
3885
|
--grep TEXT, --failed (errors, 4xx and 5xx), --limit N (default 50)
|
|
@@ -3641,9 +3911,9 @@ var flag = (name) => {
|
|
|
3641
3911
|
};
|
|
3642
3912
|
var anonymous = rest.includes("--allow-anonymous") || rest.includes("--no-auth");
|
|
3643
3913
|
var port = flag("--port") ? parseInt(flag("--port"), 10) : DEFAULT_PORT;
|
|
3644
|
-
var root =
|
|
3914
|
+
var root = path12.resolve(flag("--root") ?? process.cwd());
|
|
3645
3915
|
var cfg = loadConfig();
|
|
3646
|
-
var registry = new AgentRegistry(
|
|
3916
|
+
var registry = new AgentRegistry(path12.join(CONFIG_DIR, "agents"));
|
|
3647
3917
|
async function start({ tunnel }) {
|
|
3648
3918
|
const agents = registry.list();
|
|
3649
3919
|
if (flag("--port")) {
|
|
@@ -3682,6 +3952,7 @@ async function start({ tunnel }) {
|
|
|
3682
3952
|
leftover: cfg.previewTunnels?.[port],
|
|
3683
3953
|
remember: (id) => {
|
|
3684
3954
|
cfg.previewTunnels = { ...cfg.previewTunnels, [port]: id };
|
|
3955
|
+
if (!id) delete cfg.previewTunnels[port];
|
|
3685
3956
|
saveConfig(cfg);
|
|
3686
3957
|
},
|
|
3687
3958
|
mintToken: connectToken
|
|
@@ -3695,7 +3966,7 @@ async function start({ tunnel }) {
|
|
|
3695
3966
|
spinner.fail();
|
|
3696
3967
|
if (e.code !== "EADDRINUSE") throw e;
|
|
3697
3968
|
console.error(`Port ${port} is already in use by another process.`);
|
|
3698
|
-
console.error(`See what holds it with: lsof -nP -iTCP:${port} -sTCP:LISTEN`);
|
|
3969
|
+
console.error(`See what holds it with: ${isWindows ? `netstat -ano | findstr :${port}` : `lsof -nP -iTCP:${port} -sTCP:LISTEN`}`);
|
|
3699
3970
|
console.error("Stop that process, or pick another port with --port.");
|
|
3700
3971
|
process.exit(1);
|
|
3701
3972
|
}
|
|
@@ -3725,10 +3996,10 @@ async function start({ tunnel }) {
|
|
|
3725
3996
|
}
|
|
3726
3997
|
spinner.succeed();
|
|
3727
3998
|
registry.register({ port, root, url });
|
|
3728
|
-
const shutdown = () => {
|
|
3999
|
+
const shutdown = async () => {
|
|
3729
4000
|
child?.kill();
|
|
3730
|
-
preview?.
|
|
3731
|
-
server.killAllSessions();
|
|
4001
|
+
await preview?.close();
|
|
4002
|
+
await server.killAllSessions();
|
|
3732
4003
|
server.close();
|
|
3733
4004
|
process.exit(0);
|
|
3734
4005
|
};
|
|
@@ -3744,7 +4015,7 @@ async function start({ tunnel }) {
|
|
|
3744
4015
|
var PLUGIN = "coderemote@coderemote";
|
|
3745
4016
|
async function claudeCli(...args) {
|
|
3746
4017
|
try {
|
|
3747
|
-
return (await
|
|
4018
|
+
return (await execCommand(["claude", ...args], { timeout: 12e4 })).stdout.trim();
|
|
3748
4019
|
} catch (e) {
|
|
3749
4020
|
if (e.code === "ENOENT") return null;
|
|
3750
4021
|
throw new Error((e.stderr || e.stdout || e.message).trim());
|
|
@@ -3785,7 +4056,7 @@ async function setupClaude() {
|
|
|
3785
4056
|
var CODEX_PLUGIN = "coderemote@coderemote";
|
|
3786
4057
|
async function codexCli(...args) {
|
|
3787
4058
|
try {
|
|
3788
|
-
return (await
|
|
4059
|
+
return (await execCommand(["codex", ...args], { timeout: 12e4 })).stdout.trim();
|
|
3789
4060
|
} catch (e) {
|
|
3790
4061
|
if (e.code === "ENOENT") return null;
|
|
3791
4062
|
throw new Error((e.stderr || e.stdout || e.message).trim());
|
|
@@ -3860,7 +4131,7 @@ function currentAgent() {
|
|
|
3860
4131
|
const agents = registry.list();
|
|
3861
4132
|
if (flag("--port")) return agents.find((a) => a.port === port);
|
|
3862
4133
|
const here = process.cwd();
|
|
3863
|
-
const containing = agents.filter((a) => here === a.root || here.startsWith(a.root +
|
|
4134
|
+
const containing = agents.filter((a) => here === a.root || here.startsWith(a.root + path12.sep)).sort((a, b) => b.root.length - a.root.length);
|
|
3864
4135
|
return containing[0] ?? (agents.length === 1 ? agents[0] : void 0);
|
|
3865
4136
|
}
|
|
3866
4137
|
async function previewPorts(agent, given) {
|
|
@@ -3990,7 +4261,7 @@ Token: ${cfg.token}`);
|
|
|
3990
4261
|
break;
|
|
3991
4262
|
}
|
|
3992
4263
|
for (const agent of targets) {
|
|
3993
|
-
|
|
4264
|
+
stopAgent(agent.pid);
|
|
3994
4265
|
console.log(`stopped agent for ${agent.root} (pid ${agent.pid})`);
|
|
3995
4266
|
}
|
|
3996
4267
|
break;
|
|
@@ -4000,13 +4271,57 @@ Token: ${cfg.token}`);
|
|
|
4000
4271
|
saveConfig(cfg);
|
|
4001
4272
|
const agents = registry.list();
|
|
4002
4273
|
for (const agent of agents) {
|
|
4003
|
-
|
|
4274
|
+
stopAgent(agent.pid);
|
|
4004
4275
|
console.log(`stopped agent for ${agent.root} (pid ${agent.pid})`);
|
|
4005
4276
|
}
|
|
4006
4277
|
console.log("Token rotated. Re-pair your devices.");
|
|
4007
4278
|
if (agents.length) console.log("Run `coderemote up` again in each folder above; each keeps its tunnel URL.");
|
|
4008
4279
|
break;
|
|
4009
4280
|
}
|
|
4281
|
+
case "tunnels": {
|
|
4282
|
+
await resolveDevtunnel();
|
|
4283
|
+
const { tunnels, error } = await listTunnels(devtunnel);
|
|
4284
|
+
if (error) {
|
|
4285
|
+
console.error(error);
|
|
4286
|
+
console.error("Signed in? Run `coderemote login`.");
|
|
4287
|
+
process.exit(1);
|
|
4288
|
+
}
|
|
4289
|
+
if (rest[0] !== "delete") {
|
|
4290
|
+
console.log(tunnels.length ? formatTunnels(tunnels, cfg).join("\n") : "No tunnels on this account.");
|
|
4291
|
+
break;
|
|
4292
|
+
}
|
|
4293
|
+
const ids = rest.slice(1).filter((a) => !a.startsWith("--"));
|
|
4294
|
+
if (!ids.length) {
|
|
4295
|
+
console.error("Usage: coderemote tunnels delete <tunnel id>... [--force]");
|
|
4296
|
+
process.exit(1);
|
|
4297
|
+
}
|
|
4298
|
+
let failed = false;
|
|
4299
|
+
for (const id of ids) {
|
|
4300
|
+
const tunnel = findTunnel(tunnels, id);
|
|
4301
|
+
if (!tunnel) {
|
|
4302
|
+
console.error(`${id}: no such tunnel on this account`);
|
|
4303
|
+
failed = true;
|
|
4304
|
+
continue;
|
|
4305
|
+
}
|
|
4306
|
+
const use = ownUse(cfg, tunnel.tunnelId);
|
|
4307
|
+
if (!rest.includes("--force") && (tunnel.hostConnections > 0 || use)) {
|
|
4308
|
+
const why = tunnel.hostConnections > 0 ? "an agent is serving it right now" : `it is this computer's ${use}`;
|
|
4309
|
+
console.error(`${tunnel.tunnelId}: kept, ${why}. Deleting it unpairs its phones; add --force to delete it anyway.`);
|
|
4310
|
+
failed = true;
|
|
4311
|
+
continue;
|
|
4312
|
+
}
|
|
4313
|
+
const res = await devtunnel("delete", tunnel.tunnelId, "-f");
|
|
4314
|
+
if (!res.ok) {
|
|
4315
|
+
console.error(`${tunnel.tunnelId}: ${res.out.trim()}`);
|
|
4316
|
+
failed = true;
|
|
4317
|
+
continue;
|
|
4318
|
+
}
|
|
4319
|
+
if (forgetTunnel(cfg, tunnel.tunnelId)) saveConfig(cfg);
|
|
4320
|
+
console.log(`deleted ${tunnel.tunnelId}${use ? ` (this computer's ${use}: the next \`coderemote up\` makes a new one)` : ""}`);
|
|
4321
|
+
}
|
|
4322
|
+
if (failed) process.exit(1);
|
|
4323
|
+
break;
|
|
4324
|
+
}
|
|
4010
4325
|
case "login": {
|
|
4011
4326
|
await resolveDevtunnel();
|
|
4012
4327
|
process.exit(await interactiveLogin(loginArgs()));
|
|
@@ -4025,16 +4340,16 @@ Token: ${cfg.token}`);
|
|
|
4025
4340
|
console.log(`coderemote ${VERSION} is the latest version.`);
|
|
4026
4341
|
break;
|
|
4027
4342
|
}
|
|
4028
|
-
const [command, args] = installCommand(
|
|
4343
|
+
const [command, args] = installCommand(fs14.realpathSync(process.argv[1]));
|
|
4029
4344
|
console.log(`Updating coderemote ${VERSION} \u2192 ${latest}: ${command} ${args.join(" ")}
|
|
4030
4345
|
`);
|
|
4031
4346
|
const code = await new Promise((resolve) => {
|
|
4032
|
-
|
|
4347
|
+
spawnCommand([command, ...args], { stdio: "inherit" }).on("exit", resolve).on("error", () => resolve(127));
|
|
4033
4348
|
});
|
|
4034
4349
|
if (code !== 0) {
|
|
4035
4350
|
console.error(`
|
|
4036
4351
|
The update failed (${command} exited with ${code}).`);
|
|
4037
|
-
console.error(`If it was a permission error (EACCES), run it with sudo: sudo ${command} ${args.join(" ")}`);
|
|
4352
|
+
if (!isWindows && command === "npm") console.error(`If it was a permission error (EACCES), run it with sudo: sudo ${command} ${args.join(" ")}`);
|
|
4038
4353
|
process.exit(code || 1);
|
|
4039
4354
|
}
|
|
4040
4355
|
const running = registry.list();
|