coderemote 0.2.0 → 0.2.1

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,6 +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 rotate-token # revoke every paired device
99
99
  coderemote login # sign in again (--microsoft, or --device-code without a browser)
100
+ coderemote update # install the latest version (`up` says when there's one)
100
101
  coderemote help # all options
101
102
  ```
102
103
 
@@ -3,9 +3,10 @@
3
3
  // bin/coderemote.js
4
4
  import { execFile as execFile5, spawn as spawn3 } from "node:child_process";
5
5
  import { randomBytes } from "node:crypto";
6
- import fs7 from "node:fs";
6
+ import fs8 from "node:fs";
7
7
  import os4 from "node:os";
8
- import path6 from "node:path";
8
+ import path7 from "node:path";
9
+ import { fileURLToPath } from "node:url";
9
10
  import { promisify as promisify3 } from "node:util";
10
11
  import qrcode from "qrcode-terminal";
11
12
 
@@ -1365,7 +1366,7 @@ var PreviewTunnel = class {
1365
1366
  const created = await this.devtunnel(
1366
1367
  "create",
1367
1368
  "--expiration",
1368
- "1d",
1369
+ "30d",
1369
1370
  "-d",
1370
1371
  this.description,
1371
1372
  "--labels",
@@ -1529,6 +1530,130 @@ function tunnelStatus({ devtunnel: devtunnel2, tunnelId, account, anonymous: ano
1529
1530
  };
1530
1531
  }
1531
1532
 
1533
+ // src/update.js
1534
+ import fs7 from "node:fs";
1535
+ import path6 from "node:path";
1536
+ var LATEST_URL = "https://registry.npmjs.org/coderemote/latest";
1537
+ var CHECK_INTERVAL_MS = 24 * 36e5;
1538
+ var SEMVER = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
1539
+ function isNewer(latest, current) {
1540
+ const a = SEMVER.exec(latest ?? "");
1541
+ const b = SEMVER.exec(current ?? "");
1542
+ if (!a || !b) return false;
1543
+ for (let i = 1; i <= 3; i++) {
1544
+ if (+a[i] !== +b[i]) return +a[i] > +b[i];
1545
+ }
1546
+ return !a[4] && !!b[4];
1547
+ }
1548
+ async function fetchLatest({ fetch: fetch2 = globalThis.fetch, timeoutMs = 3e3 } = {}) {
1549
+ try {
1550
+ const res = await fetch2(LATEST_URL, { signal: AbortSignal.timeout(timeoutMs) });
1551
+ if (!res.ok) return null;
1552
+ const { version } = await res.json();
1553
+ return SEMVER.test(version) ? version : null;
1554
+ } catch {
1555
+ return null;
1556
+ }
1557
+ }
1558
+ async function availableUpdate({ current, dir, fetch: fetch2, now = Date.now() }) {
1559
+ const file = path6.join(dir, "update.json");
1560
+ let cache = {};
1561
+ try {
1562
+ cache = JSON.parse(fs7.readFileSync(file, "utf8"));
1563
+ } catch {
1564
+ }
1565
+ let latest = cache.latest;
1566
+ if (!latest || !(now - (cache.checkedAt ?? 0) < CHECK_INTERVAL_MS)) {
1567
+ const fetched = await fetchLatest({ fetch: fetch2 });
1568
+ if (fetched) {
1569
+ latest = fetched;
1570
+ try {
1571
+ fs7.mkdirSync(dir, { recursive: true, mode: 448 });
1572
+ fs7.writeFileSync(file, JSON.stringify({ latest, checkedAt: now }));
1573
+ } catch {
1574
+ }
1575
+ }
1576
+ }
1577
+ return isNewer(latest, current) ? latest : null;
1578
+ }
1579
+ function installCommand(installedAt) {
1580
+ const p = installedAt.split(path6.sep).join("/");
1581
+ if (p.includes("/pnpm/") || p.includes("/.pnpm/")) return ["pnpm", ["add", "-g", "coderemote@latest"]];
1582
+ if (p.includes("/.bun/")) return ["bun", ["add", "-g", "coderemote@latest"]];
1583
+ if (p.includes("/.yarn/") || p.includes("/yarn/global/")) return ["yarn", ["global", "add", "coderemote@latest"]];
1584
+ return ["npm", ["install", "-g", "coderemote@latest"]];
1585
+ }
1586
+
1587
+ // package.json
1588
+ var package_default = {
1589
+ name: "coderemote",
1590
+ version: "0.2.1",
1591
+ 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
+ keywords: [
1593
+ "remote",
1594
+ "remote-terminal",
1595
+ "remote-development",
1596
+ "terminal",
1597
+ "ios",
1598
+ "iphone",
1599
+ "mobile",
1600
+ "dev-tunnels",
1601
+ "devtunnel",
1602
+ "tunnel",
1603
+ "ssh-alternative",
1604
+ "file-explorer",
1605
+ "git",
1606
+ "pty",
1607
+ "cli",
1608
+ "vscode",
1609
+ "code-remote"
1610
+ ],
1611
+ homepage: "https://coderemoteapp.github.io/web/",
1612
+ bugs: {
1613
+ url: "https://github.com/CodeRemoteApp/coderemote/issues"
1614
+ },
1615
+ repository: {
1616
+ type: "git",
1617
+ url: "git+https://github.com/CodeRemoteApp/coderemote.git",
1618
+ directory: "apps/agent"
1619
+ },
1620
+ license: "AGPL-3.0-or-later",
1621
+ author: "Thanh Tran",
1622
+ type: "module",
1623
+ bin: {
1624
+ coderemote: "dist/coderemote.js"
1625
+ },
1626
+ files: [
1627
+ "dist",
1628
+ "scripts/fix-node-pty.js"
1629
+ ],
1630
+ engines: {
1631
+ node: ">=20"
1632
+ },
1633
+ os: [
1634
+ "darwin",
1635
+ "linux"
1636
+ ],
1637
+ scripts: {
1638
+ build: "node scripts/build.js",
1639
+ prepare: "node scripts/build.js",
1640
+ prepack: "node scripts/build.js && cp ../../LICENSE LICENSE",
1641
+ postpack: "rm -f LICENSE",
1642
+ postinstall: "node scripts/fix-node-pty.js",
1643
+ test: "node --test 'test/*.test.js'"
1644
+ },
1645
+ dependencies: {
1646
+ "node-pty": "^1.1.0",
1647
+ "qrcode-terminal": "^0.12.0",
1648
+ ws: "^8.22.0"
1649
+ },
1650
+ devDependencies: {
1651
+ "@vsremote/agent-core": "*",
1652
+ "@vsremote/protocol": "*",
1653
+ esbuild: "^0.25.11"
1654
+ }
1655
+ };
1656
+
1532
1657
  // src/spinner.js
1533
1658
  var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1534
1659
  var dim = (s) => `\x1B[2m${s}\x1B[22m`;
@@ -1650,16 +1775,18 @@ ${log2}`));
1650
1775
 
1651
1776
  // bin/coderemote.js
1652
1777
  var run4 = promisify3(execFile5);
1653
- var CONFIG_DIR = path6.join(os4.homedir(), ".coderemote");
1654
- var LEGACY_CONFIG_DIR = path6.join(os4.homedir(), ".vsremote");
1655
- if (!fs7.existsSync(CONFIG_DIR) && fs7.existsSync(LEGACY_CONFIG_DIR)) fs7.renameSync(LEGACY_CONFIG_DIR, CONFIG_DIR);
1656
- var CONFIG_FILE = path6.join(CONFIG_DIR, "config.json");
1778
+ var CONFIG_DIR = path7.join(os4.homedir(), ".coderemote");
1779
+ var LEGACY_CONFIG_DIR = path7.join(os4.homedir(), ".vsremote");
1780
+ if (!fs8.existsSync(CONFIG_DIR) && fs8.existsSync(LEGACY_CONFIG_DIR)) fs8.renameSync(LEGACY_CONFIG_DIR, CONFIG_DIR);
1781
+ var CONFIG_FILE = path7.join(CONFIG_DIR, "config.json");
1657
1782
  var DEFAULT_PORT = 31580;
1783
+ var VERSION = package_default.version;
1784
+ var PACKAGED = path7.basename(path7.dirname(fileURLToPath(import.meta.url))) === "dist";
1658
1785
  var spinner = new Spinner();
1659
1786
  function loadConfig() {
1660
1787
  let cfg2 = {};
1661
1788
  try {
1662
- cfg2 = JSON.parse(fs7.readFileSync(CONFIG_FILE, "utf8"));
1789
+ cfg2 = JSON.parse(fs8.readFileSync(CONFIG_FILE, "utf8"));
1663
1790
  } catch {
1664
1791
  }
1665
1792
  if (!cfg2.token) {
@@ -1669,8 +1796,8 @@ function loadConfig() {
1669
1796
  return cfg2;
1670
1797
  }
1671
1798
  function saveConfig(cfg2) {
1672
- fs7.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
1673
- fs7.writeFileSync(CONFIG_FILE, JSON.stringify(cfg2, null, 2), { mode: 384 });
1799
+ fs8.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
1800
+ fs8.writeFileSync(CONFIG_FILE, JSON.stringify(cfg2, null, 2), { mode: 384 });
1674
1801
  }
1675
1802
  function listen(token, port2, root2, tunnelToken, preview, status2) {
1676
1803
  const server = createAgentServer({ token, root: root2, tunnelToken, preview, tunnelStatus: status2 });
@@ -1683,7 +1810,7 @@ var DEVTUNNEL = "devtunnel";
1683
1810
  async function resolveDevtunnel() {
1684
1811
  let installing = false;
1685
1812
  DEVTUNNEL = await ensureDevtunnel({
1686
- dir: path6.join(CONFIG_DIR, "bin"),
1813
+ dir: path7.join(CONFIG_DIR, "bin"),
1687
1814
  progress: (detail2) => {
1688
1815
  if (!installing) {
1689
1816
  installing = true;
@@ -1849,6 +1976,8 @@ var USAGE = `Usage: coderemote <command> [options]
1849
1976
  kill <id>... | --all kill terminal sessions (an id prefix is enough)
1850
1977
  stop [--all] stop the agent for this folder (or every agent)
1851
1978
  rotate-token revoke every paired device
1979
+ update install the latest coderemote (up says when there's one)
1980
+ version print this coderemote's version (also --version, -v)
1852
1981
  login sign in to Microsoft Dev Tunnels with GitHub (up does this when needed);
1853
1982
  --microsoft for a Microsoft account, --device-code without a browser
1854
1983
 
@@ -1861,16 +1990,16 @@ Options:
1861
1990
  this folder in VS Code in the browser (needs VS Code's \`code\`)`;
1862
1991
  var HELP_FLAGS = ["help", "--help", "-help", "-h"];
1863
1992
  var argv = process.argv.slice(2);
1864
- var [cmd, ...rest] = argv.length === 0 || argv.some((a) => HELP_FLAGS.includes(a)) ? ["help"] : argv;
1993
+ var [cmd, ...rest] = argv.length === 0 || argv.some((a) => HELP_FLAGS.includes(a)) ? ["help"] : argv.some((a) => a === "--version" || a === "-v") ? ["version"] : argv;
1865
1994
  var flag = (name) => {
1866
1995
  const i = rest.indexOf(name);
1867
1996
  return i >= 0 ? rest[i + 1] : void 0;
1868
1997
  };
1869
1998
  var anonymous = rest.includes("--allow-anonymous") || rest.includes("--no-auth");
1870
1999
  var port = flag("--port") ? parseInt(flag("--port"), 10) : DEFAULT_PORT;
1871
- var root = path6.resolve(flag("--root") ?? process.cwd());
2000
+ var root = path7.resolve(flag("--root") ?? process.cwd());
1872
2001
  var cfg = loadConfig();
1873
- var registry = new AgentRegistry(path6.join(CONFIG_DIR, "agents"));
2002
+ var registry = new AgentRegistry(path7.join(CONFIG_DIR, "agents"));
1874
2003
  async function start({ tunnel }) {
1875
2004
  const running = registry.list().find((a) => a.port === port);
1876
2005
  if (running) {
@@ -1979,6 +2108,7 @@ Token: ${cfg.token}`);
1979
2108
  break;
1980
2109
  }
1981
2110
  case "up": {
2111
+ const update = PACKAGED && !process.env.CODEREMOTE_NO_UPDATE_CHECK ? availableUpdate({ current: VERSION, dir: CONFIG_DIR }).catch(() => null) : null;
1982
2112
  let started;
1983
2113
  try {
1984
2114
  started = await start({ tunnel: true });
@@ -1991,6 +2121,9 @@ Token: ${cfg.token}`);
1991
2121
  console.log(`
1992
2122
  Sharing: ${root}`);
1993
2123
  printPairing(url, cfg.token, access, vscodeUrl);
2124
+ const newer = await update;
2125
+ if (newer) console.log(` Update available: ${VERSION} \u2192 ${newer}. Run \`coderemote update\` to install it.
2126
+ `);
1994
2127
  break;
1995
2128
  }
1996
2129
  case "status":
@@ -2059,6 +2192,44 @@ Token: ${cfg.token}`);
2059
2192
  await resolveDevtunnel();
2060
2193
  process.exit(await interactiveLogin(loginArgs()));
2061
2194
  }
2195
+ case "update": {
2196
+ if (!PACKAGED) {
2197
+ console.log("This coderemote runs from a source checkout: update it with `git pull`.");
2198
+ break;
2199
+ }
2200
+ const latest = await fetchLatest({ timeoutMs: 1e4 });
2201
+ if (!latest) {
2202
+ console.error("Couldn't reach the npm registry. Try again, or run `npm install -g coderemote@latest`.");
2203
+ process.exit(1);
2204
+ }
2205
+ if (!isNewer(latest, VERSION)) {
2206
+ console.log(`coderemote ${VERSION} is the latest version.`);
2207
+ break;
2208
+ }
2209
+ const [command, args] = installCommand(fs8.realpathSync(process.argv[1]));
2210
+ console.log(`Updating coderemote ${VERSION} \u2192 ${latest}: ${command} ${args.join(" ")}
2211
+ `);
2212
+ const code = await new Promise((resolve) => {
2213
+ spawn3(command, args, { stdio: "inherit" }).on("exit", resolve).on("error", () => resolve(127));
2214
+ });
2215
+ if (code !== 0) {
2216
+ console.error(`
2217
+ The update failed (${command} exited with ${code}).`);
2218
+ console.error(`If it was a permission error (EACCES), run it with sudo: sudo ${command} ${args.join(" ")}`);
2219
+ process.exit(code || 1);
2220
+ }
2221
+ const running = registry.list();
2222
+ console.log(`
2223
+ Updated to coderemote ${latest}.`);
2224
+ if (running.length) {
2225
+ console.log(`${running.length === 1 ? "An agent is" : `${running.length} agents are`} still running the previous version.`);
2226
+ console.log("Restart: `coderemote stop --all`, then `coderemote up` in each shared folder.");
2227
+ }
2228
+ break;
2229
+ }
2230
+ case "version":
2231
+ console.log(VERSION);
2232
+ break;
2062
2233
  case "help":
2063
2234
  console.log(USAGE);
2064
2235
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coderemote",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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",