coderemote 0.3.0 → 0.3.2

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
@@ -97,7 +97,7 @@ coderemote stop # stop the agent for this folder (--all: every
97
97
  coderemote serve # share on 127.0.0.1 only, no tunnel (local testing)
98
98
  coderemote requests [PORT] # requests that reached a previewed port (--grep, --failed, --json)
99
99
  coderemote request <ID> # headers and bodies of one request (--show-secrets, --json)
100
- coderemote rotate-token # revoke every paired device
100
+ coderemote rotate-token # revoke every paired device, stop running agents
101
101
  coderemote login # sign in again (--microsoft, or --device-code without a browser)
102
102
  coderemote update # install the latest version (`up` says when there's one)
103
103
  coderemote help # all options
@@ -4,6 +4,7 @@
4
4
  import { execFile as execFile5, spawn as spawn3 } from "node:child_process";
5
5
  import { randomBytes } from "node:crypto";
6
6
  import fs8 from "node:fs";
7
+ import net2 from "node:net";
7
8
  import os4 from "node:os";
8
9
  import path7 from "node:path";
9
10
  import { fileURLToPath } from "node:url";
@@ -434,11 +435,11 @@ async function listeningSockets() {
434
435
  return sockets;
435
436
  }
436
437
  async function processTable() {
437
- const out = await run("ps", ["-A", "-ww", "-o", "pid=,ppid=,command="]);
438
+ const out = await run("ps", ["-A", "-ww", "-o", "pid=,ppid=,tty=,command="]);
438
439
  const table = /* @__PURE__ */ new Map();
439
440
  for (const line of out.split("\n")) {
440
- const m = line.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);
441
- if (m) table.set(parseInt(m[1], 10), { ppid: parseInt(m[2], 10), command: m[3] });
441
+ const m = line.match(/^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/);
442
+ if (m) table.set(parseInt(m[1], 10), { ppid: parseInt(m[2], 10), tty: /^\?+$/.test(m[3]) ? null : m[3], command: m[4] });
442
443
  }
443
444
  return table;
444
445
  }
@@ -476,6 +477,8 @@ async function listPorts(shells = /* @__PURE__ */ new Map()) {
476
477
  command: table.get(s.pid)?.command ?? s.name,
477
478
  cwd: null,
478
479
  session: sessionOf(s.pid, table, shells),
480
+ // Started from a terminal (a dev server) rather than by launchd, an app or a daemon.
481
+ terminal: table.get(s.pid)?.tty != null,
479
482
  self: s.pid === process.pid,
480
483
  agent: s.pid === process.pid || AGENT_COMMAND.test(table.get(s.pid)?.command ?? "")
481
484
  });
@@ -1478,7 +1481,7 @@ function parse(out) {
1478
1481
  return JSON.parse(out.slice(out.indexOf("{")));
1479
1482
  }
1480
1483
  function machineName(description) {
1481
- return (description || "").replace(/^(coderemote|vsremote)\s+/, "").replace(/\.local$/, "") || null;
1484
+ return (description || "").replace(/^(coderemote|vsremote)\s+/, "").replace(/\.local\b/, "") || null;
1482
1485
  }
1483
1486
  function machine(tunnel, current) {
1484
1487
  return {
@@ -1587,7 +1590,7 @@ function installCommand(installedAt) {
1587
1590
  // package.json
1588
1591
  var package_default = {
1589
1592
  name: "coderemote",
1590
- version: "0.3.0",
1593
+ version: "0.3.2",
1591
1594
  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.",
1592
1595
  keywords: [
1593
1596
  "remote",
@@ -1935,12 +1938,23 @@ function loadConfig() {
1935
1938
  cfg2.token = randomBytes(32).toString("base64url");
1936
1939
  saveConfig(cfg2);
1937
1940
  }
1941
+ if (cfg2.tunnelId && !cfg2.tunnels) {
1942
+ cfg2.tunnels = { [DEFAULT_PORT]: cfg2.tunnelId };
1943
+ saveConfig(cfg2);
1944
+ }
1938
1945
  return cfg2;
1939
1946
  }
1940
1947
  function saveConfig(cfg2) {
1941
1948
  fs8.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
1942
1949
  fs8.writeFileSync(CONFIG_FILE, JSON.stringify(cfg2, null, 2), { mode: 384 });
1943
1950
  }
1951
+ function portFree(port2) {
1952
+ return new Promise((resolve) => {
1953
+ const probe = net2.createServer();
1954
+ probe.once("error", () => resolve(false));
1955
+ probe.listen(port2, "127.0.0.1", () => probe.close(() => resolve(true)));
1956
+ });
1957
+ }
1944
1958
  function listen(token, port2, root2, tunnelToken, preview, status2) {
1945
1959
  const server = createAgentServer({ token, root: root2, tunnelToken, preview, tunnelStatus: status2 });
1946
1960
  return new Promise((resolve, reject) => {
@@ -2009,20 +2023,22 @@ function applyAccess(tunnelId, anonymous2) {
2009
2023
  return anonymous2 ? devtunnel("access", "create", tunnelId, "--anonymous") : devtunnel("access", "reset", tunnelId);
2010
2024
  }
2011
2025
  async function ensureTunnel(cfg2, port2, anonymous2) {
2012
- if (cfg2.tunnelId) {
2026
+ const known = cfg2.tunnels?.[port2];
2027
+ if (known) {
2013
2028
  const [updated, access] = await Promise.all([
2014
- devtunnel("update", cfg2.tunnelId, "--expiration", "30d", "--add-labels", MACHINE_LABEL),
2015
- applyAccess(cfg2.tunnelId, anonymous2)
2029
+ devtunnel("update", known, "--expiration", "30d", "--add-labels", MACHINE_LABEL),
2030
+ applyAccess(known, anonymous2)
2016
2031
  ]);
2017
2032
  if (updated.ok) {
2018
2033
  if (!access.ok) throw new Error(`devtunnel access failed:
2019
2034
  ${access.out}`);
2020
- return cfg2.tunnelId;
2035
+ return known;
2021
2036
  }
2022
2037
  if (!/not found/i.test(updated.out)) throw new Error(`devtunnel update failed:
2023
2038
  ${updated.out}`);
2024
2039
  }
2025
- const createArgs = ["create", "--expiration", "30d", "-d", `coderemote ${os4.hostname()}`, "--labels", MACHINE_LABEL, "--json"];
2040
+ const name = port2 === DEFAULT_PORT ? os4.hostname() : `${os4.hostname()} :${port2}`;
2041
+ const createArgs = ["create", "--expiration", "30d", "-d", `coderemote ${name}`, "--labels", MACHINE_LABEL, "--json"];
2026
2042
  if (anonymous2) createArgs.push("--allow-anonymous");
2027
2043
  const created = await devtunnel(...createArgs);
2028
2044
  if (!created.ok) throw new Error(`devtunnel create failed:
@@ -2033,7 +2049,8 @@ ${created.out}`);
2033
2049
  const portRes = await devtunnel("port", "create", tunnelId, "-p", String(port2), "--protocol", "http");
2034
2050
  if (!portRes.ok) throw new Error(`devtunnel port create failed:
2035
2051
  ${portRes.out}`);
2036
- cfg2.tunnelId = tunnelId;
2052
+ cfg2.tunnels = { ...cfg2.tunnels, [port2]: tunnelId };
2053
+ if (port2 === DEFAULT_PORT) cfg2.tunnelId = tunnelId;
2037
2054
  saveConfig(cfg2);
2038
2055
  return tunnelId;
2039
2056
  }
@@ -2117,7 +2134,7 @@ var USAGE = `Usage: coderemote <command> [options]
2117
2134
  status list running agents and their terminal sessions
2118
2135
  kill <id>... | --all kill terminal sessions (an id prefix is enough)
2119
2136
  stop [--all] stop the agent for this folder (or every agent)
2120
- rotate-token revoke every paired device
2137
+ rotate-token revoke every paired device and stop running agents
2121
2138
  update install the latest coderemote (up says when there's one)
2122
2139
  requests [PORT] list the requests that reached a previewed port, oldest first;
2123
2140
  --grep TEXT, --failed (errors, 4xx and 5xx), --limit N (default 50)
@@ -2131,7 +2148,8 @@ var USAGE = `Usage: coderemote <command> [options]
2131
2148
 
2132
2149
  Options:
2133
2150
  --root DIR folder to share (default: current folder)
2134
- --port N local port (default: ${DEFAULT_PORT})
2151
+ --port N local port (default: the first free one from ${DEFAULT_PORT}; each port
2152
+ keeps its own tunnel URL, so a folder pairs once per port)
2135
2153
  --allow-anonymous up: skip the sign-in, the pairing token alone grants access
2136
2154
  (alias --no-auth)
2137
2155
  --web up: also run \`code tunnel\` and print a vscode.dev link that opens
@@ -2149,11 +2167,32 @@ var root = path7.resolve(flag("--root") ?? process.cwd());
2149
2167
  var cfg = loadConfig();
2150
2168
  var registry = new AgentRegistry(path7.join(CONFIG_DIR, "agents"));
2151
2169
  async function start({ tunnel }) {
2152
- const running = registry.list().find((a) => a.port === port);
2153
- if (running) {
2154
- console.error(`An agent already runs on port ${port} for ${running.root} (pid ${running.pid}).`);
2155
- console.error("Stop it with `coderemote stop --all`, or pick another port with --port.");
2156
- process.exit(1);
2170
+ const agents = registry.list();
2171
+ if (flag("--port")) {
2172
+ const running = agents.find((a) => a.port === port);
2173
+ if (running) {
2174
+ console.error(`An agent already runs on port ${port} for ${running.root} (pid ${running.pid}).`);
2175
+ console.error("Stop it with `coderemote stop --all`, or pick another port with --port.");
2176
+ process.exit(1);
2177
+ }
2178
+ } else {
2179
+ const sharing = agents.find((a) => a.root === root);
2180
+ if (sharing) {
2181
+ console.error(`${root} is already shared on port ${sharing.port} (pid ${sharing.pid}).`);
2182
+ console.error("Stop it with `coderemote stop`, or start another one with --port.");
2183
+ process.exit(1);
2184
+ }
2185
+ const taken = async (p) => agents.some((a) => a.port === p) || !await portFree(p);
2186
+ const remembered = cfg.folderPorts?.[root];
2187
+ if (remembered && !await taken(remembered)) port = remembered;
2188
+ else {
2189
+ const claimed = new Set(Object.values(cfg.folderPorts ?? {}));
2190
+ while (claimed.has(port) || await taken(port)) port++;
2191
+ }
2192
+ if (port !== remembered) {
2193
+ cfg.folderPorts = { ...cfg.folderPorts, [root]: port };
2194
+ saveConfig(cfg);
2195
+ }
2157
2196
  }
2158
2197
  let server;
2159
2198
  let tunnelId;
@@ -2193,7 +2232,7 @@ async function start({ tunnel }) {
2193
2232
  account = user;
2194
2233
  spinner.succeed();
2195
2234
  spinner.start("Connecting to the relay");
2196
- const known = cfg.tunnelId;
2235
+ const known = cfg.tunnels?.[port];
2197
2236
  let hosting = known ? hostTunnel(known, port) : null;
2198
2237
  hosting?.catch(() => {
2199
2238
  });
@@ -2356,7 +2395,13 @@ Token: ${cfg.token}`);
2356
2395
  case "rotate-token": {
2357
2396
  cfg.token = randomBytes(32).toString("base64url");
2358
2397
  saveConfig(cfg);
2359
- console.log("Token rotated. Re-pair your devices, and restart running agents.");
2398
+ const agents = registry.list();
2399
+ for (const agent of agents) {
2400
+ process.kill(agent.pid, "SIGTERM");
2401
+ console.log(`stopped agent for ${agent.root} (pid ${agent.pid})`);
2402
+ }
2403
+ console.log("Token rotated. Re-pair your devices.");
2404
+ if (agents.length) console.log("Run `coderemote up` again in each folder above; each keeps its tunnel URL.");
2360
2405
  break;
2361
2406
  }
2362
2407
  case "login": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coderemote",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
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",