coderemote 0.2.0 → 0.3.0
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 +9 -0
- package/dist/coderemote.js +429 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -95,14 +95,23 @@ coderemote kill <id>... # end terminal sessions (an id prefix from `sta
|
|
|
95
95
|
coderemote kill --all # end every session
|
|
96
96
|
coderemote stop # stop the agent for this folder (--all: every agent)
|
|
97
97
|
coderemote serve # share on 127.0.0.1 only, no tunnel (local testing)
|
|
98
|
+
coderemote requests [PORT] # requests that reached a previewed port (--grep, --failed, --json)
|
|
99
|
+
coderemote request <ID> # headers and bodies of one request (--show-secrets, --json)
|
|
98
100
|
coderemote rotate-token # revoke every paired device
|
|
99
101
|
coderemote login # sign in again (--microsoft, or --device-code without a browser)
|
|
102
|
+
coderemote update # install the latest version (`up` says when there's one)
|
|
100
103
|
coderemote help # all options
|
|
101
104
|
```
|
|
102
105
|
|
|
103
106
|
Options: `--root DIR` shares another folder; `--port N` picks the local port (default
|
|
104
107
|
`31580`). The tunnel URL stays the same between runs, so paired phones reconnect on their own.
|
|
105
108
|
|
|
109
|
+
`requests` and `request` read the traffic log of a port you preview from the app, so a coding
|
|
110
|
+
agent in a terminal (Claude Code, Codex, …) can debug with it: run them in the shared folder.
|
|
111
|
+
The log lives in the agent's memory only (the latest 200 requests per port, bodies up to 64 KB).
|
|
112
|
+
Auth headers, cookies and token-like fields are masked; `--show-secrets` prints them, for
|
|
113
|
+
example to reuse a session token in a test.
|
|
114
|
+
|
|
106
115
|
## Requirements
|
|
107
116
|
|
|
108
117
|
- **Node.js 20** or newer on **macOS** (Linux is expected to work but not yet tested)
|
package/dist/coderemote.js
CHANGED
|
@@ -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
|
|
6
|
+
import fs8 from "node:fs";
|
|
7
7
|
import os4 from "node:os";
|
|
8
|
-
import
|
|
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
|
-
"
|
|
1369
|
+
"30d",
|
|
1369
1370
|
"-d",
|
|
1370
1371
|
this.description,
|
|
1371
1372
|
"--labels",
|
|
@@ -1529,6 +1530,272 @@ 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.3.0",
|
|
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
|
+
|
|
1657
|
+
// src/requests.js
|
|
1658
|
+
var MASK = "[redacted]";
|
|
1659
|
+
var SECRET_HEADER = /^(authorization|proxy-authorization|cookie|set-cookie)$|token|secret|api[-_]?key|session|signature|csrf|xsrf/i;
|
|
1660
|
+
var SECRET_FIELD = /pass(word|wd)?$|secret|token|api[-_]?key|authorization|cookie|session|credential|private[-_]?key|^otp$|^code$|signature/i;
|
|
1661
|
+
var Redactor = class {
|
|
1662
|
+
hidden = 0;
|
|
1663
|
+
constructor({ show: show2 = false } = {}) {
|
|
1664
|
+
this.show = show2;
|
|
1665
|
+
}
|
|
1666
|
+
#mask() {
|
|
1667
|
+
this.hidden++;
|
|
1668
|
+
return MASK;
|
|
1669
|
+
}
|
|
1670
|
+
/** Query parameters with secret-looking names, e.g. ?token=…. */
|
|
1671
|
+
path(value) {
|
|
1672
|
+
if (this.show) return value;
|
|
1673
|
+
const q = value.indexOf("?");
|
|
1674
|
+
if (q < 0) return value;
|
|
1675
|
+
const params = value.slice(q + 1).split("&").map((pair) => {
|
|
1676
|
+
const eq = pair.indexOf("=");
|
|
1677
|
+
if (eq < 0 || !SECRET_FIELD.test(decodeSafe(pair.slice(0, eq)))) return pair;
|
|
1678
|
+
return `${pair.slice(0, eq)}=${this.#mask()}`;
|
|
1679
|
+
});
|
|
1680
|
+
return `${value.slice(0, q)}?${params.join("&")}`;
|
|
1681
|
+
}
|
|
1682
|
+
header(name, value) {
|
|
1683
|
+
if (this.show || !SECRET_HEADER.test(name)) return value;
|
|
1684
|
+
const lower = name.toLowerCase();
|
|
1685
|
+
if (lower === "authorization" || lower === "proxy-authorization") {
|
|
1686
|
+
const space = value.indexOf(" ");
|
|
1687
|
+
return space > 0 ? `${value.slice(0, space)} ${this.#mask()}` : this.#mask();
|
|
1688
|
+
}
|
|
1689
|
+
if (lower === "cookie") return value.split(/;\s*/).map((c) => this.#cookie(c)).join("; ");
|
|
1690
|
+
if (lower === "set-cookie") {
|
|
1691
|
+
const [first, ...attributes] = value.split(/;\s*/);
|
|
1692
|
+
return [this.#cookie(first), ...attributes].join("; ");
|
|
1693
|
+
}
|
|
1694
|
+
return this.#mask();
|
|
1695
|
+
}
|
|
1696
|
+
#cookie(pair) {
|
|
1697
|
+
const eq = pair.indexOf("=");
|
|
1698
|
+
return eq < 0 ? pair : `${pair.slice(0, eq)}=${this.#mask()}`;
|
|
1699
|
+
}
|
|
1700
|
+
/** JSON and form bodies get secret fields masked; other text is left as sent. */
|
|
1701
|
+
body(text, contentType = "", { pretty = true } = {}) {
|
|
1702
|
+
if (/json/i.test(contentType) || /^\s*[[{]/.test(text)) {
|
|
1703
|
+
try {
|
|
1704
|
+
const value = JSON.parse(text);
|
|
1705
|
+
return JSON.stringify(this.show ? value : this.#json(value), null, pretty ? 2 : void 0);
|
|
1706
|
+
} catch {
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
if (!this.show && /x-www-form-urlencoded/i.test(contentType)) return this.path(`?${text}`).slice(1);
|
|
1710
|
+
return text;
|
|
1711
|
+
}
|
|
1712
|
+
#json(value) {
|
|
1713
|
+
if (Array.isArray(value)) return value.map((v) => this.#json(v));
|
|
1714
|
+
if (!value || typeof value !== "object") return value;
|
|
1715
|
+
return Object.fromEntries(
|
|
1716
|
+
Object.entries(value).map(([k, v]) => [
|
|
1717
|
+
k,
|
|
1718
|
+
SECRET_FIELD.test(k) && v != null && typeof v !== "object" && typeof v !== "boolean" ? this.#mask() : this.#json(v)
|
|
1719
|
+
])
|
|
1720
|
+
);
|
|
1721
|
+
}
|
|
1722
|
+
};
|
|
1723
|
+
function decodeSafe(s) {
|
|
1724
|
+
try {
|
|
1725
|
+
return decodeURIComponent(s);
|
|
1726
|
+
} catch {
|
|
1727
|
+
return s;
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
function filterRequests(requests, { grep, failed } = {}) {
|
|
1731
|
+
const needle = grep?.toLowerCase();
|
|
1732
|
+
return requests.filter(
|
|
1733
|
+
(r) => (!needle || `${r.method} ${r.path}`.toLowerCase().includes(needle)) && (!failed || r.error || (r.status ?? 0) >= 400)
|
|
1734
|
+
);
|
|
1735
|
+
}
|
|
1736
|
+
function time(ms) {
|
|
1737
|
+
return new Date(ms).toTimeString().slice(0, 8);
|
|
1738
|
+
}
|
|
1739
|
+
function bytes(n) {
|
|
1740
|
+
if (n < 1024) return `${n} B`;
|
|
1741
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
1742
|
+
return `${(n / 1024 / 1024).toFixed(1)} MB`;
|
|
1743
|
+
}
|
|
1744
|
+
function formatList(requests, redactor, { showPort = false } = {}) {
|
|
1745
|
+
return requests.map((r) => {
|
|
1746
|
+
const status2 = r.error ? "ERR" : r.status == null ? "..." : String(r.status);
|
|
1747
|
+
const parts = [
|
|
1748
|
+
`#${r.id}`.padEnd(6),
|
|
1749
|
+
status2.padEnd(4),
|
|
1750
|
+
`${r.method} ${redactor.path(r.path)}`,
|
|
1751
|
+
r.duration == null ? "pending" : `${r.duration} ms`,
|
|
1752
|
+
bytes(r.responseSize),
|
|
1753
|
+
time(r.time)
|
|
1754
|
+
];
|
|
1755
|
+
if (showPort) parts.push(`port ${r.port}`);
|
|
1756
|
+
if (r.error) parts.push(r.error);
|
|
1757
|
+
return parts.join(" ");
|
|
1758
|
+
}).join("\n");
|
|
1759
|
+
}
|
|
1760
|
+
function formatDetail(d, redactor) {
|
|
1761
|
+
const status2 = d.error ? `error: ${d.error}` : d.status == null ? "no response yet" : String(d.status);
|
|
1762
|
+
const lines = [
|
|
1763
|
+
`#${d.id} ${d.method} ${redactor.path(d.path)} -> ${status2}`,
|
|
1764
|
+
`${new Date(d.time).toISOString()}${d.duration == null ? "" : ` ${d.duration} ms`}`
|
|
1765
|
+
];
|
|
1766
|
+
for (const [title, message] of [
|
|
1767
|
+
["Request", d.request],
|
|
1768
|
+
["Response", d.response]
|
|
1769
|
+
]) {
|
|
1770
|
+
lines.push("", `${title} headers:`);
|
|
1771
|
+
for (const name of Object.keys(message.headers).sort()) {
|
|
1772
|
+
const values = [].concat(message.headers[name]);
|
|
1773
|
+
for (const value of values) lines.push(` ${name}: ${redactor.header(name, String(value))}`);
|
|
1774
|
+
}
|
|
1775
|
+
lines.push("", `${title} body: ${describeBody(message.body, redactor, message.headers["content-type"])}`);
|
|
1776
|
+
}
|
|
1777
|
+
return lines.join("\n");
|
|
1778
|
+
}
|
|
1779
|
+
function describeBody(body, redactor, contentType) {
|
|
1780
|
+
if (!body.size) return "(empty)";
|
|
1781
|
+
if (body.truncated) return `${bytes(body.size)}, too large to keep (only bodies up to 64 KB are recorded)`;
|
|
1782
|
+
if (body.text == null) return `${bytes(body.size)} of binary data`;
|
|
1783
|
+
return `${bytes(body.size)}
|
|
1784
|
+
${indent(redactor.body(body.text, contentType))}`;
|
|
1785
|
+
}
|
|
1786
|
+
function indent(text) {
|
|
1787
|
+
return text.replace(/^/gm, " ");
|
|
1788
|
+
}
|
|
1789
|
+
function redactDetail(d, redactor) {
|
|
1790
|
+
const message = (m) => ({
|
|
1791
|
+
headers: Object.fromEntries(
|
|
1792
|
+
Object.entries(m.headers).map(([k, v]) => [k, Array.isArray(v) ? v.map((x) => redactor.header(k, String(x))) : redactor.header(k, String(v))])
|
|
1793
|
+
),
|
|
1794
|
+
body: m.body.text == null ? m.body : { ...m.body, text: redactor.body(m.body.text, m.headers["content-type"], { pretty: false }) }
|
|
1795
|
+
});
|
|
1796
|
+
return { ...d, path: redactor.path(d.path), request: message(d.request), response: message(d.response) };
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1532
1799
|
// src/spinner.js
|
|
1533
1800
|
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1534
1801
|
var dim = (s) => `\x1B[2m${s}\x1B[22m`;
|
|
@@ -1650,16 +1917,18 @@ ${log2}`));
|
|
|
1650
1917
|
|
|
1651
1918
|
// bin/coderemote.js
|
|
1652
1919
|
var run4 = promisify3(execFile5);
|
|
1653
|
-
var CONFIG_DIR =
|
|
1654
|
-
var LEGACY_CONFIG_DIR =
|
|
1655
|
-
if (!
|
|
1656
|
-
var CONFIG_FILE =
|
|
1920
|
+
var CONFIG_DIR = path7.join(os4.homedir(), ".coderemote");
|
|
1921
|
+
var LEGACY_CONFIG_DIR = path7.join(os4.homedir(), ".vsremote");
|
|
1922
|
+
if (!fs8.existsSync(CONFIG_DIR) && fs8.existsSync(LEGACY_CONFIG_DIR)) fs8.renameSync(LEGACY_CONFIG_DIR, CONFIG_DIR);
|
|
1923
|
+
var CONFIG_FILE = path7.join(CONFIG_DIR, "config.json");
|
|
1657
1924
|
var DEFAULT_PORT = 31580;
|
|
1925
|
+
var VERSION = package_default.version;
|
|
1926
|
+
var PACKAGED = path7.basename(path7.dirname(fileURLToPath(import.meta.url))) === "dist";
|
|
1658
1927
|
var spinner = new Spinner();
|
|
1659
1928
|
function loadConfig() {
|
|
1660
1929
|
let cfg2 = {};
|
|
1661
1930
|
try {
|
|
1662
|
-
cfg2 = JSON.parse(
|
|
1931
|
+
cfg2 = JSON.parse(fs8.readFileSync(CONFIG_FILE, "utf8"));
|
|
1663
1932
|
} catch {
|
|
1664
1933
|
}
|
|
1665
1934
|
if (!cfg2.token) {
|
|
@@ -1669,8 +1938,8 @@ function loadConfig() {
|
|
|
1669
1938
|
return cfg2;
|
|
1670
1939
|
}
|
|
1671
1940
|
function saveConfig(cfg2) {
|
|
1672
|
-
|
|
1673
|
-
|
|
1941
|
+
fs8.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
1942
|
+
fs8.writeFileSync(CONFIG_FILE, JSON.stringify(cfg2, null, 2), { mode: 384 });
|
|
1674
1943
|
}
|
|
1675
1944
|
function listen(token, port2, root2, tunnelToken, preview, status2) {
|
|
1676
1945
|
const server = createAgentServer({ token, root: root2, tunnelToken, preview, tunnelStatus: status2 });
|
|
@@ -1683,7 +1952,7 @@ var DEVTUNNEL = "devtunnel";
|
|
|
1683
1952
|
async function resolveDevtunnel() {
|
|
1684
1953
|
let installing = false;
|
|
1685
1954
|
DEVTUNNEL = await ensureDevtunnel({
|
|
1686
|
-
dir:
|
|
1955
|
+
dir: path7.join(CONFIG_DIR, "bin"),
|
|
1687
1956
|
progress: (detail2) => {
|
|
1688
1957
|
if (!installing) {
|
|
1689
1958
|
installing = true;
|
|
@@ -1849,6 +2118,14 @@ var USAGE = `Usage: coderemote <command> [options]
|
|
|
1849
2118
|
kill <id>... | --all kill terminal sessions (an id prefix is enough)
|
|
1850
2119
|
stop [--all] stop the agent for this folder (or every agent)
|
|
1851
2120
|
rotate-token revoke every paired device
|
|
2121
|
+
update install the latest coderemote (up says when there's one)
|
|
2122
|
+
requests [PORT] list the requests that reached a previewed port, oldest first;
|
|
2123
|
+
--grep TEXT, --failed (errors, 4xx and 5xx), --limit N (default 50)
|
|
2124
|
+
requests clear [PORT] forget the recorded requests
|
|
2125
|
+
request <ID> [PORT] headers and bodies of one request (bodies up to 64 KB are kept);
|
|
2126
|
+
auth headers, cookies and token-like fields are masked unless
|
|
2127
|
+
--show-secrets is given; --json on either for machine-readable output
|
|
2128
|
+
version print this coderemote's version (also --version, -v)
|
|
1852
2129
|
login sign in to Microsoft Dev Tunnels with GitHub (up does this when needed);
|
|
1853
2130
|
--microsoft for a Microsoft account, --device-code without a browser
|
|
1854
2131
|
|
|
@@ -1861,16 +2138,16 @@ Options:
|
|
|
1861
2138
|
this folder in VS Code in the browser (needs VS Code's \`code\`)`;
|
|
1862
2139
|
var HELP_FLAGS = ["help", "--help", "-help", "-h"];
|
|
1863
2140
|
var argv = process.argv.slice(2);
|
|
1864
|
-
var [cmd, ...rest] = argv.length === 0 || argv.some((a) => HELP_FLAGS.includes(a)) ? ["help"] : argv;
|
|
2141
|
+
var [cmd, ...rest] = argv.length === 0 || argv.some((a) => HELP_FLAGS.includes(a)) ? ["help"] : argv.some((a) => a === "--version" || a === "-v") ? ["version"] : argv;
|
|
1865
2142
|
var flag = (name) => {
|
|
1866
2143
|
const i = rest.indexOf(name);
|
|
1867
2144
|
return i >= 0 ? rest[i + 1] : void 0;
|
|
1868
2145
|
};
|
|
1869
2146
|
var anonymous = rest.includes("--allow-anonymous") || rest.includes("--no-auth");
|
|
1870
2147
|
var port = flag("--port") ? parseInt(flag("--port"), 10) : DEFAULT_PORT;
|
|
1871
|
-
var root =
|
|
2148
|
+
var root = path7.resolve(flag("--root") ?? process.cwd());
|
|
1872
2149
|
var cfg = loadConfig();
|
|
1873
|
-
var registry = new AgentRegistry(
|
|
2150
|
+
var registry = new AgentRegistry(path7.join(CONFIG_DIR, "agents"));
|
|
1874
2151
|
async function start({ tunnel }) {
|
|
1875
2152
|
const running = registry.list().find((a) => a.port === port);
|
|
1876
2153
|
if (running) {
|
|
@@ -1963,6 +2240,26 @@ async function sessionsOf(agent) {
|
|
|
1963
2240
|
return [];
|
|
1964
2241
|
}
|
|
1965
2242
|
}
|
|
2243
|
+
function currentAgent() {
|
|
2244
|
+
const agents = registry.list();
|
|
2245
|
+
if (flag("--port")) return agents.find((a) => a.port === port);
|
|
2246
|
+
const here = process.cwd();
|
|
2247
|
+
const containing = agents.filter((a) => here === a.root || here.startsWith(a.root + path7.sep)).sort((a, b) => b.root.length - a.root.length);
|
|
2248
|
+
return containing[0] ?? (agents.length === 1 ? agents[0] : void 0);
|
|
2249
|
+
}
|
|
2250
|
+
async function previewPorts(agent, given) {
|
|
2251
|
+
if (given) return [given];
|
|
2252
|
+
let ports = [];
|
|
2253
|
+
try {
|
|
2254
|
+
ports = (await agentApi(agent, "GET", "/api/preview")).ports.map((p) => p.port);
|
|
2255
|
+
} catch {
|
|
2256
|
+
}
|
|
2257
|
+
if (!ports.length) {
|
|
2258
|
+
console.error("No port is being previewed. Open a preview from the app, or name the port (a closed preview keeps its log).");
|
|
2259
|
+
process.exit(1);
|
|
2260
|
+
}
|
|
2261
|
+
return ports;
|
|
2262
|
+
}
|
|
1966
2263
|
function ago(ms) {
|
|
1967
2264
|
const s = Math.round((Date.now() - ms) / 1e3);
|
|
1968
2265
|
if (s < 60) return `${s}s`;
|
|
@@ -1979,6 +2276,7 @@ Token: ${cfg.token}`);
|
|
|
1979
2276
|
break;
|
|
1980
2277
|
}
|
|
1981
2278
|
case "up": {
|
|
2279
|
+
const update = PACKAGED && !process.env.CODEREMOTE_NO_UPDATE_CHECK ? availableUpdate({ current: VERSION, dir: CONFIG_DIR }).catch(() => null) : null;
|
|
1982
2280
|
let started;
|
|
1983
2281
|
try {
|
|
1984
2282
|
started = await start({ tunnel: true });
|
|
@@ -1991,6 +2289,12 @@ Token: ${cfg.token}`);
|
|
|
1991
2289
|
console.log(`
|
|
1992
2290
|
Sharing: ${root}`);
|
|
1993
2291
|
printPairing(url, cfg.token, access, vscodeUrl);
|
|
2292
|
+
const newer = await update;
|
|
2293
|
+
if (newer) {
|
|
2294
|
+
const c = (code, s) => process.stdout.isTTY ? `\x1B[${code}m${s}\x1B[0m` : s;
|
|
2295
|
+
console.log(` ${c("1;33", "\u2B06 Update available:")} ${c("2", VERSION)} \u2192 ${c("1;32", newer)}. Run ${c("36", "coderemote update")} to install it.
|
|
2296
|
+
`);
|
|
2297
|
+
}
|
|
1994
2298
|
break;
|
|
1995
2299
|
}
|
|
1996
2300
|
case "status":
|
|
@@ -2059,6 +2363,117 @@ Token: ${cfg.token}`);
|
|
|
2059
2363
|
await resolveDevtunnel();
|
|
2060
2364
|
process.exit(await interactiveLogin(loginArgs()));
|
|
2061
2365
|
}
|
|
2366
|
+
case "update": {
|
|
2367
|
+
if (!PACKAGED) {
|
|
2368
|
+
console.log("This coderemote runs from a source checkout: update it with `git pull`.");
|
|
2369
|
+
break;
|
|
2370
|
+
}
|
|
2371
|
+
const latest = await fetchLatest({ timeoutMs: 1e4 });
|
|
2372
|
+
if (!latest) {
|
|
2373
|
+
console.error("Couldn't reach the npm registry. Try again, or run `npm install -g coderemote@latest`.");
|
|
2374
|
+
process.exit(1);
|
|
2375
|
+
}
|
|
2376
|
+
if (!isNewer(latest, VERSION)) {
|
|
2377
|
+
console.log(`coderemote ${VERSION} is the latest version.`);
|
|
2378
|
+
break;
|
|
2379
|
+
}
|
|
2380
|
+
const [command, args] = installCommand(fs8.realpathSync(process.argv[1]));
|
|
2381
|
+
console.log(`Updating coderemote ${VERSION} \u2192 ${latest}: ${command} ${args.join(" ")}
|
|
2382
|
+
`);
|
|
2383
|
+
const code = await new Promise((resolve) => {
|
|
2384
|
+
spawn3(command, args, { stdio: "inherit" }).on("exit", resolve).on("error", () => resolve(127));
|
|
2385
|
+
});
|
|
2386
|
+
if (code !== 0) {
|
|
2387
|
+
console.error(`
|
|
2388
|
+
The update failed (${command} exited with ${code}).`);
|
|
2389
|
+
console.error(`If it was a permission error (EACCES), run it with sudo: sudo ${command} ${args.join(" ")}`);
|
|
2390
|
+
process.exit(code || 1);
|
|
2391
|
+
}
|
|
2392
|
+
const running = registry.list();
|
|
2393
|
+
console.log(`
|
|
2394
|
+
Updated to coderemote ${latest}.`);
|
|
2395
|
+
if (running.length) {
|
|
2396
|
+
console.log(`${running.length === 1 ? "An agent is" : `${running.length} agents are`} still running the previous version.`);
|
|
2397
|
+
console.log("Restart: `coderemote stop --all`, then `coderemote up` in each shared folder.");
|
|
2398
|
+
}
|
|
2399
|
+
break;
|
|
2400
|
+
}
|
|
2401
|
+
case "requests":
|
|
2402
|
+
case "request": {
|
|
2403
|
+
const agent = currentAgent();
|
|
2404
|
+
if (!agent) {
|
|
2405
|
+
const running = registry.list();
|
|
2406
|
+
console.error(
|
|
2407
|
+
flag("--port") ? `No agent is running on port ${port}.` : running.length ? `Several agents are running; run this inside a shared folder or pick one with --port (${running.map((a) => a.port).join(", ")}).` : "No agent is running. Start one with `coderemote up`."
|
|
2408
|
+
);
|
|
2409
|
+
process.exit(1);
|
|
2410
|
+
}
|
|
2411
|
+
const valued = /* @__PURE__ */ new Set(["--grep", "--limit", "--port"]);
|
|
2412
|
+
const words = rest.filter((a, i) => !a.startsWith("--") && !valued.has(rest[i - 1]));
|
|
2413
|
+
const json = rest.includes("--json");
|
|
2414
|
+
const redactor = new Redactor({ show: rest.includes("--show-secrets") });
|
|
2415
|
+
const hint = () => {
|
|
2416
|
+
if (redactor.hidden && !json) console.error(`
|
|
2417
|
+
(${redactor.hidden} secret value${redactor.hidden === 1 ? "" : "s"} masked; add --show-secrets to print them)`);
|
|
2418
|
+
};
|
|
2419
|
+
if (cmd === "request") {
|
|
2420
|
+
const id = parseInt(words[0], 10);
|
|
2421
|
+
if (!id) {
|
|
2422
|
+
console.error("Usage: coderemote request <ID> [PORT] [--show-secrets] [--json]");
|
|
2423
|
+
process.exit(1);
|
|
2424
|
+
}
|
|
2425
|
+
let found;
|
|
2426
|
+
for (const p of await previewPorts(agent, words[1])) {
|
|
2427
|
+
try {
|
|
2428
|
+
found = await agentApi(agent, "GET", "/api/preview/request", { port: p, id });
|
|
2429
|
+
break;
|
|
2430
|
+
} catch {
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
if (!found) {
|
|
2434
|
+
console.error(`No request #${id} (only the latest 200 per port are kept).`);
|
|
2435
|
+
process.exit(1);
|
|
2436
|
+
}
|
|
2437
|
+
console.log(json ? JSON.stringify(redactDetail(found, redactor), null, 2) : formatDetail(found, redactor));
|
|
2438
|
+
hint();
|
|
2439
|
+
break;
|
|
2440
|
+
}
|
|
2441
|
+
if (words[0] === "clear") {
|
|
2442
|
+
for (const p of await previewPorts(agent, words[1])) {
|
|
2443
|
+
await agentApi(agent, "DELETE", "/api/preview/requests", { port: p });
|
|
2444
|
+
console.log(`cleared the requests to port ${p}`);
|
|
2445
|
+
}
|
|
2446
|
+
break;
|
|
2447
|
+
}
|
|
2448
|
+
const ports = await previewPorts(agent, words[0]);
|
|
2449
|
+
let requests = [];
|
|
2450
|
+
for (const p of ports) {
|
|
2451
|
+
const { requests: list } = await agentApi(agent, "GET", "/api/preview/requests", { port: p });
|
|
2452
|
+
requests.push(...list.map((r) => ({ ...r, port: Number(p) })));
|
|
2453
|
+
}
|
|
2454
|
+
requests.sort((a, b) => a.id - b.id);
|
|
2455
|
+
requests = filterRequests(requests, { grep: flag("--grep"), failed: rest.includes("--failed") });
|
|
2456
|
+
const limit = parseInt(flag("--limit"), 10) || 50;
|
|
2457
|
+
const total = requests.length;
|
|
2458
|
+
requests = requests.slice(-limit);
|
|
2459
|
+
if (json) {
|
|
2460
|
+
console.log(JSON.stringify(requests.map((r) => ({ ...r, path: redactor.path(r.path) })), null, 2));
|
|
2461
|
+
break;
|
|
2462
|
+
}
|
|
2463
|
+
if (!total) {
|
|
2464
|
+
console.log("No matching requests.");
|
|
2465
|
+
break;
|
|
2466
|
+
}
|
|
2467
|
+
console.log(formatList(requests, redactor, { showPort: ports.length > 1 }));
|
|
2468
|
+
if (total > requests.length) console.error(`
|
|
2469
|
+
(${total - requests.length} older not shown; raise --limit)`);
|
|
2470
|
+
console.error("\nDetails: coderemote request <ID>");
|
|
2471
|
+
hint();
|
|
2472
|
+
break;
|
|
2473
|
+
}
|
|
2474
|
+
case "version":
|
|
2475
|
+
console.log(VERSION);
|
|
2476
|
+
break;
|
|
2062
2477
|
case "help":
|
|
2063
2478
|
console.log(USAGE);
|
|
2064
2479
|
break;
|
package/package.json
CHANGED