coderemote 0.2.1 → 0.3.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 +8 -0
- package/dist/coderemote.js +297 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -95,6 +95,8 @@ 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)
|
|
100
102
|
coderemote update # install the latest version (`up` says when there's one)
|
|
@@ -104,6 +106,12 @@ coderemote help # all options
|
|
|
104
106
|
Options: `--root DIR` shares another folder; `--port N` picks the local port (default
|
|
105
107
|
`31580`). The tunnel URL stays the same between runs, so paired phones reconnect on their own.
|
|
106
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
|
+
|
|
107
115
|
## Requirements
|
|
108
116
|
|
|
109
117
|
- **Node.js 20** or newer on **macOS** (Linux is expected to work but not yet tested)
|
package/dist/coderemote.js
CHANGED
|
@@ -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";
|
|
@@ -1478,7 +1479,7 @@ function parse(out) {
|
|
|
1478
1479
|
return JSON.parse(out.slice(out.indexOf("{")));
|
|
1479
1480
|
}
|
|
1480
1481
|
function machineName(description) {
|
|
1481
|
-
return (description || "").replace(/^(coderemote|vsremote)\s+/, "").replace(/\.local
|
|
1482
|
+
return (description || "").replace(/^(coderemote|vsremote)\s+/, "").replace(/\.local\b/, "") || null;
|
|
1482
1483
|
}
|
|
1483
1484
|
function machine(tunnel, current) {
|
|
1484
1485
|
return {
|
|
@@ -1587,7 +1588,7 @@ function installCommand(installedAt) {
|
|
|
1587
1588
|
// package.json
|
|
1588
1589
|
var package_default = {
|
|
1589
1590
|
name: "coderemote",
|
|
1590
|
-
version: "0.
|
|
1591
|
+
version: "0.3.1",
|
|
1591
1592
|
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
1593
|
keywords: [
|
|
1593
1594
|
"remote",
|
|
@@ -1654,6 +1655,148 @@ var package_default = {
|
|
|
1654
1655
|
}
|
|
1655
1656
|
};
|
|
1656
1657
|
|
|
1658
|
+
// src/requests.js
|
|
1659
|
+
var MASK = "[redacted]";
|
|
1660
|
+
var SECRET_HEADER = /^(authorization|proxy-authorization|cookie|set-cookie)$|token|secret|api[-_]?key|session|signature|csrf|xsrf/i;
|
|
1661
|
+
var SECRET_FIELD = /pass(word|wd)?$|secret|token|api[-_]?key|authorization|cookie|session|credential|private[-_]?key|^otp$|^code$|signature/i;
|
|
1662
|
+
var Redactor = class {
|
|
1663
|
+
hidden = 0;
|
|
1664
|
+
constructor({ show: show2 = false } = {}) {
|
|
1665
|
+
this.show = show2;
|
|
1666
|
+
}
|
|
1667
|
+
#mask() {
|
|
1668
|
+
this.hidden++;
|
|
1669
|
+
return MASK;
|
|
1670
|
+
}
|
|
1671
|
+
/** Query parameters with secret-looking names, e.g. ?token=…. */
|
|
1672
|
+
path(value) {
|
|
1673
|
+
if (this.show) return value;
|
|
1674
|
+
const q = value.indexOf("?");
|
|
1675
|
+
if (q < 0) return value;
|
|
1676
|
+
const params = value.slice(q + 1).split("&").map((pair) => {
|
|
1677
|
+
const eq = pair.indexOf("=");
|
|
1678
|
+
if (eq < 0 || !SECRET_FIELD.test(decodeSafe(pair.slice(0, eq)))) return pair;
|
|
1679
|
+
return `${pair.slice(0, eq)}=${this.#mask()}`;
|
|
1680
|
+
});
|
|
1681
|
+
return `${value.slice(0, q)}?${params.join("&")}`;
|
|
1682
|
+
}
|
|
1683
|
+
header(name, value) {
|
|
1684
|
+
if (this.show || !SECRET_HEADER.test(name)) return value;
|
|
1685
|
+
const lower = name.toLowerCase();
|
|
1686
|
+
if (lower === "authorization" || lower === "proxy-authorization") {
|
|
1687
|
+
const space = value.indexOf(" ");
|
|
1688
|
+
return space > 0 ? `${value.slice(0, space)} ${this.#mask()}` : this.#mask();
|
|
1689
|
+
}
|
|
1690
|
+
if (lower === "cookie") return value.split(/;\s*/).map((c) => this.#cookie(c)).join("; ");
|
|
1691
|
+
if (lower === "set-cookie") {
|
|
1692
|
+
const [first, ...attributes] = value.split(/;\s*/);
|
|
1693
|
+
return [this.#cookie(first), ...attributes].join("; ");
|
|
1694
|
+
}
|
|
1695
|
+
return this.#mask();
|
|
1696
|
+
}
|
|
1697
|
+
#cookie(pair) {
|
|
1698
|
+
const eq = pair.indexOf("=");
|
|
1699
|
+
return eq < 0 ? pair : `${pair.slice(0, eq)}=${this.#mask()}`;
|
|
1700
|
+
}
|
|
1701
|
+
/** JSON and form bodies get secret fields masked; other text is left as sent. */
|
|
1702
|
+
body(text, contentType = "", { pretty = true } = {}) {
|
|
1703
|
+
if (/json/i.test(contentType) || /^\s*[[{]/.test(text)) {
|
|
1704
|
+
try {
|
|
1705
|
+
const value = JSON.parse(text);
|
|
1706
|
+
return JSON.stringify(this.show ? value : this.#json(value), null, pretty ? 2 : void 0);
|
|
1707
|
+
} catch {
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
if (!this.show && /x-www-form-urlencoded/i.test(contentType)) return this.path(`?${text}`).slice(1);
|
|
1711
|
+
return text;
|
|
1712
|
+
}
|
|
1713
|
+
#json(value) {
|
|
1714
|
+
if (Array.isArray(value)) return value.map((v) => this.#json(v));
|
|
1715
|
+
if (!value || typeof value !== "object") return value;
|
|
1716
|
+
return Object.fromEntries(
|
|
1717
|
+
Object.entries(value).map(([k, v]) => [
|
|
1718
|
+
k,
|
|
1719
|
+
SECRET_FIELD.test(k) && v != null && typeof v !== "object" && typeof v !== "boolean" ? this.#mask() : this.#json(v)
|
|
1720
|
+
])
|
|
1721
|
+
);
|
|
1722
|
+
}
|
|
1723
|
+
};
|
|
1724
|
+
function decodeSafe(s) {
|
|
1725
|
+
try {
|
|
1726
|
+
return decodeURIComponent(s);
|
|
1727
|
+
} catch {
|
|
1728
|
+
return s;
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
function filterRequests(requests, { grep, failed } = {}) {
|
|
1732
|
+
const needle = grep?.toLowerCase();
|
|
1733
|
+
return requests.filter(
|
|
1734
|
+
(r) => (!needle || `${r.method} ${r.path}`.toLowerCase().includes(needle)) && (!failed || r.error || (r.status ?? 0) >= 400)
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
function time(ms) {
|
|
1738
|
+
return new Date(ms).toTimeString().slice(0, 8);
|
|
1739
|
+
}
|
|
1740
|
+
function bytes(n) {
|
|
1741
|
+
if (n < 1024) return `${n} B`;
|
|
1742
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
1743
|
+
return `${(n / 1024 / 1024).toFixed(1)} MB`;
|
|
1744
|
+
}
|
|
1745
|
+
function formatList(requests, redactor, { showPort = false } = {}) {
|
|
1746
|
+
return requests.map((r) => {
|
|
1747
|
+
const status2 = r.error ? "ERR" : r.status == null ? "..." : String(r.status);
|
|
1748
|
+
const parts = [
|
|
1749
|
+
`#${r.id}`.padEnd(6),
|
|
1750
|
+
status2.padEnd(4),
|
|
1751
|
+
`${r.method} ${redactor.path(r.path)}`,
|
|
1752
|
+
r.duration == null ? "pending" : `${r.duration} ms`,
|
|
1753
|
+
bytes(r.responseSize),
|
|
1754
|
+
time(r.time)
|
|
1755
|
+
];
|
|
1756
|
+
if (showPort) parts.push(`port ${r.port}`);
|
|
1757
|
+
if (r.error) parts.push(r.error);
|
|
1758
|
+
return parts.join(" ");
|
|
1759
|
+
}).join("\n");
|
|
1760
|
+
}
|
|
1761
|
+
function formatDetail(d, redactor) {
|
|
1762
|
+
const status2 = d.error ? `error: ${d.error}` : d.status == null ? "no response yet" : String(d.status);
|
|
1763
|
+
const lines = [
|
|
1764
|
+
`#${d.id} ${d.method} ${redactor.path(d.path)} -> ${status2}`,
|
|
1765
|
+
`${new Date(d.time).toISOString()}${d.duration == null ? "" : ` ${d.duration} ms`}`
|
|
1766
|
+
];
|
|
1767
|
+
for (const [title, message] of [
|
|
1768
|
+
["Request", d.request],
|
|
1769
|
+
["Response", d.response]
|
|
1770
|
+
]) {
|
|
1771
|
+
lines.push("", `${title} headers:`);
|
|
1772
|
+
for (const name of Object.keys(message.headers).sort()) {
|
|
1773
|
+
const values = [].concat(message.headers[name]);
|
|
1774
|
+
for (const value of values) lines.push(` ${name}: ${redactor.header(name, String(value))}`);
|
|
1775
|
+
}
|
|
1776
|
+
lines.push("", `${title} body: ${describeBody(message.body, redactor, message.headers["content-type"])}`);
|
|
1777
|
+
}
|
|
1778
|
+
return lines.join("\n");
|
|
1779
|
+
}
|
|
1780
|
+
function describeBody(body, redactor, contentType) {
|
|
1781
|
+
if (!body.size) return "(empty)";
|
|
1782
|
+
if (body.truncated) return `${bytes(body.size)}, too large to keep (only bodies up to 64 KB are recorded)`;
|
|
1783
|
+
if (body.text == null) return `${bytes(body.size)} of binary data`;
|
|
1784
|
+
return `${bytes(body.size)}
|
|
1785
|
+
${indent(redactor.body(body.text, contentType))}`;
|
|
1786
|
+
}
|
|
1787
|
+
function indent(text) {
|
|
1788
|
+
return text.replace(/^/gm, " ");
|
|
1789
|
+
}
|
|
1790
|
+
function redactDetail(d, redactor) {
|
|
1791
|
+
const message = (m) => ({
|
|
1792
|
+
headers: Object.fromEntries(
|
|
1793
|
+
Object.entries(m.headers).map(([k, v]) => [k, Array.isArray(v) ? v.map((x) => redactor.header(k, String(x))) : redactor.header(k, String(v))])
|
|
1794
|
+
),
|
|
1795
|
+
body: m.body.text == null ? m.body : { ...m.body, text: redactor.body(m.body.text, m.headers["content-type"], { pretty: false }) }
|
|
1796
|
+
});
|
|
1797
|
+
return { ...d, path: redactor.path(d.path), request: message(d.request), response: message(d.response) };
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1657
1800
|
// src/spinner.js
|
|
1658
1801
|
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1659
1802
|
var dim = (s) => `\x1B[2m${s}\x1B[22m`;
|
|
@@ -1793,12 +1936,23 @@ function loadConfig() {
|
|
|
1793
1936
|
cfg2.token = randomBytes(32).toString("base64url");
|
|
1794
1937
|
saveConfig(cfg2);
|
|
1795
1938
|
}
|
|
1939
|
+
if (cfg2.tunnelId && !cfg2.tunnels) {
|
|
1940
|
+
cfg2.tunnels = { [DEFAULT_PORT]: cfg2.tunnelId };
|
|
1941
|
+
saveConfig(cfg2);
|
|
1942
|
+
}
|
|
1796
1943
|
return cfg2;
|
|
1797
1944
|
}
|
|
1798
1945
|
function saveConfig(cfg2) {
|
|
1799
1946
|
fs8.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
1800
1947
|
fs8.writeFileSync(CONFIG_FILE, JSON.stringify(cfg2, null, 2), { mode: 384 });
|
|
1801
1948
|
}
|
|
1949
|
+
function portFree(port2) {
|
|
1950
|
+
return new Promise((resolve) => {
|
|
1951
|
+
const probe = net2.createServer();
|
|
1952
|
+
probe.once("error", () => resolve(false));
|
|
1953
|
+
probe.listen(port2, "127.0.0.1", () => probe.close(() => resolve(true)));
|
|
1954
|
+
});
|
|
1955
|
+
}
|
|
1802
1956
|
function listen(token, port2, root2, tunnelToken, preview, status2) {
|
|
1803
1957
|
const server = createAgentServer({ token, root: root2, tunnelToken, preview, tunnelStatus: status2 });
|
|
1804
1958
|
return new Promise((resolve, reject) => {
|
|
@@ -1867,20 +2021,22 @@ function applyAccess(tunnelId, anonymous2) {
|
|
|
1867
2021
|
return anonymous2 ? devtunnel("access", "create", tunnelId, "--anonymous") : devtunnel("access", "reset", tunnelId);
|
|
1868
2022
|
}
|
|
1869
2023
|
async function ensureTunnel(cfg2, port2, anonymous2) {
|
|
1870
|
-
|
|
2024
|
+
const known = cfg2.tunnels?.[port2];
|
|
2025
|
+
if (known) {
|
|
1871
2026
|
const [updated, access] = await Promise.all([
|
|
1872
|
-
devtunnel("update",
|
|
1873
|
-
applyAccess(
|
|
2027
|
+
devtunnel("update", known, "--expiration", "30d", "--add-labels", MACHINE_LABEL),
|
|
2028
|
+
applyAccess(known, anonymous2)
|
|
1874
2029
|
]);
|
|
1875
2030
|
if (updated.ok) {
|
|
1876
2031
|
if (!access.ok) throw new Error(`devtunnel access failed:
|
|
1877
2032
|
${access.out}`);
|
|
1878
|
-
return
|
|
2033
|
+
return known;
|
|
1879
2034
|
}
|
|
1880
2035
|
if (!/not found/i.test(updated.out)) throw new Error(`devtunnel update failed:
|
|
1881
2036
|
${updated.out}`);
|
|
1882
2037
|
}
|
|
1883
|
-
const
|
|
2038
|
+
const name = port2 === DEFAULT_PORT ? os4.hostname() : `${os4.hostname()} :${port2}`;
|
|
2039
|
+
const createArgs = ["create", "--expiration", "30d", "-d", `coderemote ${name}`, "--labels", MACHINE_LABEL, "--json"];
|
|
1884
2040
|
if (anonymous2) createArgs.push("--allow-anonymous");
|
|
1885
2041
|
const created = await devtunnel(...createArgs);
|
|
1886
2042
|
if (!created.ok) throw new Error(`devtunnel create failed:
|
|
@@ -1891,7 +2047,8 @@ ${created.out}`);
|
|
|
1891
2047
|
const portRes = await devtunnel("port", "create", tunnelId, "-p", String(port2), "--protocol", "http");
|
|
1892
2048
|
if (!portRes.ok) throw new Error(`devtunnel port create failed:
|
|
1893
2049
|
${portRes.out}`);
|
|
1894
|
-
cfg2.
|
|
2050
|
+
cfg2.tunnels = { ...cfg2.tunnels, [port2]: tunnelId };
|
|
2051
|
+
if (port2 === DEFAULT_PORT) cfg2.tunnelId = tunnelId;
|
|
1895
2052
|
saveConfig(cfg2);
|
|
1896
2053
|
return tunnelId;
|
|
1897
2054
|
}
|
|
@@ -1977,13 +2134,20 @@ var USAGE = `Usage: coderemote <command> [options]
|
|
|
1977
2134
|
stop [--all] stop the agent for this folder (or every agent)
|
|
1978
2135
|
rotate-token revoke every paired device
|
|
1979
2136
|
update install the latest coderemote (up says when there's one)
|
|
2137
|
+
requests [PORT] list the requests that reached a previewed port, oldest first;
|
|
2138
|
+
--grep TEXT, --failed (errors, 4xx and 5xx), --limit N (default 50)
|
|
2139
|
+
requests clear [PORT] forget the recorded requests
|
|
2140
|
+
request <ID> [PORT] headers and bodies of one request (bodies up to 64 KB are kept);
|
|
2141
|
+
auth headers, cookies and token-like fields are masked unless
|
|
2142
|
+
--show-secrets is given; --json on either for machine-readable output
|
|
1980
2143
|
version print this coderemote's version (also --version, -v)
|
|
1981
2144
|
login sign in to Microsoft Dev Tunnels with GitHub (up does this when needed);
|
|
1982
2145
|
--microsoft for a Microsoft account, --device-code without a browser
|
|
1983
2146
|
|
|
1984
2147
|
Options:
|
|
1985
2148
|
--root DIR folder to share (default: current folder)
|
|
1986
|
-
--port N local port (default: ${DEFAULT_PORT}
|
|
2149
|
+
--port N local port (default: the first free one from ${DEFAULT_PORT}; each port
|
|
2150
|
+
keeps its own tunnel URL, so a folder pairs once per port)
|
|
1987
2151
|
--allow-anonymous up: skip the sign-in, the pairing token alone grants access
|
|
1988
2152
|
(alias --no-auth)
|
|
1989
2153
|
--web up: also run \`code tunnel\` and print a vscode.dev link that opens
|
|
@@ -2001,11 +2165,32 @@ var root = path7.resolve(flag("--root") ?? process.cwd());
|
|
|
2001
2165
|
var cfg = loadConfig();
|
|
2002
2166
|
var registry = new AgentRegistry(path7.join(CONFIG_DIR, "agents"));
|
|
2003
2167
|
async function start({ tunnel }) {
|
|
2004
|
-
const
|
|
2005
|
-
if (
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2168
|
+
const agents = registry.list();
|
|
2169
|
+
if (flag("--port")) {
|
|
2170
|
+
const running = agents.find((a) => a.port === port);
|
|
2171
|
+
if (running) {
|
|
2172
|
+
console.error(`An agent already runs on port ${port} for ${running.root} (pid ${running.pid}).`);
|
|
2173
|
+
console.error("Stop it with `coderemote stop --all`, or pick another port with --port.");
|
|
2174
|
+
process.exit(1);
|
|
2175
|
+
}
|
|
2176
|
+
} else {
|
|
2177
|
+
const sharing = agents.find((a) => a.root === root);
|
|
2178
|
+
if (sharing) {
|
|
2179
|
+
console.error(`${root} is already shared on port ${sharing.port} (pid ${sharing.pid}).`);
|
|
2180
|
+
console.error("Stop it with `coderemote stop`, or start another one with --port.");
|
|
2181
|
+
process.exit(1);
|
|
2182
|
+
}
|
|
2183
|
+
const taken = async (p) => agents.some((a) => a.port === p) || !await portFree(p);
|
|
2184
|
+
const remembered = cfg.folderPorts?.[root];
|
|
2185
|
+
if (remembered && !await taken(remembered)) port = remembered;
|
|
2186
|
+
else {
|
|
2187
|
+
const claimed = new Set(Object.values(cfg.folderPorts ?? {}));
|
|
2188
|
+
while (claimed.has(port) || await taken(port)) port++;
|
|
2189
|
+
}
|
|
2190
|
+
if (port !== remembered) {
|
|
2191
|
+
cfg.folderPorts = { ...cfg.folderPorts, [root]: port };
|
|
2192
|
+
saveConfig(cfg);
|
|
2193
|
+
}
|
|
2009
2194
|
}
|
|
2010
2195
|
let server;
|
|
2011
2196
|
let tunnelId;
|
|
@@ -2045,7 +2230,7 @@ async function start({ tunnel }) {
|
|
|
2045
2230
|
account = user;
|
|
2046
2231
|
spinner.succeed();
|
|
2047
2232
|
spinner.start("Connecting to the relay");
|
|
2048
|
-
const known = cfg.
|
|
2233
|
+
const known = cfg.tunnels?.[port];
|
|
2049
2234
|
let hosting = known ? hostTunnel(known, port) : null;
|
|
2050
2235
|
hosting?.catch(() => {
|
|
2051
2236
|
});
|
|
@@ -2092,6 +2277,26 @@ async function sessionsOf(agent) {
|
|
|
2092
2277
|
return [];
|
|
2093
2278
|
}
|
|
2094
2279
|
}
|
|
2280
|
+
function currentAgent() {
|
|
2281
|
+
const agents = registry.list();
|
|
2282
|
+
if (flag("--port")) return agents.find((a) => a.port === port);
|
|
2283
|
+
const here = process.cwd();
|
|
2284
|
+
const containing = agents.filter((a) => here === a.root || here.startsWith(a.root + path7.sep)).sort((a, b) => b.root.length - a.root.length);
|
|
2285
|
+
return containing[0] ?? (agents.length === 1 ? agents[0] : void 0);
|
|
2286
|
+
}
|
|
2287
|
+
async function previewPorts(agent, given) {
|
|
2288
|
+
if (given) return [given];
|
|
2289
|
+
let ports = [];
|
|
2290
|
+
try {
|
|
2291
|
+
ports = (await agentApi(agent, "GET", "/api/preview")).ports.map((p) => p.port);
|
|
2292
|
+
} catch {
|
|
2293
|
+
}
|
|
2294
|
+
if (!ports.length) {
|
|
2295
|
+
console.error("No port is being previewed. Open a preview from the app, or name the port (a closed preview keeps its log).");
|
|
2296
|
+
process.exit(1);
|
|
2297
|
+
}
|
|
2298
|
+
return ports;
|
|
2299
|
+
}
|
|
2095
2300
|
function ago(ms) {
|
|
2096
2301
|
const s = Math.round((Date.now() - ms) / 1e3);
|
|
2097
2302
|
if (s < 60) return `${s}s`;
|
|
@@ -2122,8 +2327,11 @@ Token: ${cfg.token}`);
|
|
|
2122
2327
|
Sharing: ${root}`);
|
|
2123
2328
|
printPairing(url, cfg.token, access, vscodeUrl);
|
|
2124
2329
|
const newer = await update;
|
|
2125
|
-
if (newer)
|
|
2330
|
+
if (newer) {
|
|
2331
|
+
const c = (code, s) => process.stdout.isTTY ? `\x1B[${code}m${s}\x1B[0m` : s;
|
|
2332
|
+
console.log(` ${c("1;33", "\u2B06 Update available:")} ${c("2", VERSION)} \u2192 ${c("1;32", newer)}. Run ${c("36", "coderemote update")} to install it.
|
|
2126
2333
|
`);
|
|
2334
|
+
}
|
|
2127
2335
|
break;
|
|
2128
2336
|
}
|
|
2129
2337
|
case "status":
|
|
@@ -2227,6 +2435,79 @@ Updated to coderemote ${latest}.`);
|
|
|
2227
2435
|
}
|
|
2228
2436
|
break;
|
|
2229
2437
|
}
|
|
2438
|
+
case "requests":
|
|
2439
|
+
case "request": {
|
|
2440
|
+
const agent = currentAgent();
|
|
2441
|
+
if (!agent) {
|
|
2442
|
+
const running = registry.list();
|
|
2443
|
+
console.error(
|
|
2444
|
+
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`."
|
|
2445
|
+
);
|
|
2446
|
+
process.exit(1);
|
|
2447
|
+
}
|
|
2448
|
+
const valued = /* @__PURE__ */ new Set(["--grep", "--limit", "--port"]);
|
|
2449
|
+
const words = rest.filter((a, i) => !a.startsWith("--") && !valued.has(rest[i - 1]));
|
|
2450
|
+
const json = rest.includes("--json");
|
|
2451
|
+
const redactor = new Redactor({ show: rest.includes("--show-secrets") });
|
|
2452
|
+
const hint = () => {
|
|
2453
|
+
if (redactor.hidden && !json) console.error(`
|
|
2454
|
+
(${redactor.hidden} secret value${redactor.hidden === 1 ? "" : "s"} masked; add --show-secrets to print them)`);
|
|
2455
|
+
};
|
|
2456
|
+
if (cmd === "request") {
|
|
2457
|
+
const id = parseInt(words[0], 10);
|
|
2458
|
+
if (!id) {
|
|
2459
|
+
console.error("Usage: coderemote request <ID> [PORT] [--show-secrets] [--json]");
|
|
2460
|
+
process.exit(1);
|
|
2461
|
+
}
|
|
2462
|
+
let found;
|
|
2463
|
+
for (const p of await previewPorts(agent, words[1])) {
|
|
2464
|
+
try {
|
|
2465
|
+
found = await agentApi(agent, "GET", "/api/preview/request", { port: p, id });
|
|
2466
|
+
break;
|
|
2467
|
+
} catch {
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
if (!found) {
|
|
2471
|
+
console.error(`No request #${id} (only the latest 200 per port are kept).`);
|
|
2472
|
+
process.exit(1);
|
|
2473
|
+
}
|
|
2474
|
+
console.log(json ? JSON.stringify(redactDetail(found, redactor), null, 2) : formatDetail(found, redactor));
|
|
2475
|
+
hint();
|
|
2476
|
+
break;
|
|
2477
|
+
}
|
|
2478
|
+
if (words[0] === "clear") {
|
|
2479
|
+
for (const p of await previewPorts(agent, words[1])) {
|
|
2480
|
+
await agentApi(agent, "DELETE", "/api/preview/requests", { port: p });
|
|
2481
|
+
console.log(`cleared the requests to port ${p}`);
|
|
2482
|
+
}
|
|
2483
|
+
break;
|
|
2484
|
+
}
|
|
2485
|
+
const ports = await previewPorts(agent, words[0]);
|
|
2486
|
+
let requests = [];
|
|
2487
|
+
for (const p of ports) {
|
|
2488
|
+
const { requests: list } = await agentApi(agent, "GET", "/api/preview/requests", { port: p });
|
|
2489
|
+
requests.push(...list.map((r) => ({ ...r, port: Number(p) })));
|
|
2490
|
+
}
|
|
2491
|
+
requests.sort((a, b) => a.id - b.id);
|
|
2492
|
+
requests = filterRequests(requests, { grep: flag("--grep"), failed: rest.includes("--failed") });
|
|
2493
|
+
const limit = parseInt(flag("--limit"), 10) || 50;
|
|
2494
|
+
const total = requests.length;
|
|
2495
|
+
requests = requests.slice(-limit);
|
|
2496
|
+
if (json) {
|
|
2497
|
+
console.log(JSON.stringify(requests.map((r) => ({ ...r, path: redactor.path(r.path) })), null, 2));
|
|
2498
|
+
break;
|
|
2499
|
+
}
|
|
2500
|
+
if (!total) {
|
|
2501
|
+
console.log("No matching requests.");
|
|
2502
|
+
break;
|
|
2503
|
+
}
|
|
2504
|
+
console.log(formatList(requests, redactor, { showPort: ports.length > 1 }));
|
|
2505
|
+
if (total > requests.length) console.error(`
|
|
2506
|
+
(${total - requests.length} older not shown; raise --limit)`);
|
|
2507
|
+
console.error("\nDetails: coderemote request <ID>");
|
|
2508
|
+
hint();
|
|
2509
|
+
break;
|
|
2510
|
+
}
|
|
2230
2511
|
case "version":
|
|
2231
2512
|
console.log(VERSION);
|
|
2232
2513
|
break;
|
package/package.json
CHANGED