coderemote 0.3.6 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/coderemote.js +68 -13
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ cd ~/projects/my-app && coderemote up
|
|
|
45
45
|
|
|
46
46
|
With Homebrew: `brew install coderemoteapp/tap/coderemote`.
|
|
47
47
|
|
|
48
|
-
No Node.js? The install scripts use yours when there is one (
|
|
48
|
+
No Node.js? The install scripts use yours when there is one (22 or newer), and otherwise
|
|
49
49
|
download Node.js into `~/.coderemote/runtime` for coderemote alone:
|
|
50
50
|
|
|
51
51
|
```sh
|
|
@@ -143,7 +143,7 @@ example to reuse a session token in a test.
|
|
|
143
143
|
|
|
144
144
|
## Requirements
|
|
145
145
|
|
|
146
|
-
- **Node.js
|
|
146
|
+
- **Node.js 22** or newer on **macOS** or **Windows 10/11** (Linux is expected to work but not
|
|
147
147
|
yet tested); the install scripts download it when it's missing. On Windows, terminals run
|
|
148
148
|
PowerShell (`pwsh` when installed).
|
|
149
149
|
- A **GitHub or Microsoft account** to sign in to Dev Tunnels
|
package/dist/coderemote.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { execFile as execFile8, execFileSync, spawn as spawn3 } from "node:child_process";
|
|
5
5
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
6
6
|
import fs14 from "node:fs";
|
|
7
|
-
import
|
|
7
|
+
import net3 from "node:net";
|
|
8
8
|
import os6 from "node:os";
|
|
9
9
|
import path12 from "node:path";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
@@ -532,6 +532,7 @@ var AgentHub = class extends EventEmitter {
|
|
|
532
532
|
}
|
|
533
533
|
switch (event.hook_event_name) {
|
|
534
534
|
case "SessionStart":
|
|
535
|
+
if (s.state === "running" && s.turnStartedAt == null) s.state = "idle";
|
|
535
536
|
break;
|
|
536
537
|
case "UserPromptSubmit": {
|
|
537
538
|
const text = typeof event.prompt === "string" ? event.prompt : "";
|
|
@@ -1053,6 +1054,7 @@ var LANDING_HTML = `<!doctype html>
|
|
|
1053
1054
|
// ../../packages/agent-core/src/ports.js
|
|
1054
1055
|
import { execFile as execFile3 } from "node:child_process";
|
|
1055
1056
|
import fs6 from "node:fs/promises";
|
|
1057
|
+
import net from "node:net";
|
|
1056
1058
|
import os from "node:os";
|
|
1057
1059
|
|
|
1058
1060
|
// ../../packages/agent-core/src/platform.js
|
|
@@ -1278,14 +1280,27 @@ function sessionOf(pid, table, shells) {
|
|
|
1278
1280
|
async function killListener(pid, { force = false, shells } = {}) {
|
|
1279
1281
|
if (!Number.isInteger(pid) || pid <= 1) throw new HttpError(400, "pid must be a process id");
|
|
1280
1282
|
if (pid === process.pid) throw new HttpError(403, "the agent cannot stop itself");
|
|
1281
|
-
const ports = await listPorts(shells);
|
|
1282
|
-
if (!ports.
|
|
1283
|
+
const ports = (await listPorts(shells)).filter((p) => p.pid === pid).map((p) => p.port);
|
|
1284
|
+
if (!ports.length) throw new HttpError(404, "no listening process with that pid");
|
|
1283
1285
|
try {
|
|
1284
1286
|
process.kill(pid, force ? "SIGKILL" : "SIGTERM");
|
|
1285
1287
|
} catch (e) {
|
|
1286
1288
|
if (e.code === "ESRCH") throw new HttpError(404, "no listening process with that pid");
|
|
1287
1289
|
throw e;
|
|
1288
1290
|
}
|
|
1291
|
+
return ports;
|
|
1292
|
+
}
|
|
1293
|
+
function isListening(port2, timeout = 2e3) {
|
|
1294
|
+
return new Promise((resolve) => {
|
|
1295
|
+
const socket = net.connect({ host: "localhost", port: port2, timeout });
|
|
1296
|
+
const done = (up) => {
|
|
1297
|
+
socket.destroy();
|
|
1298
|
+
resolve(up);
|
|
1299
|
+
};
|
|
1300
|
+
socket.once("connect", () => done(true));
|
|
1301
|
+
socket.once("timeout", () => done(true));
|
|
1302
|
+
socket.once("error", () => done(false));
|
|
1303
|
+
});
|
|
1289
1304
|
}
|
|
1290
1305
|
|
|
1291
1306
|
// ../../packages/agent-core/src/push.js
|
|
@@ -1507,7 +1522,7 @@ var PushSender = class {
|
|
|
1507
1522
|
|
|
1508
1523
|
// ../../packages/agent-core/src/inspector.js
|
|
1509
1524
|
import http from "node:http";
|
|
1510
|
-
import
|
|
1525
|
+
import net2 from "node:net";
|
|
1511
1526
|
import zlib from "node:zlib";
|
|
1512
1527
|
var MAX_ENTRIES2 = 200;
|
|
1513
1528
|
var MAX_BODY = 64 * 1024;
|
|
@@ -1641,7 +1656,7 @@ var InspectingProxy = class {
|
|
|
1641
1656
|
/** WebSockets (hot reload) pass through as raw bytes; only the handshake is recorded. */
|
|
1642
1657
|
#tunnelUpgrade(req, socket, head) {
|
|
1643
1658
|
const entry2 = this.#record(req);
|
|
1644
|
-
const upstream =
|
|
1659
|
+
const upstream = net2.connect({ host: "localhost", port: this.target }, () => {
|
|
1645
1660
|
const headers = this.#headers(req);
|
|
1646
1661
|
const lines = [`${req.method} ${req.url} HTTP/1.1`];
|
|
1647
1662
|
for (const [k, v] of Object.entries(headers)) {
|
|
@@ -1990,7 +2005,8 @@ function createAgentServer({
|
|
|
1990
2005
|
pluginInstalled = null,
|
|
1991
2006
|
agents = new AgentHub(),
|
|
1992
2007
|
pushStore: pushStore2 = null,
|
|
1993
|
-
pushOptions = {}
|
|
2008
|
+
pushOptions = {},
|
|
2009
|
+
previewSweep = {}
|
|
1994
2010
|
}) {
|
|
1995
2011
|
const handoff = new TunnelHandoff({ token, mint: tunnelToken });
|
|
1996
2012
|
const sandbox = new Sandbox(root2);
|
|
@@ -2140,6 +2156,36 @@ function createAgentServer({
|
|
|
2140
2156
|
ports.sort((a, b) => a.port - b.port);
|
|
2141
2157
|
return { ports, token: issued?.token ?? null, expiresAt: issued?.expiresAt ?? null };
|
|
2142
2158
|
}
|
|
2159
|
+
const { checkMs = 5e3, graceMs = 3e4, killedMs = 1500 } = previewSweep;
|
|
2160
|
+
const silentSince = /* @__PURE__ */ new Map();
|
|
2161
|
+
let sweeping = false;
|
|
2162
|
+
async function sweepPreviews(ports = [...proxies.keys()], now = false) {
|
|
2163
|
+
const forwarded = new Set(preview.list().map((p) => p.port));
|
|
2164
|
+
for (const port2 of ports) {
|
|
2165
|
+
const proxy = proxies.get(port2);
|
|
2166
|
+
if (!proxy || !forwarded.has(proxy.port) || await isListening(port2)) {
|
|
2167
|
+
silentSince.delete(port2);
|
|
2168
|
+
continue;
|
|
2169
|
+
}
|
|
2170
|
+
if (!silentSince.has(port2)) silentSince.set(port2, Date.now());
|
|
2171
|
+
if (!now && Date.now() - silentSince.get(port2) < graceMs) continue;
|
|
2172
|
+
silentSince.delete(port2);
|
|
2173
|
+
try {
|
|
2174
|
+
await preview.unforward(proxy.port);
|
|
2175
|
+
} catch {
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
const previewTimer = preview && setInterval(async () => {
|
|
2180
|
+
if (sweeping || !proxies.size) return;
|
|
2181
|
+
sweeping = true;
|
|
2182
|
+
try {
|
|
2183
|
+
await sweepPreviews();
|
|
2184
|
+
} finally {
|
|
2185
|
+
sweeping = false;
|
|
2186
|
+
}
|
|
2187
|
+
}, checkMs);
|
|
2188
|
+
previewTimer?.unref();
|
|
2143
2189
|
function shellPids() {
|
|
2144
2190
|
return new Map(sessions.list().map((s) => [s.pid, s.id]));
|
|
2145
2191
|
}
|
|
@@ -2259,7 +2305,9 @@ function createAgentServer({
|
|
|
2259
2305
|
[Routes.portsList]: async () => ({ ports: await listPorts(shellPids()) }),
|
|
2260
2306
|
[Routes.portsKill]: async (url, req) => {
|
|
2261
2307
|
const { pid, force } = await readJson(req);
|
|
2262
|
-
await killListener(pid, { force: force === true, shells: shellPids() });
|
|
2308
|
+
const ports = await killListener(pid, { force: force === true, shells: shellPids() });
|
|
2309
|
+
if (preview) setTimeout(() => sweepPreviews(ports, true).catch(() => {
|
|
2310
|
+
}), killedMs).unref();
|
|
2263
2311
|
return {};
|
|
2264
2312
|
},
|
|
2265
2313
|
[Routes.previewList]: async () => {
|
|
@@ -2596,6 +2644,7 @@ function createAgentServer({
|
|
|
2596
2644
|
}
|
|
2597
2645
|
server.on("close", () => {
|
|
2598
2646
|
clearInterval(sweep);
|
|
2647
|
+
clearInterval(previewTimer);
|
|
2599
2648
|
sessions.killAll();
|
|
2600
2649
|
for (const proxy of proxies.values()) proxy.close();
|
|
2601
2650
|
});
|
|
@@ -3179,6 +3228,7 @@ var AgentRegistry = class {
|
|
|
3179
3228
|
|
|
3180
3229
|
// src/tunnel-status.js
|
|
3181
3230
|
var MACHINE_LABEL = "coderemote";
|
|
3231
|
+
var PROVIDER = "devtunnels";
|
|
3182
3232
|
var CACHE_MS = 15e3;
|
|
3183
3233
|
function parse(out) {
|
|
3184
3234
|
return JSON.parse(out.slice(out.indexOf("{")));
|
|
@@ -3205,11 +3255,13 @@ function tunnelStatus({ devtunnel: devtunnel2, tunnelId, account, anonymous: ano
|
|
|
3205
3255
|
if (cached && Date.now() - cached.at < CACHE_MS) return { ...cached.value, previewPorts: preview?.list().length ?? 0 };
|
|
3206
3256
|
const [own, listed] = await Promise.all([
|
|
3207
3257
|
devtunnel2("show", id, "--json"),
|
|
3208
|
-
|
|
3258
|
+
// Every tunnel, previews' too: they all count against the account's limit.
|
|
3259
|
+
devtunnel2("list", "--limit", "100", "--json")
|
|
3209
3260
|
]);
|
|
3210
3261
|
if (!own.ok) throw new Error(`devtunnel show failed: ${own.out.trim()}`);
|
|
3211
3262
|
const tunnel = parse(own.out).tunnel;
|
|
3212
|
-
const
|
|
3263
|
+
const all = listed.ok ? parse(listed.out).tunnels ?? [] : null;
|
|
3264
|
+
const others = (all ?? []).filter((t) => t.tunnelId !== id && t.labels?.includes(MACHINE_LABEL));
|
|
3213
3265
|
const shown = await Promise.all(
|
|
3214
3266
|
others.map(async (t) => {
|
|
3215
3267
|
const res = await devtunnel2("show", t.tunnelId, "--json");
|
|
@@ -3218,6 +3270,7 @@ function tunnelStatus({ devtunnel: devtunnel2, tunnelId, account, anonymous: ano
|
|
|
3218
3270
|
);
|
|
3219
3271
|
const machines = [machine(tunnel, true), ...shown.filter(Boolean).map((t) => machine(t, false))];
|
|
3220
3272
|
const value = {
|
|
3273
|
+
provider: PROVIDER,
|
|
3221
3274
|
account: account() ?? null,
|
|
3222
3275
|
tunnel: {
|
|
3223
3276
|
id,
|
|
@@ -3229,7 +3282,9 @@ function tunnelStatus({ devtunnel: devtunnel2, tunnelId, account, anonymous: ano
|
|
|
3229
3282
|
uploaded: tunnel.uploadTotal ?? null,
|
|
3230
3283
|
downloaded: tunnel.downloadTotal ?? null
|
|
3231
3284
|
},
|
|
3232
|
-
machines
|
|
3285
|
+
machines,
|
|
3286
|
+
// null when the account couldn't be listed
|
|
3287
|
+
quota: all && { used: all.length, limit: TUNNEL_LIMIT }
|
|
3233
3288
|
};
|
|
3234
3289
|
cached = { at: Date.now(), value };
|
|
3235
3290
|
return { ...value, previewPorts: preview?.list().length ?? 0 };
|
|
@@ -3300,7 +3355,7 @@ function installCommand(installedAt) {
|
|
|
3300
3355
|
// package.json
|
|
3301
3356
|
var package_default = {
|
|
3302
3357
|
name: "coderemote",
|
|
3303
|
-
version: "0.3.
|
|
3358
|
+
version: "0.3.8",
|
|
3304
3359
|
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.",
|
|
3305
3360
|
keywords: [
|
|
3306
3361
|
"remote",
|
|
@@ -3341,7 +3396,7 @@ var package_default = {
|
|
|
3341
3396
|
"scripts/fix-node-pty.js"
|
|
3342
3397
|
],
|
|
3343
3398
|
engines: {
|
|
3344
|
-
node: ">=
|
|
3399
|
+
node: ">=22"
|
|
3345
3400
|
},
|
|
3346
3401
|
scripts: {
|
|
3347
3402
|
build: "node scripts/build.js",
|
|
@@ -3665,7 +3720,7 @@ function saveConfig(cfg2) {
|
|
|
3665
3720
|
}
|
|
3666
3721
|
function portFree(port2) {
|
|
3667
3722
|
return new Promise((resolve) => {
|
|
3668
|
-
const probe =
|
|
3723
|
+
const probe = net3.createServer();
|
|
3669
3724
|
probe.once("error", () => resolve(false));
|
|
3670
3725
|
probe.listen(port2, "127.0.0.1", () => probe.close(() => resolve(true)));
|
|
3671
3726
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coderemote",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"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.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"remote",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"scripts/fix-node-pty.js"
|
|
42
42
|
],
|
|
43
43
|
"engines": {
|
|
44
|
-
"node": ">=
|
|
44
|
+
"node": ">=22"
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"build": "node scripts/build.js",
|