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 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 (20 or newer), and otherwise
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 20** or newer on **macOS** or **Windows 10/11** (Linux is expected to work but not
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
@@ -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 net2 from "node:net";
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.some((p) => p.pid === pid)) throw new HttpError(404, "no listening process with that pid");
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 net from "node:net";
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 = net.connect({ host: "localhost", port: this.target }, () => {
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
- devtunnel2("list", "--labels", MACHINE_LABEL, "--json")
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 others = listed.ok ? (parse(listed.out).tunnels ?? []).filter((t) => t.tunnelId !== id) : [];
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.6",
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: ">=20"
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 = net2.createServer();
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.6",
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": ">=20"
44
+ "node": ">=22"
45
45
  },
46
46
  "scripts": {
47
47
  "build": "node scripts/build.js",