coderemote 0.3.4 → 0.3.7
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 +575 -228
- package/package.json +5 -9
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;
|
|
@@ -66,6 +65,8 @@ var Routes = Object.freeze({
|
|
|
66
65
|
agentsSetup: "GET /api/agents/setup",
|
|
67
66
|
agentsPrompt: "POST /api/agents/prompt",
|
|
68
67
|
agentsKeys: "POST /api/agents/keys",
|
|
68
|
+
agentsStop: "POST /api/agents/stop",
|
|
69
|
+
agentsForget: "POST /api/agents/forget",
|
|
69
70
|
agentsUpload: "POST /api/agents/uploads",
|
|
70
71
|
pushDeviceSet: "PUT /api/push/device",
|
|
71
72
|
pushDeviceDelete: "DELETE /api/push/device",
|
|
@@ -116,7 +117,7 @@ var HttpError = class extends Error {
|
|
|
116
117
|
var Sandbox = class {
|
|
117
118
|
constructor(root2) {
|
|
118
119
|
this.root = path.resolve(root2);
|
|
119
|
-
this.realRoot = fsSync.realpathSync(this.root);
|
|
120
|
+
this.realRoot = fsSync.realpathSync.native(this.root);
|
|
120
121
|
}
|
|
121
122
|
#containsReal(p) {
|
|
122
123
|
return p === this.realRoot || p.startsWith(this.realRoot + path.sep);
|
|
@@ -633,6 +634,17 @@ var AgentHub = class extends EventEmitter {
|
|
|
633
634
|
}
|
|
634
635
|
}
|
|
635
636
|
}
|
|
637
|
+
/**
|
|
638
|
+
* Drops a session from the list, withdrawing its requests. One still running elsewhere comes
|
|
639
|
+
* back with its next hook event.
|
|
640
|
+
*/
|
|
641
|
+
forget(id) {
|
|
642
|
+
const s = this.#sessions.get(id);
|
|
643
|
+
if (!s) throw new HttpError(404, "no such session");
|
|
644
|
+
if (s.state !== "ended") this.#end(s);
|
|
645
|
+
this.#sessions.delete(id);
|
|
646
|
+
this.emit("message", { type: "session_removed", session: id, replacedBy: null });
|
|
647
|
+
}
|
|
636
648
|
/** Forgets sessions that ended more than a day ago. */
|
|
637
649
|
sweep() {
|
|
638
650
|
const cutoff = this.#now() - ENDED_TTL;
|
|
@@ -1039,12 +1051,100 @@ var LANDING_HTML = `<!doctype html>
|
|
|
1039
1051
|
`;
|
|
1040
1052
|
|
|
1041
1053
|
// ../../packages/agent-core/src/ports.js
|
|
1042
|
-
import { execFile as
|
|
1043
|
-
import
|
|
1054
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
1055
|
+
import fs6 from "node:fs/promises";
|
|
1044
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
|
|
1045
1145
|
function run(cmd2, args) {
|
|
1046
1146
|
return new Promise((resolve, reject) => {
|
|
1047
|
-
|
|
1147
|
+
execFile3(cmd2, args, { maxBuffer: 8 * 1024 * 1024 }, (err, stdout) => {
|
|
1048
1148
|
if (err?.code === "ENOENT") reject(err);
|
|
1049
1149
|
else resolve(stdout || "");
|
|
1050
1150
|
});
|
|
@@ -1096,9 +1196,9 @@ async function processTable() {
|
|
|
1096
1196
|
}
|
|
1097
1197
|
async function workingDirs(pids) {
|
|
1098
1198
|
const dirs = /* @__PURE__ */ new Map();
|
|
1099
|
-
if (!pids.length) return dirs;
|
|
1199
|
+
if (!pids.length || isWindows) return dirs;
|
|
1100
1200
|
if (os.platform() === "linux") {
|
|
1101
|
-
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))));
|
|
1102
1202
|
return dirs;
|
|
1103
1203
|
}
|
|
1104
1204
|
const out = await run("lsof", ["-a", "-p", pids.join(","), "-d", "cwd", "-F", "pn"]).catch(() => "");
|
|
@@ -1109,9 +1209,39 @@ async function workingDirs(pids) {
|
|
|
1109
1209
|
}
|
|
1110
1210
|
return dirs;
|
|
1111
1211
|
}
|
|
1112
|
-
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|$)/;
|
|
1113
1243
|
async function listPorts(shells = /* @__PURE__ */ new Map()) {
|
|
1114
|
-
const [sockets, table] = await Promise.all([listeningSockets(), processTable()]);
|
|
1244
|
+
const [sockets, table] = isWindows ? await windowsListeners() : await Promise.all([listeningSockets(), processTable()]);
|
|
1115
1245
|
const byKey = /* @__PURE__ */ new Map();
|
|
1116
1246
|
for (const s of sockets) {
|
|
1117
1247
|
const key = `${s.pid}:${s.port}`;
|
|
@@ -1159,11 +1289,11 @@ async function killListener(pid, { force = false, shells } = {}) {
|
|
|
1159
1289
|
}
|
|
1160
1290
|
|
|
1161
1291
|
// ../../packages/agent-core/src/push.js
|
|
1162
|
-
import { execFile as
|
|
1292
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
1163
1293
|
import { createCipheriv, randomBytes } from "node:crypto";
|
|
1164
1294
|
import os2 from "node:os";
|
|
1165
1295
|
import { promisify } from "node:util";
|
|
1166
|
-
var run2 = promisify(
|
|
1296
|
+
var run2 = promisify(execFile4);
|
|
1167
1297
|
var FINISHED_AFTER = 60 * 1e3;
|
|
1168
1298
|
var ACTIVE_WITHIN = 2 * 60;
|
|
1169
1299
|
async function macIdleSeconds() {
|
|
@@ -1555,17 +1685,25 @@ function detail(e) {
|
|
|
1555
1685
|
}
|
|
1556
1686
|
|
|
1557
1687
|
// ../../packages/agent-core/src/sessions.js
|
|
1688
|
+
import { execFile as execFile5 } from "node:child_process";
|
|
1558
1689
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1559
1690
|
import os3 from "node:os";
|
|
1560
1691
|
import pty from "node-pty";
|
|
1561
1692
|
var SCROLLBACK_LIMIT = 256 * 1024;
|
|
1562
|
-
function
|
|
1563
|
-
|
|
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?.();
|
|
1564
1702
|
}
|
|
1565
1703
|
var TerminalSession = class {
|
|
1566
1704
|
/**
|
|
1567
|
-
* `command` (an argv array) runs that program
|
|
1568
|
-
*
|
|
1705
|
+
* `command` (an argv array) runs that program instead of an interactive shell, started as
|
|
1706
|
+
* terminalCommand says; the session ends when it exits.
|
|
1569
1707
|
*/
|
|
1570
1708
|
constructor({ cols, rows, cwd, env = {}, command = null }) {
|
|
1571
1709
|
this.id = randomUUID2();
|
|
@@ -1574,11 +1712,10 @@ var TerminalSession = class {
|
|
|
1574
1712
|
this.bufferedBytes = 0;
|
|
1575
1713
|
this.clients = /* @__PURE__ */ new Set();
|
|
1576
1714
|
this.exitCode = null;
|
|
1577
|
-
const
|
|
1578
|
-
this.shell =
|
|
1715
|
+
const { file, args } = command ? terminalCommand(command) : interactiveShell();
|
|
1716
|
+
this.shell = command ? interactiveShell().file : file;
|
|
1579
1717
|
this.command = command;
|
|
1580
|
-
|
|
1581
|
-
this.pty = pty.spawn(shell, args, {
|
|
1718
|
+
this.pty = pty.spawn(file, args, {
|
|
1582
1719
|
name: "xterm-256color",
|
|
1583
1720
|
cols,
|
|
1584
1721
|
rows,
|
|
@@ -1586,9 +1723,11 @@ var TerminalSession = class {
|
|
|
1586
1723
|
// CODEREMOTE_TERM_ID lets `coderemote hook` tell which terminal a coding agent runs in.
|
|
1587
1724
|
env: { ...process.env, ...env, CODEREMOTE_TERM_ID: this.id, TERM: "xterm-256color", COLORTERM: "truecolor" }
|
|
1588
1725
|
});
|
|
1726
|
+
if (isWindows) guardConpty(this.pty);
|
|
1589
1727
|
this.pty.onData((data) => this.#output(Buffer.from(data, "utf8")));
|
|
1590
1728
|
this.pty.onExit(({ exitCode }) => {
|
|
1591
1729
|
this.exitCode = exitCode;
|
|
1730
|
+
if (isWindows) releaseConpty(this.pty);
|
|
1592
1731
|
for (const ws of this.clients) {
|
|
1593
1732
|
ws.send(JSON.stringify({ type: "exit", code: exitCode }));
|
|
1594
1733
|
ws.close(1e3, "session exited");
|
|
@@ -1611,7 +1750,7 @@ var TerminalSession = class {
|
|
|
1611
1750
|
if (replay.length) ws.send(replay, { binary: true });
|
|
1612
1751
|
ws.on("message", (data, isBinary) => {
|
|
1613
1752
|
if (isBinary) {
|
|
1614
|
-
this.
|
|
1753
|
+
this.write(data.toString("utf8"));
|
|
1615
1754
|
return;
|
|
1616
1755
|
}
|
|
1617
1756
|
let msg;
|
|
@@ -1620,22 +1759,25 @@ var TerminalSession = class {
|
|
|
1620
1759
|
} catch {
|
|
1621
1760
|
return;
|
|
1622
1761
|
}
|
|
1623
|
-
if (msg.type === "input" && typeof msg.data === "string") this.
|
|
1762
|
+
if (msg.type === "input" && typeof msg.data === "string") this.write(msg.data);
|
|
1624
1763
|
else if (msg.type === "resize") this.resize(msg.cols, msg.rows);
|
|
1625
1764
|
});
|
|
1626
1765
|
ws.on("close", () => this.clients.delete(ws));
|
|
1627
1766
|
}
|
|
1628
1767
|
resize(cols, rows) {
|
|
1629
|
-
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) {
|
|
1630
1769
|
this.pty.resize(cols, rows);
|
|
1631
1770
|
}
|
|
1632
1771
|
}
|
|
1633
|
-
|
|
1634
|
-
|
|
1772
|
+
/** Ends the session; on Windows, which has no signals, always forcibly (see releaseConpty). */
|
|
1773
|
+
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
|
+
});
|
|
1635
1777
|
}
|
|
1636
|
-
/** 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. */
|
|
1637
1779
|
write(data) {
|
|
1638
|
-
this.pty.write(data);
|
|
1780
|
+
if (this.exitCode === null) this.pty.write(data);
|
|
1639
1781
|
}
|
|
1640
1782
|
toJSON() {
|
|
1641
1783
|
return {
|
|
@@ -1669,28 +1811,42 @@ var SessionManager = class {
|
|
|
1669
1811
|
list() {
|
|
1670
1812
|
return [...this.#sessions.values()].map((s) => s.toJSON());
|
|
1671
1813
|
}
|
|
1672
|
-
|
|
1673
|
-
|
|
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));
|
|
1674
1830
|
}
|
|
1675
1831
|
};
|
|
1676
1832
|
|
|
1677
1833
|
// ../../packages/agent-core/src/uploads.js
|
|
1678
|
-
import { execFile as
|
|
1834
|
+
import { execFile as execFile6 } from "node:child_process";
|
|
1679
1835
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
1680
1836
|
import { createWriteStream } from "node:fs";
|
|
1681
|
-
import
|
|
1837
|
+
import fs7 from "node:fs/promises";
|
|
1682
1838
|
import os4 from "node:os";
|
|
1683
|
-
import
|
|
1839
|
+
import path5 from "node:path";
|
|
1684
1840
|
import { Transform } from "node:stream";
|
|
1685
1841
|
import { pipeline } from "node:stream/promises";
|
|
1686
1842
|
import { promisify as promisify2 } from "node:util";
|
|
1687
|
-
var run3 = promisify2(
|
|
1843
|
+
var run3 = promisify2(execFile6);
|
|
1688
1844
|
var UPLOAD_LIMIT = 100 * 1024 * 1024;
|
|
1689
1845
|
var KEEP = 7 * 24 * 60 * 60 * 1e3;
|
|
1690
1846
|
var ID = /^[0-9a-f-]{36}$/;
|
|
1691
1847
|
function safeName(name) {
|
|
1692
|
-
const
|
|
1693
|
-
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();
|
|
1694
1850
|
return clean || "file";
|
|
1695
1851
|
}
|
|
1696
1852
|
var UploadStore = class {
|
|
@@ -1703,8 +1859,8 @@ var UploadStore = class {
|
|
|
1703
1859
|
if (Number(length) > limit) throw new HttpError(413, `files are limited to ${limit / 1024 / 1024} MB`);
|
|
1704
1860
|
const id = randomUUID3();
|
|
1705
1861
|
const day = new Date(this.now()).toISOString().slice(0, 10);
|
|
1706
|
-
await
|
|
1707
|
-
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)}`);
|
|
1708
1864
|
let size = 0;
|
|
1709
1865
|
const counter = new Transform({
|
|
1710
1866
|
transform(chunk, _enc, done) {
|
|
@@ -1715,7 +1871,7 @@ var UploadStore = class {
|
|
|
1715
1871
|
try {
|
|
1716
1872
|
await pipeline(body, counter, createWriteStream(file, { mode: 384 }));
|
|
1717
1873
|
} catch (e) {
|
|
1718
|
-
await
|
|
1874
|
+
await fs7.rm(file, { force: true });
|
|
1719
1875
|
throw e;
|
|
1720
1876
|
}
|
|
1721
1877
|
let kind = type || "application/octet-stream";
|
|
@@ -1723,31 +1879,31 @@ var UploadStore = class {
|
|
|
1723
1879
|
const jpeg = file.replace(/(\.heic)?$/i, ".jpg");
|
|
1724
1880
|
try {
|
|
1725
1881
|
await run3("sips", ["-s", "format", "jpeg", file, "--out", jpeg], { timeout: 3e4 });
|
|
1726
|
-
await
|
|
1882
|
+
await fs7.rm(file);
|
|
1727
1883
|
file = jpeg;
|
|
1728
1884
|
kind = "image/jpeg";
|
|
1729
|
-
size = (await
|
|
1885
|
+
size = (await fs7.stat(file)).size;
|
|
1730
1886
|
} catch {
|
|
1731
1887
|
}
|
|
1732
1888
|
}
|
|
1733
|
-
return { id, name:
|
|
1889
|
+
return { id, name: path5.basename(file).slice(id.length + 1), type: kind, size, path: file };
|
|
1734
1890
|
}
|
|
1735
1891
|
/** The file of an upload; 404 once it's gone (swept, or never existed). */
|
|
1736
1892
|
async path(id) {
|
|
1737
1893
|
if (typeof id !== "string" || !ID.test(id)) throw new HttpError(400, "invalid upload id");
|
|
1738
1894
|
let days = [];
|
|
1739
1895
|
try {
|
|
1740
|
-
days = (await
|
|
1896
|
+
days = (await fs7.readdir(this.dir)).sort().reverse();
|
|
1741
1897
|
} catch {
|
|
1742
1898
|
}
|
|
1743
1899
|
for (const day of days) {
|
|
1744
1900
|
let names = [];
|
|
1745
1901
|
try {
|
|
1746
|
-
names = await
|
|
1902
|
+
names = await fs7.readdir(path5.join(this.dir, day));
|
|
1747
1903
|
} catch {
|
|
1748
1904
|
}
|
|
1749
1905
|
const hit = names.find((n) => n.startsWith(`${id}-`));
|
|
1750
|
-
if (hit) return
|
|
1906
|
+
if (hit) return path5.join(this.dir, day, hit);
|
|
1751
1907
|
}
|
|
1752
1908
|
throw new HttpError(404, "no such upload (they are kept for 7 days)");
|
|
1753
1909
|
}
|
|
@@ -1755,25 +1911,25 @@ var UploadStore = class {
|
|
|
1755
1911
|
async sweep() {
|
|
1756
1912
|
let days = [];
|
|
1757
1913
|
try {
|
|
1758
|
-
days = await
|
|
1914
|
+
days = await fs7.readdir(this.dir);
|
|
1759
1915
|
} catch {
|
|
1760
1916
|
return;
|
|
1761
1917
|
}
|
|
1762
1918
|
const cutoff = this.now() - KEEP;
|
|
1763
1919
|
for (const day of days) {
|
|
1764
|
-
const dir =
|
|
1920
|
+
const dir = path5.join(this.dir, day);
|
|
1765
1921
|
let names = [];
|
|
1766
1922
|
try {
|
|
1767
|
-
names = await
|
|
1923
|
+
names = await fs7.readdir(dir);
|
|
1768
1924
|
} catch {
|
|
1769
1925
|
continue;
|
|
1770
1926
|
}
|
|
1771
1927
|
for (const n of names) {
|
|
1772
|
-
const file =
|
|
1773
|
-
const stat = await
|
|
1774
|
-
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 });
|
|
1775
1931
|
}
|
|
1776
|
-
if (!(await
|
|
1932
|
+
if (!(await fs7.readdir(dir).catch(() => [])).length) await fs7.rm(dir, { recursive: true, force: true });
|
|
1777
1933
|
}
|
|
1778
1934
|
}
|
|
1779
1935
|
};
|
|
@@ -1786,11 +1942,11 @@ function kindOf(stat) {
|
|
|
1786
1942
|
return "other";
|
|
1787
1943
|
}
|
|
1788
1944
|
async function describe(fullPath, name) {
|
|
1789
|
-
const lst = await
|
|
1945
|
+
const lst = await fs8.lstat(fullPath);
|
|
1790
1946
|
const entry2 = { name, type: kindOf(lst), size: lst.size, mtime: lst.mtimeMs };
|
|
1791
1947
|
if (lst.isSymbolicLink()) {
|
|
1792
1948
|
try {
|
|
1793
|
-
entry2.target = kindOf(await
|
|
1949
|
+
entry2.target = kindOf(await fs8.stat(fullPath));
|
|
1794
1950
|
} catch {
|
|
1795
1951
|
entry2.target = "broken";
|
|
1796
1952
|
}
|
|
@@ -1830,7 +1986,7 @@ function createAgentServer({
|
|
|
1830
1986
|
uploadsDir = null,
|
|
1831
1987
|
claudeCommand = "claude",
|
|
1832
1988
|
codexCommand = "codex",
|
|
1833
|
-
codexHome = process.env.CODEX_HOME ||
|
|
1989
|
+
codexHome = process.env.CODEX_HOME || path6.join(os5.homedir(), ".codex"),
|
|
1834
1990
|
pluginInstalled = null,
|
|
1835
1991
|
agents = new AgentHub(),
|
|
1836
1992
|
pushStore: pushStore2 = null,
|
|
@@ -1885,17 +2041,6 @@ function createAgentServer({
|
|
|
1885
2041
|
if (!Array.isArray(ids) || ids.some((a) => typeof a !== "string")) throw new HttpError(400, "attachments must be upload ids");
|
|
1886
2042
|
return Promise.all(ids.map((a) => uploadPath(a)));
|
|
1887
2043
|
}
|
|
1888
|
-
function loginShell(script) {
|
|
1889
|
-
const shell = process.env.SHELL || "/bin/sh";
|
|
1890
|
-
return new Promise((resolve) => {
|
|
1891
|
-
execFile5(
|
|
1892
|
-
shell,
|
|
1893
|
-
["-l", "-c", script],
|
|
1894
|
-
{ timeout: 2e4, maxBuffer: 16 * 1024 * 1024 },
|
|
1895
|
-
(err, out) => resolve(err ? null : out)
|
|
1896
|
-
);
|
|
1897
|
-
});
|
|
1898
|
-
}
|
|
1899
2044
|
const launchers = {
|
|
1900
2045
|
claude: {
|
|
1901
2046
|
command: claudeCommand,
|
|
@@ -1910,7 +2055,7 @@ function createAgentServer({
|
|
|
1910
2055
|
// terminal's queries; the classic renderer doesn't need them.
|
|
1911
2056
|
env: { CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN: "1" },
|
|
1912
2057
|
async plugin() {
|
|
1913
|
-
const list = JSON.parse(await
|
|
2058
|
+
const list = JSON.parse(await capture([claudeCommand, "plugin", "list", "--json"]) ?? "[]");
|
|
1914
2059
|
return list.some((p) => p.id === "coderemote@coderemote" && p.enabled !== false);
|
|
1915
2060
|
},
|
|
1916
2061
|
missingPlugin: "Claude Code needs the Code Remote plugin first: run `coderemote setup claude` once on the computer."
|
|
@@ -1928,14 +2073,14 @@ function createAgentServer({
|
|
|
1928
2073
|
],
|
|
1929
2074
|
env: {},
|
|
1930
2075
|
async plugin() {
|
|
1931
|
-
const list = JSON.parse(await
|
|
2076
|
+
const list = JSON.parse(await capture([codexCommand, "plugin", "list", "--marketplace", "coderemote", "--json"]) ?? "{}");
|
|
1932
2077
|
return (list.installed ?? []).some((p) => p.pluginId === "coderemote@coderemote" && p.enabled !== false);
|
|
1933
2078
|
},
|
|
1934
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."
|
|
1935
2080
|
}
|
|
1936
2081
|
};
|
|
1937
|
-
|
|
1938
|
-
return
|
|
2082
|
+
function installed(agent) {
|
|
2083
|
+
return commandExists(launchers[agent].command);
|
|
1939
2084
|
}
|
|
1940
2085
|
const plugins = {};
|
|
1941
2086
|
async function hasPlugin(agent) {
|
|
@@ -1951,7 +2096,7 @@ function createAgentServer({
|
|
|
1951
2096
|
}
|
|
1952
2097
|
async function codexModels() {
|
|
1953
2098
|
try {
|
|
1954
|
-
const { models } = JSON.parse(await
|
|
2099
|
+
const { models } = JSON.parse(await fs8.readFile(path6.join(codexHome, "models_cache.json"), "utf8"));
|
|
1955
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 ?? "" }));
|
|
1956
2101
|
} catch {
|
|
1957
2102
|
return [];
|
|
@@ -2001,20 +2146,20 @@ function createAgentServer({
|
|
|
2001
2146
|
async function repoOf(p) {
|
|
2002
2147
|
const repo = await sandbox.resolve(p);
|
|
2003
2148
|
try {
|
|
2004
|
-
await
|
|
2149
|
+
await fs8.access(path6.join(repo, ".git"));
|
|
2005
2150
|
} catch {
|
|
2006
2151
|
throw new HttpError(400, "not a git repository");
|
|
2007
2152
|
}
|
|
2008
2153
|
return repo;
|
|
2009
2154
|
}
|
|
2010
2155
|
async function filesIn(repo, files) {
|
|
2011
|
-
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))) {
|
|
2012
2157
|
throw new HttpError(400, "files must be a non-empty list of paths relative to the repository");
|
|
2013
2158
|
}
|
|
2014
2159
|
return Promise.all(
|
|
2015
2160
|
files.map(async (f) => {
|
|
2016
|
-
const abs = await sandbox.resolve(
|
|
2017
|
-
const rel =
|
|
2161
|
+
const abs = await sandbox.resolve(path6.join(repo, f), { allowMissing: true });
|
|
2162
|
+
const rel = path6.relative(repo, abs);
|
|
2018
2163
|
if (!rel || rel.startsWith("..")) throw new HttpError(403, "path is outside the repository");
|
|
2019
2164
|
return rel;
|
|
2020
2165
|
})
|
|
@@ -2027,21 +2172,22 @@ function createAgentServer({
|
|
|
2027
2172
|
platform: os5.platform(),
|
|
2028
2173
|
root: sandbox.root,
|
|
2029
2174
|
user: os5.userInfo().username,
|
|
2030
|
-
|
|
2175
|
+
// On Windows, terminals run PowerShell whatever $SHELL (Git Bash sets one) says.
|
|
2176
|
+
shell: isWindows ? interactiveShell().file : process.env.SHELL || null
|
|
2031
2177
|
}),
|
|
2032
2178
|
[Routes.fsList]: async (url) => {
|
|
2033
2179
|
const dir = await sandbox.resolve(url.searchParams.get("path"));
|
|
2034
|
-
const names = await
|
|
2035
|
-
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);
|
|
2036
2182
|
return { path: dir, entries };
|
|
2037
2183
|
},
|
|
2038
2184
|
[Routes.fsStat]: async (url) => {
|
|
2039
2185
|
const p = await sandbox.resolve(url.searchParams.get("path"));
|
|
2040
|
-
return { path: p, ...await describe(p,
|
|
2186
|
+
return { path: p, ...await describe(p, path6.basename(p)) };
|
|
2041
2187
|
},
|
|
2042
2188
|
[Routes.fsRead]: async (url, req, res) => {
|
|
2043
2189
|
const p = await sandbox.resolve(url.searchParams.get("path"));
|
|
2044
|
-
const stat = await
|
|
2190
|
+
const stat = await fs8.stat(p);
|
|
2045
2191
|
if (!stat.isFile()) throw new HttpError(400, "not a file");
|
|
2046
2192
|
res.writeHead(200, { "content-type": "application/octet-stream", "content-length": stat.size });
|
|
2047
2193
|
await pipeline2(createReadStream(p), res);
|
|
@@ -2051,24 +2197,24 @@ function createAgentServer({
|
|
|
2051
2197
|
const tmp = `${p}.vsremote-${process.pid}-${Date.now()}.tmp`;
|
|
2052
2198
|
try {
|
|
2053
2199
|
await pipeline2(req, createWriteStream2(tmp));
|
|
2054
|
-
await
|
|
2200
|
+
await fs8.rename(tmp, p);
|
|
2055
2201
|
} catch (e) {
|
|
2056
|
-
await
|
|
2202
|
+
await fs8.rm(tmp, { force: true });
|
|
2057
2203
|
throw e;
|
|
2058
2204
|
}
|
|
2059
|
-
return describe(p,
|
|
2205
|
+
return describe(p, path6.basename(p));
|
|
2060
2206
|
},
|
|
2061
2207
|
[Routes.fsMkdir]: async (url) => {
|
|
2062
|
-
await
|
|
2208
|
+
await fs8.mkdir(await sandbox.resolve(url.searchParams.get("path"), { allowMissing: true }), { recursive: true });
|
|
2063
2209
|
return {};
|
|
2064
2210
|
},
|
|
2065
2211
|
[Routes.fsRename]: async (url, req) => {
|
|
2066
2212
|
const { from, to } = await readJson(req);
|
|
2067
|
-
await
|
|
2213
|
+
await fs8.rename(await sandbox.resolveChild(from), await sandbox.resolveChild(to, { allowMissing: true }));
|
|
2068
2214
|
return {};
|
|
2069
2215
|
},
|
|
2070
2216
|
[Routes.fsDelete]: async (url) => {
|
|
2071
|
-
await
|
|
2217
|
+
await fs8.rm(await sandbox.resolveChild(url.searchParams.get("path")), { recursive: true });
|
|
2072
2218
|
return {};
|
|
2073
2219
|
},
|
|
2074
2220
|
[Routes.gitRepos]: async () => ({ repos: await findRepos(sandbox.root) }),
|
|
@@ -2203,7 +2349,7 @@ function createAgentServer({
|
|
|
2203
2349
|
} catch (e) {
|
|
2204
2350
|
throw new HttpError(400, e.message);
|
|
2205
2351
|
}
|
|
2206
|
-
if (!(await
|
|
2352
|
+
if (!(await fs8.stat(dir)).isDirectory()) throw new HttpError(400, "cwd must be a folder");
|
|
2207
2353
|
if (model !== void 0 && (typeof model !== "string" || !/^[A-Za-z0-9._[\]-]{1,80}$/.test(model))) {
|
|
2208
2354
|
throw new HttpError(400, "invalid model");
|
|
2209
2355
|
}
|
|
@@ -2225,7 +2371,7 @@ function createAgentServer({
|
|
|
2225
2371
|
throw Object.assign(new HttpError(400, launcher.missingPlugin), { code: "ENOPLUGIN" });
|
|
2226
2372
|
}
|
|
2227
2373
|
const id = resume ?? randomUUID4();
|
|
2228
|
-
if (uploadsDir) await
|
|
2374
|
+
if (uploadsDir) await fs8.mkdir(uploadsDir, { recursive: true, mode: 448 });
|
|
2229
2375
|
const argv2 = [
|
|
2230
2376
|
launcher.command,
|
|
2231
2377
|
...launcher.args({
|
|
@@ -2288,6 +2434,25 @@ function createAgentServer({
|
|
|
2288
2434
|
}
|
|
2289
2435
|
return {};
|
|
2290
2436
|
},
|
|
2437
|
+
[Routes.agentsStop]: async (url, req) => {
|
|
2438
|
+
const { session: id } = await readJson(req);
|
|
2439
|
+
const { term } = typingTarget(id, { whileWaiting: true });
|
|
2440
|
+
term.kill();
|
|
2441
|
+
setTimeout(() => {
|
|
2442
|
+
if (term.exitCode === null) term.kill("SIGKILL");
|
|
2443
|
+
}, 3e3).unref();
|
|
2444
|
+
return {};
|
|
2445
|
+
},
|
|
2446
|
+
[Routes.agentsForget]: async (url, req) => {
|
|
2447
|
+
const { session: id } = await readJson(req);
|
|
2448
|
+
const session = typeof id === "string" ? agents.session(id) : null;
|
|
2449
|
+
if (!session) throw new HttpError(404, "no such session");
|
|
2450
|
+
if (session.state !== "ended" && session.terminal && sessions.get(session.terminal)) {
|
|
2451
|
+
throw new HttpError(409, "this session still runs here; stop it first");
|
|
2452
|
+
}
|
|
2453
|
+
agents.forget(id);
|
|
2454
|
+
return {};
|
|
2455
|
+
},
|
|
2291
2456
|
[Routes.agentsAnswer]: async (url, req) => {
|
|
2292
2457
|
agents.answer(await readJson(req));
|
|
2293
2458
|
return {};
|
|
@@ -2443,42 +2608,33 @@ function createAgentServer({
|
|
|
2443
2608
|
}
|
|
2444
2609
|
|
|
2445
2610
|
// src/devtunnel.js
|
|
2446
|
-
import { execFile as
|
|
2611
|
+
import { execFile as execFile7 } from "node:child_process";
|
|
2447
2612
|
import { createHash } from "node:crypto";
|
|
2448
|
-
import
|
|
2449
|
-
import
|
|
2613
|
+
import fs9 from "node:fs";
|
|
2614
|
+
import path7 from "node:path";
|
|
2450
2615
|
import { promisify as promisify3 } from "node:util";
|
|
2451
|
-
var run4 = promisify3(
|
|
2616
|
+
var run4 = promisify3(execFile7);
|
|
2452
2617
|
var DOWNLOAD_BASE = "https://tunnelsassetsprod.blob.core.windows.net/cli";
|
|
2453
2618
|
var BUILDS = {
|
|
2454
2619
|
"darwin-arm64": "osx-arm64-devtunnel",
|
|
2455
2620
|
"darwin-x64": "osx-x64-devtunnel",
|
|
2456
2621
|
"linux-arm64": "linux-arm64-devtunnel",
|
|
2457
|
-
"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"
|
|
2458
2626
|
};
|
|
2459
2627
|
var MICROSOFT_TEAM_ID = "UBF8T346G9";
|
|
2628
|
+
var MICROSOFT_SIGNER = /^CN=Microsoft Corporation, O=Microsoft Corporation,/;
|
|
2460
2629
|
var REFRESH_INTERVAL_MS = 24 * 36e5;
|
|
2461
|
-
function executable(file) {
|
|
2462
|
-
try {
|
|
2463
|
-
fs8.accessSync(file, fs8.constants.X_OK);
|
|
2464
|
-
return fs8.statSync(file).isFile();
|
|
2465
|
-
} catch {
|
|
2466
|
-
return false;
|
|
2467
|
-
}
|
|
2468
|
-
}
|
|
2469
|
-
function onPath(name) {
|
|
2470
|
-
for (const dir of (process.env.PATH || "").split(path6.delimiter)) {
|
|
2471
|
-
if (dir && executable(path6.join(dir, name))) return path6.join(dir, name);
|
|
2472
|
-
}
|
|
2473
|
-
return null;
|
|
2474
|
-
}
|
|
2475
2630
|
async function ensureDevtunnel({ dir, progress }) {
|
|
2476
2631
|
const override = process.env.CODEREMOTE_DEVTUNNEL;
|
|
2477
2632
|
if (override) {
|
|
2478
|
-
|
|
2633
|
+
const found = findExecutable(override);
|
|
2634
|
+
if (found) return found;
|
|
2479
2635
|
throw new Error(`CODEREMOTE_DEVTUNNEL=${override} isn't an executable file.`);
|
|
2480
2636
|
}
|
|
2481
|
-
const fromPath =
|
|
2637
|
+
const fromPath = findExecutable("devtunnel");
|
|
2482
2638
|
if (fromPath) return fromPath;
|
|
2483
2639
|
const build = BUILDS[`${process.platform}-${process.arch}`];
|
|
2484
2640
|
if (!build) {
|
|
@@ -2486,14 +2642,14 @@ async function ensureDevtunnel({ dir, progress }) {
|
|
|
2486
2642
|
`No devtunnel build for ${process.platform}-${process.arch}. Install it yourself: https://learn.microsoft.com/azure/developer/dev-tunnels/get-started`
|
|
2487
2643
|
);
|
|
2488
2644
|
}
|
|
2489
|
-
const bin =
|
|
2490
|
-
const metaFile =
|
|
2645
|
+
const bin = path7.join(dir, isWindows ? "devtunnel.exe" : "devtunnel");
|
|
2646
|
+
const metaFile = path7.join(dir, "devtunnel.json");
|
|
2491
2647
|
let meta = {};
|
|
2492
2648
|
try {
|
|
2493
|
-
meta = JSON.parse(
|
|
2649
|
+
meta = JSON.parse(fs9.readFileSync(metaFile, "utf8"));
|
|
2494
2650
|
} catch {
|
|
2495
2651
|
}
|
|
2496
|
-
const installed =
|
|
2652
|
+
const installed = findExecutable(bin) !== null;
|
|
2497
2653
|
if (installed && Date.now() - (meta.checkedAt ?? 0) < REFRESH_INTERVAL_MS) return bin;
|
|
2498
2654
|
const url = `${DOWNLOAD_BASE}/${build}`;
|
|
2499
2655
|
if (installed) {
|
|
@@ -2501,23 +2657,27 @@ async function ensureDevtunnel({ dir, progress }) {
|
|
|
2501
2657
|
const head = await fetch(url, { method: "HEAD", signal: AbortSignal.timeout(3e3) });
|
|
2502
2658
|
const lastModified = head.headers.get("last-modified");
|
|
2503
2659
|
if (head.ok && lastModified && lastModified === meta.lastModified) {
|
|
2504
|
-
|
|
2660
|
+
fs9.writeFileSync(metaFile, JSON.stringify({ ...meta, checkedAt: Date.now() }));
|
|
2505
2661
|
return bin;
|
|
2506
2662
|
}
|
|
2507
2663
|
} catch {
|
|
2508
2664
|
return bin;
|
|
2509
2665
|
}
|
|
2510
2666
|
}
|
|
2511
|
-
|
|
2667
|
+
try {
|
|
2668
|
+
await download(url, bin, metaFile, progress);
|
|
2669
|
+
} catch (e) {
|
|
2670
|
+
if (!installed) throw e;
|
|
2671
|
+
}
|
|
2512
2672
|
return bin;
|
|
2513
2673
|
}
|
|
2514
2674
|
async function download(url, bin, metaFile, progress) {
|
|
2515
|
-
|
|
2675
|
+
fs9.mkdirSync(path7.dirname(bin), { recursive: true, mode: 448 });
|
|
2516
2676
|
const res = await fetch(url, { signal: AbortSignal.timeout(12e4) });
|
|
2517
2677
|
if (!res.ok || !res.body) throw new Error(`Downloading devtunnel failed: HTTP ${res.status} from ${url}`);
|
|
2518
2678
|
const total = Number(res.headers.get("content-length")) || 0;
|
|
2519
2679
|
const tmp = `${bin}.${process.pid}.download`;
|
|
2520
|
-
const out =
|
|
2680
|
+
const out = fs9.createWriteStream(tmp, { mode: 493 });
|
|
2521
2681
|
const md5 = createHash("md5");
|
|
2522
2682
|
let received = 0;
|
|
2523
2683
|
try {
|
|
@@ -2530,17 +2690,18 @@ async function download(url, bin, metaFile, progress) {
|
|
|
2530
2690
|
await new Promise((resolve, reject) => out.end((e) => e ? reject(e) : resolve()));
|
|
2531
2691
|
if (total && received !== total) throw new Error(`devtunnel download was cut short (${received} of ${total} bytes)`);
|
|
2532
2692
|
await verify(tmp, md5.digest("base64"), res.headers.get("content-md5"));
|
|
2533
|
-
|
|
2534
|
-
|
|
2693
|
+
fs9.chmodSync(tmp, 493);
|
|
2694
|
+
fs9.renameSync(tmp, bin);
|
|
2535
2695
|
} catch (e) {
|
|
2536
2696
|
out.destroy();
|
|
2537
|
-
|
|
2697
|
+
fs9.rmSync(tmp, { force: true });
|
|
2538
2698
|
throw e;
|
|
2539
2699
|
}
|
|
2540
|
-
|
|
2700
|
+
fs9.writeFileSync(metaFile, JSON.stringify({ lastModified: res.headers.get("last-modified"), checkedAt: Date.now() }));
|
|
2541
2701
|
}
|
|
2542
2702
|
async function verify(file, md5, expectedMd5) {
|
|
2543
2703
|
if (expectedMd5 && md5 !== expectedMd5) throw new Error("devtunnel download is corrupt (MD5 mismatch)");
|
|
2704
|
+
if (isWindows) return verifyAuthenticode(file);
|
|
2544
2705
|
if (process.platform !== "darwin") return;
|
|
2545
2706
|
try {
|
|
2546
2707
|
await run4("codesign", ["--verify", "--strict", file]);
|
|
@@ -2550,52 +2711,69 @@ async function verify(file, md5, expectedMd5) {
|
|
|
2550
2711
|
throw new Error(`devtunnel download failed its signature check: ${e.message}`);
|
|
2551
2712
|
}
|
|
2552
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
|
+
}
|
|
2553
2730
|
|
|
2554
2731
|
// src/hook.js
|
|
2555
2732
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
2556
|
-
import
|
|
2733
|
+
import fs10 from "node:fs";
|
|
2557
2734
|
import http3 from "node:http";
|
|
2558
|
-
import
|
|
2735
|
+
import path8 from "node:path";
|
|
2559
2736
|
var CONNECT_TIMEOUT = 1e3;
|
|
2560
2737
|
var EVENT_TIMEOUT = 5e3;
|
|
2561
2738
|
function ensureHookSecret(dir) {
|
|
2562
|
-
const file =
|
|
2739
|
+
const file = path8.join(dir, "hook-secret");
|
|
2563
2740
|
try {
|
|
2564
|
-
const secret2 =
|
|
2741
|
+
const secret2 = fs10.readFileSync(file, "utf8").trim();
|
|
2565
2742
|
if (secret2) return secret2;
|
|
2566
2743
|
} catch {
|
|
2567
2744
|
}
|
|
2568
|
-
|
|
2745
|
+
fs10.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
2569
2746
|
const secret = randomBytes2(32).toString("base64url");
|
|
2570
|
-
|
|
2747
|
+
fs10.writeFileSync(file, secret, { mode: 384 });
|
|
2571
2748
|
return secret;
|
|
2572
2749
|
}
|
|
2573
2750
|
function writeHookLauncher(dir, { node = process.execPath, script }) {
|
|
2574
|
-
const bin =
|
|
2575
|
-
|
|
2751
|
+
const bin = path8.join(dir, "bin");
|
|
2752
|
+
fs10.mkdirSync(bin, { recursive: true, mode: 448 });
|
|
2576
2753
|
const quote = (s) => `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
2577
|
-
const file =
|
|
2578
|
-
|
|
2754
|
+
const file = path8.join(bin, "hook");
|
|
2755
|
+
fs10.writeFileSync(file, `#!/bin/sh
|
|
2579
2756
|
exec ${quote(node)} ${quote(script)} hook "$@"
|
|
2580
2757
|
`, { mode: 493 });
|
|
2581
|
-
|
|
2758
|
+
fs10.chmodSync(file, 493);
|
|
2759
|
+
fs10.writeFileSync(path8.join(bin, "hook.json"), JSON.stringify({ node, script }), { mode: 384 });
|
|
2582
2760
|
return file;
|
|
2583
2761
|
}
|
|
2584
2762
|
function readHookSecret(dir) {
|
|
2585
2763
|
try {
|
|
2586
|
-
return
|
|
2764
|
+
return fs10.readFileSync(path8.join(dir, "hook-secret"), "utf8").trim() || null;
|
|
2587
2765
|
} catch {
|
|
2588
2766
|
return null;
|
|
2589
2767
|
}
|
|
2590
2768
|
}
|
|
2591
2769
|
function real(p) {
|
|
2592
2770
|
try {
|
|
2593
|
-
return
|
|
2771
|
+
return fs10.realpathSync.native(p);
|
|
2594
2772
|
} catch {
|
|
2595
|
-
return
|
|
2773
|
+
return path8.resolve(p);
|
|
2596
2774
|
}
|
|
2597
2775
|
}
|
|
2598
|
-
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);
|
|
2599
2777
|
function hookTarget(agents, cwd, env = process.env) {
|
|
2600
2778
|
const port2 = parseInt(env.CODEREMOTE_AGENT_PORT, 10);
|
|
2601
2779
|
if (port2 && agents.some((a) => a.port === port2)) return { port: port2, terminal: env.CODEREMOTE_TERM_ID || null };
|
|
@@ -2651,8 +2829,80 @@ async function runHook({ input, agents, secret, agent = "claude", env = process.
|
|
|
2651
2829
|
}
|
|
2652
2830
|
|
|
2653
2831
|
// src/preview.js
|
|
2654
|
-
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
|
|
2655
2904
|
var HOST_READY_TIMEOUT_MS = 3e4;
|
|
2905
|
+
var CLOSE_TIMEOUT_MS = 5e3;
|
|
2656
2906
|
var TOKEN_REUSE_MS = 12 * 36e5;
|
|
2657
2907
|
var PreviewTunnel = class {
|
|
2658
2908
|
#queue = Promise.resolve();
|
|
@@ -2732,8 +2982,13 @@ var PreviewTunnel = class {
|
|
|
2732
2982
|
return this.#serial(async () => {
|
|
2733
2983
|
const url = this.#host?.urls.get(port2);
|
|
2734
2984
|
if (url) return { port: port2, url };
|
|
2735
|
-
|
|
2736
|
-
|
|
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
|
+
}
|
|
2737
2992
|
if (!res.ok && !/already exists|conflict/i.test(res.out)) throw new Error(`devtunnel port create failed: ${res.out.trim()}`);
|
|
2738
2993
|
this.#ports.add(port2);
|
|
2739
2994
|
try {
|
|
@@ -2768,20 +3023,42 @@ var PreviewTunnel = class {
|
|
|
2768
3023
|
stop() {
|
|
2769
3024
|
this.#stopHost();
|
|
2770
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
|
+
}
|
|
2771
3047
|
async #ensureTunnel() {
|
|
2772
3048
|
if (this.#tunnelId) return this.#tunnelId;
|
|
2773
3049
|
if (this.leftover) await this.devtunnel("delete", this.leftover, "-f");
|
|
2774
|
-
const
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
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
|
+
}
|
|
2785
3062
|
const id = JSON.parse(created.out.slice(created.out.indexOf("{"))).tunnel?.tunnelId;
|
|
2786
3063
|
if (!id) throw new Error(`could not parse tunnel id from:
|
|
2787
3064
|
${created.out}`);
|
|
@@ -2799,7 +3076,7 @@ ${created.out}`);
|
|
|
2799
3076
|
}
|
|
2800
3077
|
#startHost() {
|
|
2801
3078
|
return new Promise((resolve, reject) => {
|
|
2802
|
-
const child =
|
|
3079
|
+
const child = spawn2(this.command(), ["host", this.#tunnelId], { stdio: ["ignore", "pipe", "pipe"] });
|
|
2803
3080
|
const urls = /* @__PURE__ */ new Map();
|
|
2804
3081
|
let log2 = "";
|
|
2805
3082
|
const timer = setTimeout(() => {
|
|
@@ -2834,40 +3111,40 @@ ${log2}`));
|
|
|
2834
3111
|
};
|
|
2835
3112
|
|
|
2836
3113
|
// src/push-store.js
|
|
2837
|
-
import
|
|
2838
|
-
import
|
|
3114
|
+
import fs11 from "node:fs";
|
|
3115
|
+
import path9 from "node:path";
|
|
2839
3116
|
function pushStore(dir, port2) {
|
|
2840
|
-
const file =
|
|
3117
|
+
const file = path9.join(dir, `${port2}.json`);
|
|
2841
3118
|
return {
|
|
2842
3119
|
load() {
|
|
2843
3120
|
try {
|
|
2844
|
-
const devices = JSON.parse(
|
|
3121
|
+
const devices = JSON.parse(fs11.readFileSync(file, "utf8"));
|
|
2845
3122
|
return Array.isArray(devices) ? devices : [];
|
|
2846
3123
|
} catch {
|
|
2847
3124
|
return [];
|
|
2848
3125
|
}
|
|
2849
3126
|
},
|
|
2850
3127
|
save(devices) {
|
|
2851
|
-
|
|
2852
|
-
|
|
3128
|
+
fs11.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
3129
|
+
fs11.writeFileSync(file, JSON.stringify(devices, null, 2), { mode: 384 });
|
|
2853
3130
|
}
|
|
2854
3131
|
};
|
|
2855
3132
|
}
|
|
2856
3133
|
|
|
2857
3134
|
// src/registry.js
|
|
2858
|
-
import
|
|
2859
|
-
import
|
|
3135
|
+
import fs12 from "node:fs";
|
|
3136
|
+
import path10 from "node:path";
|
|
2860
3137
|
var AgentRegistry = class {
|
|
2861
3138
|
constructor(dir) {
|
|
2862
3139
|
this.dir = dir;
|
|
2863
3140
|
}
|
|
2864
3141
|
register({ port: port2, root: root2, url }) {
|
|
2865
|
-
|
|
3142
|
+
fs12.mkdirSync(this.dir, { recursive: true, mode: 448 });
|
|
2866
3143
|
const file = this.#file(port2);
|
|
2867
|
-
|
|
3144
|
+
fs12.writeFileSync(file, JSON.stringify({ pid: process.pid, port: port2, root: root2, url, startedAt: Date.now() }), { mode: 384 });
|
|
2868
3145
|
const cleanup = () => {
|
|
2869
3146
|
try {
|
|
2870
|
-
if (JSON.parse(
|
|
3147
|
+
if (JSON.parse(fs12.readFileSync(file, "utf8")).pid === process.pid) fs12.rmSync(file);
|
|
2871
3148
|
} catch {
|
|
2872
3149
|
}
|
|
2873
3150
|
};
|
|
@@ -2878,25 +3155,25 @@ var AgentRegistry = class {
|
|
|
2878
3155
|
list() {
|
|
2879
3156
|
let names = [];
|
|
2880
3157
|
try {
|
|
2881
|
-
names =
|
|
3158
|
+
names = fs12.readdirSync(this.dir).filter((n) => n.endsWith(".json"));
|
|
2882
3159
|
} catch {
|
|
2883
3160
|
return [];
|
|
2884
3161
|
}
|
|
2885
3162
|
const agents = [];
|
|
2886
3163
|
for (const name of names) {
|
|
2887
|
-
const file =
|
|
3164
|
+
const file = path10.join(this.dir, name);
|
|
2888
3165
|
try {
|
|
2889
|
-
const agent = JSON.parse(
|
|
3166
|
+
const agent = JSON.parse(fs12.readFileSync(file, "utf8"));
|
|
2890
3167
|
process.kill(agent.pid, 0);
|
|
2891
3168
|
agents.push(agent);
|
|
2892
3169
|
} catch {
|
|
2893
|
-
|
|
3170
|
+
fs12.rmSync(file, { force: true });
|
|
2894
3171
|
}
|
|
2895
3172
|
}
|
|
2896
3173
|
return agents.sort((a, b) => a.port - b.port);
|
|
2897
3174
|
}
|
|
2898
3175
|
#file(port2) {
|
|
2899
|
-
return
|
|
3176
|
+
return path10.join(this.dir, `${port2}.json`);
|
|
2900
3177
|
}
|
|
2901
3178
|
};
|
|
2902
3179
|
|
|
@@ -2960,8 +3237,8 @@ function tunnelStatus({ devtunnel: devtunnel2, tunnelId, account, anonymous: ano
|
|
|
2960
3237
|
}
|
|
2961
3238
|
|
|
2962
3239
|
// src/update.js
|
|
2963
|
-
import
|
|
2964
|
-
import
|
|
3240
|
+
import fs13 from "node:fs";
|
|
3241
|
+
import path11 from "node:path";
|
|
2965
3242
|
var LATEST_URL = "https://registry.npmjs.org/coderemote/latest";
|
|
2966
3243
|
var CHECK_INTERVAL_MS = 24 * 36e5;
|
|
2967
3244
|
var SEMVER = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
|
|
@@ -2985,10 +3262,10 @@ async function fetchLatest({ fetch: fetch2 = globalThis.fetch, timeoutMs = 3e3 }
|
|
|
2985
3262
|
}
|
|
2986
3263
|
}
|
|
2987
3264
|
async function availableUpdate({ current, dir, fetch: fetch2, now = Date.now() }) {
|
|
2988
|
-
const file =
|
|
3265
|
+
const file = path11.join(dir, "update.json");
|
|
2989
3266
|
let cache = {};
|
|
2990
3267
|
try {
|
|
2991
|
-
cache = JSON.parse(
|
|
3268
|
+
cache = JSON.parse(fs13.readFileSync(file, "utf8"));
|
|
2992
3269
|
} catch {
|
|
2993
3270
|
}
|
|
2994
3271
|
let latest = cache.latest;
|
|
@@ -2997,8 +3274,8 @@ async function availableUpdate({ current, dir, fetch: fetch2, now = Date.now() }
|
|
|
2997
3274
|
if (fetched) {
|
|
2998
3275
|
latest = fetched;
|
|
2999
3276
|
try {
|
|
3000
|
-
|
|
3001
|
-
|
|
3277
|
+
fs13.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
3278
|
+
fs13.writeFileSync(file, JSON.stringify({ latest, checkedAt: now }));
|
|
3002
3279
|
} catch {
|
|
3003
3280
|
}
|
|
3004
3281
|
}
|
|
@@ -3006,7 +3283,14 @@ async function availableUpdate({ current, dir, fetch: fetch2, now = Date.now() }
|
|
|
3006
3283
|
return isNewer(latest, current) ? latest : null;
|
|
3007
3284
|
}
|
|
3008
3285
|
function installCommand(installedAt) {
|
|
3009
|
-
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"]];
|
|
3010
3294
|
if (p.includes("/pnpm/") || p.includes("/.pnpm/")) return ["pnpm", ["add", "-g", "coderemote@latest"]];
|
|
3011
3295
|
if (p.includes("/.bun/")) return ["bun", ["add", "-g", "coderemote@latest"]];
|
|
3012
3296
|
if (p.includes("/.yarn/") || p.includes("/yarn/global/")) return ["yarn", ["global", "add", "coderemote@latest"]];
|
|
@@ -3016,7 +3300,7 @@ function installCommand(installedAt) {
|
|
|
3016
3300
|
// package.json
|
|
3017
3301
|
var package_default = {
|
|
3018
3302
|
name: "coderemote",
|
|
3019
|
-
version: "0.3.
|
|
3303
|
+
version: "0.3.7",
|
|
3020
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.",
|
|
3021
3305
|
keywords: [
|
|
3022
3306
|
"remote",
|
|
@@ -3057,19 +3341,15 @@ var package_default = {
|
|
|
3057
3341
|
"scripts/fix-node-pty.js"
|
|
3058
3342
|
],
|
|
3059
3343
|
engines: {
|
|
3060
|
-
node: ">=
|
|
3344
|
+
node: ">=22"
|
|
3061
3345
|
},
|
|
3062
|
-
os: [
|
|
3063
|
-
"darwin",
|
|
3064
|
-
"linux"
|
|
3065
|
-
],
|
|
3066
3346
|
scripts: {
|
|
3067
3347
|
build: "node scripts/build.js",
|
|
3068
3348
|
prepare: "node scripts/build.js",
|
|
3069
|
-
prepack:
|
|
3070
|
-
postpack:
|
|
3349
|
+
prepack: `node scripts/build.js && node -e "require('fs').copyFileSync('../../LICENSE', 'LICENSE')"`,
|
|
3350
|
+
postpack: `node -e "require('fs').rmSync('LICENSE', { force: true })"`,
|
|
3071
3351
|
postinstall: "node scripts/fix-node-pty.js",
|
|
3072
|
-
test:
|
|
3352
|
+
test: 'node --test "test/*.test.js"'
|
|
3073
3353
|
},
|
|
3074
3354
|
dependencies: {
|
|
3075
3355
|
"node-pty": "^1.1.0",
|
|
@@ -3295,15 +3575,12 @@ var Spinner = class {
|
|
|
3295
3575
|
};
|
|
3296
3576
|
|
|
3297
3577
|
// src/vscode-tunnel.js
|
|
3298
|
-
import { execFile as execFile7, spawn as spawn2 } from "node:child_process";
|
|
3299
|
-
import { promisify as promisify4 } from "node:util";
|
|
3300
|
-
var run5 = promisify4(execFile7);
|
|
3301
3578
|
function findCode() {
|
|
3302
|
-
return process.env.CODEREMOTE_CODE ||
|
|
3579
|
+
return process.env.CODEREMOTE_CODE || findExecutable("code");
|
|
3303
3580
|
}
|
|
3304
3581
|
async function codeTunnelSignedIn(code) {
|
|
3305
3582
|
try {
|
|
3306
|
-
const { stdout } = await
|
|
3583
|
+
const { stdout } = await execCommand([code, "tunnel", "user", "show"]);
|
|
3307
3584
|
return /^logged in/im.test(stdout);
|
|
3308
3585
|
} catch {
|
|
3309
3586
|
return false;
|
|
@@ -3311,12 +3588,12 @@ async function codeTunnelSignedIn(code) {
|
|
|
3311
3588
|
}
|
|
3312
3589
|
function codeTunnelLogin(code) {
|
|
3313
3590
|
return new Promise((resolve) => {
|
|
3314
|
-
|
|
3591
|
+
spawnCommand([code, "tunnel", "user", "login", "--provider", "github"], { stdio: "inherit" }).on("exit", resolve);
|
|
3315
3592
|
});
|
|
3316
3593
|
}
|
|
3317
3594
|
function hostCodeTunnel(code, root2, timeoutMs = 6e4) {
|
|
3318
3595
|
return new Promise((resolve, reject) => {
|
|
3319
|
-
const child =
|
|
3596
|
+
const child = spawnCommand([code, "tunnel"], { cwd: root2, stdio: ["ignore", "pipe", "pipe"] });
|
|
3320
3597
|
let log2 = "";
|
|
3321
3598
|
const timer = setTimeout(() => {
|
|
3322
3599
|
child.kill();
|
|
@@ -3328,7 +3605,8 @@ ${log2}`));
|
|
|
3328
3605
|
const m = log2.match(/https:\/\/vscode\.dev\/tunnel\/([^/\s]+)/);
|
|
3329
3606
|
if (!m) return;
|
|
3330
3607
|
clearTimeout(timer);
|
|
3331
|
-
|
|
3608
|
+
const folder = isWindows ? `/${root2.replaceAll("\\", "/")}` : root2;
|
|
3609
|
+
resolve({ child, url: `https://vscode.dev/tunnel/${m[1]}${encodeURI(folder)}` });
|
|
3332
3610
|
};
|
|
3333
3611
|
child.stdout.on("data", onData);
|
|
3334
3612
|
child.stderr.on("data", onData);
|
|
@@ -3345,28 +3623,28 @@ ${log2}`));
|
|
|
3345
3623
|
}
|
|
3346
3624
|
|
|
3347
3625
|
// bin/coderemote.js
|
|
3348
|
-
var
|
|
3349
|
-
var CONFIG_DIR =
|
|
3350
|
-
var LEGACY_CONFIG_DIR =
|
|
3351
|
-
if (!
|
|
3352
|
-
var CONFIG_FILE =
|
|
3353
|
-
var PUSH_DIR =
|
|
3354
|
-
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");
|
|
3355
3633
|
var DEFAULT_PORT = 31580;
|
|
3356
3634
|
var VERSION = package_default.version;
|
|
3357
|
-
var PACKAGED =
|
|
3635
|
+
var PACKAGED = path12.basename(path12.dirname(fileURLToPath(import.meta.url))) === "dist";
|
|
3358
3636
|
var spinner = new Spinner();
|
|
3359
3637
|
function loadConfig() {
|
|
3360
3638
|
let cfg2 = {};
|
|
3361
3639
|
try {
|
|
3362
|
-
cfg2 = JSON.parse(
|
|
3640
|
+
cfg2 = JSON.parse(fs14.readFileSync(CONFIG_FILE, "utf8"));
|
|
3363
3641
|
} catch {
|
|
3364
3642
|
}
|
|
3365
3643
|
if (!cfg2.token) {
|
|
3366
3644
|
cfg2.token = randomBytes3(32).toString("base64url");
|
|
3367
3645
|
saveConfig(cfg2);
|
|
3368
|
-
|
|
3369
|
-
|
|
3646
|
+
fs14.rmSync(PUSH_DIR, { recursive: true, force: true });
|
|
3647
|
+
fs14.rmSync(UPLOADS_DIR, { recursive: true, force: true });
|
|
3370
3648
|
}
|
|
3371
3649
|
if (cfg2.tunnelId && !cfg2.tunnels) {
|
|
3372
3650
|
cfg2.tunnels = { [DEFAULT_PORT]: cfg2.tunnelId };
|
|
@@ -3374,9 +3652,16 @@ function loadConfig() {
|
|
|
3374
3652
|
}
|
|
3375
3653
|
return cfg2;
|
|
3376
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
|
+
}
|
|
3377
3662
|
function saveConfig(cfg2) {
|
|
3378
|
-
|
|
3379
|
-
|
|
3663
|
+
fs14.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
3664
|
+
fs14.writeFileSync(CONFIG_FILE, JSON.stringify(cfg2, null, 2), { mode: 384 });
|
|
3380
3665
|
}
|
|
3381
3666
|
function portFree(port2) {
|
|
3382
3667
|
return new Promise((resolve) => {
|
|
@@ -3387,7 +3672,7 @@ function portFree(port2) {
|
|
|
3387
3672
|
}
|
|
3388
3673
|
function listen(token, port2, root2, tunnelToken, preview, status2) {
|
|
3389
3674
|
const hookSecret = ensureHookSecret(CONFIG_DIR);
|
|
3390
|
-
writeHookLauncher(CONFIG_DIR, { script:
|
|
3675
|
+
writeHookLauncher(CONFIG_DIR, { script: fs14.realpathSync(process.argv[1]) });
|
|
3391
3676
|
const server = createAgentServer({
|
|
3392
3677
|
token,
|
|
3393
3678
|
root: root2,
|
|
@@ -3396,7 +3681,7 @@ function listen(token, port2, root2, tunnelToken, preview, status2) {
|
|
|
3396
3681
|
tunnelStatus: status2,
|
|
3397
3682
|
hookSecret,
|
|
3398
3683
|
// This very CLI, so coding agents in the app's terminals report through the same version.
|
|
3399
|
-
hookBin:
|
|
3684
|
+
hookBin: fs14.realpathSync(process.argv[1]),
|
|
3400
3685
|
pushStore: pushStore(PUSH_DIR, port2),
|
|
3401
3686
|
uploadsDir: UPLOADS_DIR
|
|
3402
3687
|
});
|
|
@@ -3409,7 +3694,7 @@ var DEVTUNNEL = "devtunnel";
|
|
|
3409
3694
|
async function resolveDevtunnel() {
|
|
3410
3695
|
let installing = false;
|
|
3411
3696
|
DEVTUNNEL = await ensureDevtunnel({
|
|
3412
|
-
dir:
|
|
3697
|
+
dir: path12.join(CONFIG_DIR, "bin"),
|
|
3413
3698
|
progress: (detail2) => {
|
|
3414
3699
|
if (!installing) {
|
|
3415
3700
|
installing = true;
|
|
@@ -3422,7 +3707,7 @@ async function resolveDevtunnel() {
|
|
|
3422
3707
|
}
|
|
3423
3708
|
async function devtunnel(...args) {
|
|
3424
3709
|
try {
|
|
3425
|
-
const { stdout } = await
|
|
3710
|
+
const { stdout } = await run5(DEVTUNNEL, args);
|
|
3426
3711
|
return { ok: true, out: stdout };
|
|
3427
3712
|
} catch (e) {
|
|
3428
3713
|
return { ok: false, out: `${e.stdout || ""}${e.stderr || ""}` || e.message };
|
|
@@ -3483,9 +3768,23 @@ ${updated.out}`);
|
|
|
3483
3768
|
const name = port2 === DEFAULT_PORT ? os6.hostname() : `${os6.hostname()} :${port2}`;
|
|
3484
3769
|
const createArgs = ["create", "--expiration", "30d", "-d", `coderemote ${name}`, "--labels", MACHINE_LABEL, "--json"];
|
|
3485
3770
|
if (anonymous2) createArgs.push("--allow-anonymous");
|
|
3486
|
-
|
|
3487
|
-
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:
|
|
3488
3786
|
${created.out}`);
|
|
3787
|
+
}
|
|
3489
3788
|
const tunnelId = JSON.parse(created.out.slice(created.out.indexOf("{"))).tunnel?.tunnelId;
|
|
3490
3789
|
if (!tunnelId) throw new Error(`could not parse tunnel id from:
|
|
3491
3790
|
${created.out}`);
|
|
@@ -3578,6 +3877,9 @@ var USAGE = `Usage: coderemote <command> [options]
|
|
|
3578
3877
|
kill <id>... | --all kill terminal sessions (an id prefix is enough)
|
|
3579
3878
|
stop [--all] stop the agent for this folder (or every agent)
|
|
3580
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)
|
|
3581
3883
|
update install the latest coderemote (up says when there's one)
|
|
3582
3884
|
requests [PORT] list the requests that reached a previewed port, oldest first;
|
|
3583
3885
|
--grep TEXT, --failed (errors, 4xx and 5xx), --limit N (default 50)
|
|
@@ -3609,9 +3911,9 @@ var flag = (name) => {
|
|
|
3609
3911
|
};
|
|
3610
3912
|
var anonymous = rest.includes("--allow-anonymous") || rest.includes("--no-auth");
|
|
3611
3913
|
var port = flag("--port") ? parseInt(flag("--port"), 10) : DEFAULT_PORT;
|
|
3612
|
-
var root =
|
|
3914
|
+
var root = path12.resolve(flag("--root") ?? process.cwd());
|
|
3613
3915
|
var cfg = loadConfig();
|
|
3614
|
-
var registry = new AgentRegistry(
|
|
3916
|
+
var registry = new AgentRegistry(path12.join(CONFIG_DIR, "agents"));
|
|
3615
3917
|
async function start({ tunnel }) {
|
|
3616
3918
|
const agents = registry.list();
|
|
3617
3919
|
if (flag("--port")) {
|
|
@@ -3650,6 +3952,7 @@ async function start({ tunnel }) {
|
|
|
3650
3952
|
leftover: cfg.previewTunnels?.[port],
|
|
3651
3953
|
remember: (id) => {
|
|
3652
3954
|
cfg.previewTunnels = { ...cfg.previewTunnels, [port]: id };
|
|
3955
|
+
if (!id) delete cfg.previewTunnels[port];
|
|
3653
3956
|
saveConfig(cfg);
|
|
3654
3957
|
},
|
|
3655
3958
|
mintToken: connectToken
|
|
@@ -3663,7 +3966,7 @@ async function start({ tunnel }) {
|
|
|
3663
3966
|
spinner.fail();
|
|
3664
3967
|
if (e.code !== "EADDRINUSE") throw e;
|
|
3665
3968
|
console.error(`Port ${port} is already in use by another process.`);
|
|
3666
|
-
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`}`);
|
|
3667
3970
|
console.error("Stop that process, or pick another port with --port.");
|
|
3668
3971
|
process.exit(1);
|
|
3669
3972
|
}
|
|
@@ -3693,10 +3996,10 @@ async function start({ tunnel }) {
|
|
|
3693
3996
|
}
|
|
3694
3997
|
spinner.succeed();
|
|
3695
3998
|
registry.register({ port, root, url });
|
|
3696
|
-
const shutdown = () => {
|
|
3999
|
+
const shutdown = async () => {
|
|
3697
4000
|
child?.kill();
|
|
3698
|
-
preview?.
|
|
3699
|
-
server.killAllSessions();
|
|
4001
|
+
await preview?.close();
|
|
4002
|
+
await server.killAllSessions();
|
|
3700
4003
|
server.close();
|
|
3701
4004
|
process.exit(0);
|
|
3702
4005
|
};
|
|
@@ -3712,7 +4015,7 @@ async function start({ tunnel }) {
|
|
|
3712
4015
|
var PLUGIN = "coderemote@coderemote";
|
|
3713
4016
|
async function claudeCli(...args) {
|
|
3714
4017
|
try {
|
|
3715
|
-
return (await
|
|
4018
|
+
return (await execCommand(["claude", ...args], { timeout: 12e4 })).stdout.trim();
|
|
3716
4019
|
} catch (e) {
|
|
3717
4020
|
if (e.code === "ENOENT") return null;
|
|
3718
4021
|
throw new Error((e.stderr || e.stdout || e.message).trim());
|
|
@@ -3753,7 +4056,7 @@ async function setupClaude() {
|
|
|
3753
4056
|
var CODEX_PLUGIN = "coderemote@coderemote";
|
|
3754
4057
|
async function codexCli(...args) {
|
|
3755
4058
|
try {
|
|
3756
|
-
return (await
|
|
4059
|
+
return (await execCommand(["codex", ...args], { timeout: 12e4 })).stdout.trim();
|
|
3757
4060
|
} catch (e) {
|
|
3758
4061
|
if (e.code === "ENOENT") return null;
|
|
3759
4062
|
throw new Error((e.stderr || e.stdout || e.message).trim());
|
|
@@ -3828,7 +4131,7 @@ function currentAgent() {
|
|
|
3828
4131
|
const agents = registry.list();
|
|
3829
4132
|
if (flag("--port")) return agents.find((a) => a.port === port);
|
|
3830
4133
|
const here = process.cwd();
|
|
3831
|
-
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);
|
|
3832
4135
|
return containing[0] ?? (agents.length === 1 ? agents[0] : void 0);
|
|
3833
4136
|
}
|
|
3834
4137
|
async function previewPorts(agent, given) {
|
|
@@ -3958,7 +4261,7 @@ Token: ${cfg.token}`);
|
|
|
3958
4261
|
break;
|
|
3959
4262
|
}
|
|
3960
4263
|
for (const agent of targets) {
|
|
3961
|
-
|
|
4264
|
+
stopAgent(agent.pid);
|
|
3962
4265
|
console.log(`stopped agent for ${agent.root} (pid ${agent.pid})`);
|
|
3963
4266
|
}
|
|
3964
4267
|
break;
|
|
@@ -3968,13 +4271,57 @@ Token: ${cfg.token}`);
|
|
|
3968
4271
|
saveConfig(cfg);
|
|
3969
4272
|
const agents = registry.list();
|
|
3970
4273
|
for (const agent of agents) {
|
|
3971
|
-
|
|
4274
|
+
stopAgent(agent.pid);
|
|
3972
4275
|
console.log(`stopped agent for ${agent.root} (pid ${agent.pid})`);
|
|
3973
4276
|
}
|
|
3974
4277
|
console.log("Token rotated. Re-pair your devices.");
|
|
3975
4278
|
if (agents.length) console.log("Run `coderemote up` again in each folder above; each keeps its tunnel URL.");
|
|
3976
4279
|
break;
|
|
3977
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
|
+
}
|
|
3978
4325
|
case "login": {
|
|
3979
4326
|
await resolveDevtunnel();
|
|
3980
4327
|
process.exit(await interactiveLogin(loginArgs()));
|
|
@@ -3993,16 +4340,16 @@ Token: ${cfg.token}`);
|
|
|
3993
4340
|
console.log(`coderemote ${VERSION} is the latest version.`);
|
|
3994
4341
|
break;
|
|
3995
4342
|
}
|
|
3996
|
-
const [command, args] = installCommand(
|
|
4343
|
+
const [command, args] = installCommand(fs14.realpathSync(process.argv[1]));
|
|
3997
4344
|
console.log(`Updating coderemote ${VERSION} \u2192 ${latest}: ${command} ${args.join(" ")}
|
|
3998
4345
|
`);
|
|
3999
4346
|
const code = await new Promise((resolve) => {
|
|
4000
|
-
|
|
4347
|
+
spawnCommand([command, ...args], { stdio: "inherit" }).on("exit", resolve).on("error", () => resolve(127));
|
|
4001
4348
|
});
|
|
4002
4349
|
if (code !== 0) {
|
|
4003
4350
|
console.error(`
|
|
4004
4351
|
The update failed (${command} exited with ${code}).`);
|
|
4005
|
-
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(" ")}`);
|
|
4006
4353
|
process.exit(code || 1);
|
|
4007
4354
|
}
|
|
4008
4355
|
const running = registry.list();
|