rexy-cli 0.2.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/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ /node_modules
2
+
package/CHANGELOG.md ADDED
@@ -0,0 +1,30 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ---
9
+
10
+ ## [0.2.0] - 2026-09-03
11
+
12
+ ### Added
13
+
14
+ - **`--file` option for `rexy run`**: TOML rules file for multi-host redirects — several `host/path/target` rules with per-rule `csp_override` in one browser session. Excludes `--host/--path/--target/--csp-override`; an empty rules list logs a warning and runs without redirects.
15
+
16
+ ### Fixed
17
+
18
+ - **CSP override was never applied**: request→response correlation state leaked through per-request handler clones (hudsucker reuses the CONNECT-phase instance as the clone source), so the override header silently stayed the target's original one. Correlation now uses overwrite semantics; a regression test emulates the clone chain.
19
+ - **Upstream TLS now trusts the OS certificate store** instead of only the bundled webpki roots — `https://` targets with internal or mkcert CAs no longer fail with `invalid peer certificate: UnknownIssuer`. Hostname validation is unchanged: the target hostname must match the certificate SANs.
20
+
21
+ ## [0.1.0] - 2026-09-02
22
+
23
+ ### Added
24
+
25
+ - **`rexy run` command**: Launches a Chromium-based browser through a local MITM proxy.
26
+ - **Selective PAC routing**: A PAC script routes only the intercepted host and its subdomains through the proxy; all other traffic stays `DIRECT`, so WebRTC calls, CDNs and messengers are unaffected.
27
+ - **Browser resolution**: `--browser` accepts `chrome`, `chromium`, or a custom executable path.
28
+ - **`rexy trust` / `rexy clean` commands**: Install/remove the Rexy Local CA in the OS trust store on macOS, Windows, and Linux.
29
+ - **`generate_ca.sh` script**: One-time local CA generation (EC P-256).
30
+ - **`--csp-override` option for `rexy run`**: Replaces all `Content-Security-Policy` headers of responses served from `--target` with the given policy (`off` removes the header entirely).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 sumbad
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,200 @@
1
+ # rexy
2
+
3
+ `rexy` launches a browser through a local man-in-the-middle (MITM) proxy. The proxy
4
+ intercepts traffic to one live site (e.g. `example.com`) and serves requests under a
5
+ chosen path prefix from your local dev server — everything else reaches the live site
6
+ untouched. No `/etc/hosts`, no DNS tricks, no self-signed certificates on your dev
7
+ server.
8
+
9
+ You give `rexy` three things: the domain of a live site, a path prefix, and your dev
10
+ server URL. It starts the local proxy, generates a PAC file that routes only that domain
11
+ through it, and launches Chrome (or Chromium, or any browser executable). Then you just
12
+ browse the site as usual — the matching pages are transparently served from your local
13
+ build.
14
+
15
+ ![How rexy works](docs/rexy-flow.svg)
16
+
17
+ ## How it works
18
+
19
+ 1. `./generate_ca.sh` generates a local certificate authority (one-time setup) in
20
+ `ca/` — `rexy.cer` (the certificate) and `rexy.key` (its private key).
21
+ 2. `rexy trust` installs that CA into your OS trust store, so the browser accepts the
22
+ certificates the proxy issues on the fly.
23
+ 3. On `rexy run`, the tool:
24
+ - starts a [hudsucker](https://crates.io/crates/hudsucker) MITM proxy on
25
+ `127.0.0.1` (a free port by default),
26
+ - serves a PAC script that routes **only** the intercepted host (and its
27
+ subdomains) through the proxy — everything else goes `DIRECT`, so messengers,
28
+ WebRTC/STUN, long-poll and CDNs are unaffected,
29
+ - launches the browser with `--proxy-pac-url=...` and `--disable-quic`,
30
+ - rewrites matching requests: `https://<host><path>*` → `<target>`.
31
+ 4. TLS interception is restricted to the intercepted host only.
32
+
33
+ Ctrl+C stops the browser and the proxy.
34
+
35
+ ## Requirements
36
+
37
+ - OpenSSL (for `generate_ca.sh`)
38
+ - A Chromium-based browser (Chrome / Chromium / any executable path)
39
+
40
+ Supported platforms: **macOS**, **Windows**, **Linux**.
41
+
42
+ ## Install
43
+
44
+ ### Quick Install (recommended)
45
+
46
+ You can install `rexy` with a single command using the installer script.
47
+
48
+ **Linux / macOS:**
49
+ ```bash
50
+ curl --proto '=https' --tlsv1.2 -LsSf https://github.com/sumbad/rexy/releases/latest/download/rexy-installer.sh | sh
51
+ ```
52
+
53
+ **Windows:**
54
+ ```bash
55
+ powershell -ExecutionPolicy Bypass -c "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; irm https://github.com/sumbad/rexy/releases/latest/download/rexy-installer.ps1 | iex"
56
+ ```
57
+
58
+ ### Manual Installation
59
+
60
+ Alternatively, you can install `rexy` by downloading a pre-compiled binary from the [**Releases page**](https://github.com/sumbad/rexy/releases).
61
+
62
+ 1. Download the appropriate archive for your system.
63
+ 2. Unpack the archive.
64
+ 3. Move the `kley` binary to a directory in your system's `PATH` (e.g., `/usr/local/bin` on macOS/Linux).
65
+
66
+ ### Install via npm (Node.js)
67
+ If you have Node.js installed, you can install `rexy` directly from npm:
68
+
69
+ ```bash
70
+ npm install -g rexy-cli
71
+ ```
72
+
73
+ ### Install via Cargo (crates.io)
74
+ If you have Rust and Cargo installed, you can install `rexy` directly from crates.io:
75
+
76
+ ```bash
77
+ cargo install --locked rexy
78
+ ```
79
+
80
+ Or build from source:
81
+
82
+ ```sh
83
+ cargo install --path .
84
+ ```
85
+
86
+ ## Setup
87
+
88
+ ```sh
89
+ # 1. Generate the local CA (one-time; creates ca/rexy.cer and ca/rexy.key)
90
+ ./generate_ca.sh
91
+
92
+ # 2. Install the CA into the OS trust store
93
+ rexy trust
94
+ ```
95
+
96
+ - **macOS** — installs into the login keychain via `security` (`trustRoot`, SSL policy)
97
+ - **Windows** — installs into the current-user `Root` store via `certutil`
98
+ - **Linux** — copies the certificate to
99
+ `/usr/local/share/ca-certificates/local-dev-proxy.crt` and runs
100
+ `update-ca-certificates` (via `pkexec`)
101
+
102
+ `rexy trust` is idempotent: re-running it after regenerating the CA replaces the old certificate. `rexy clean` removes the CA from the trust store.
103
+
104
+ ## Usage
105
+
106
+ Single rule via flags:
107
+
108
+ ```
109
+ rexy run --host <host> --path <path> --target <url> -- <browser args>
110
+ ```
111
+
112
+ Example serves an url from a local Vite dev server:
113
+
114
+ ```sh
115
+ rexy run \
116
+ --browser chrome \
117
+ --host example.com \
118
+ --path /app/ \
119
+ --target http://127.0.0.1:5173 \
120
+ -- --new-window https://example.com/app/foo
121
+ ```
122
+
123
+ If the target server sends a restrictive `Content-Security-Policy` that breaks the proxied page (e.g. `frame-ancestors` blocks embedding it in a parent shell), override the header for responses served from the target:
124
+
125
+ ```sh
126
+ rexy run \
127
+ --browser chrome \
128
+ --host example.com \
129
+ --path / \
130
+ --target https://dev.example.internal \
131
+ --csp-override "frame-ancestors *" \
132
+ -- --new-window https://app.example.com/
133
+ ```
134
+
135
+ `--csp-override off` removes the header entirely. Only responses actually redirected to `--target` are affected; production passthrough traffic and `Content-Security-Policy-Report-Only` are never modified.
136
+
137
+ ### Multiple rules via a config file
138
+
139
+ Pass `--file`/`-f` with a TOML file to intercept several hosts with one browser:
140
+
141
+ ```toml
142
+ # rules.toml
143
+ [[rules]]
144
+ host = "app.example.com"
145
+ path = "/"
146
+ target = "https://dev-app.example.internal"
147
+ csp_override = "frame-ancestors *" # optional; "off" removes the header
148
+
149
+ [[rules]]
150
+ host = "other.example.com"
151
+ target = "http://127.0.0.1:3000"
152
+ ```
153
+
154
+ ```sh
155
+ rexy run -f rules.toml
156
+ ```
157
+
158
+ - `--file` cannot be combined with `--host`, `--path`, `--target` or `--csp-override` — those configure a single rule.
159
+ - `--browser`, `--proxy-port` and browser arguments after `--` stay on the command line.
160
+ - The first matching rule wins (host + path prefix).
161
+ - An empty `rules` list is allowed: rexy logs a warning and runs without redirects.
162
+
163
+ ### Commands
164
+
165
+ | Command | Description |
166
+ | ------------- | -------------------------------------------------- |
167
+ | `rexy run` | Launch the browser through the local proxy |
168
+ | `rexy trust` | Install the Rexy Local CA into the OS trust store |
169
+ | `rexy clean` | Remove the Rexy Local CA from the OS trust store |
170
+
171
+ ### `run` options
172
+
173
+ | Option | Default | Description |
174
+ | -------------------------- | ---------- | ------------------------------------------------------------------ |
175
+ | `--browser <name or path>` | `chrome` | `chrome`, `chromium`, or a path to a browser executable |
176
+ | `--file <path>` (`-f`) | — | TOML file with `[[rules]]`; excludes `--host/--path/--target/--csp-override` |
177
+ | `--host <host>` | — | Production hostname to intercept (hostname only, no path/scheme) |
178
+ | `--path <prefix>` | `/` | Production path prefix to redirect (must start with `/`) |
179
+ | `--target <url>` | — | Local development server (`http://` or `https://`) |
180
+ | `--proxy-port <port>` | `0` | Local proxy port; `0` picks a free port |
181
+ | `--csp-override <policy\|off>` | — | Replace all `Content-Security-Policy` headers of responses served from `--target` (`off` removes them); passthrough traffic is untouched |
182
+ | `-- <args>` | — | Extra arguments passed to the browser |
183
+
184
+ ### Logging
185
+
186
+ Logging is controlled by `RUST_LOG` (via `tracing-subscriber`), e.g.:
187
+
188
+ ```sh
189
+ RUST_LOG=debug rexy run ...
190
+ ```
191
+
192
+ ## Security notes
193
+
194
+ - `ca/rexy.key` is the private key of your local CA. It never leaves your machine and must **never** be committed.
195
+ - The CA is scoped to this machine's development use. Regenerate it if it may have leaked, then re-run `rexy trust`.
196
+ - Only traffic to the hosts you explicitly configure (via `--host` or rules in `--file`) is intercepted and decrypted.
197
+
198
+ ## License
199
+
200
+ Licensed under the [MIT License](LICENSE).
@@ -0,0 +1,348 @@
1
+ const {
2
+ createWriteStream,
3
+ existsSync,
4
+ mkdirSync,
5
+ mkdtemp,
6
+ rmSync,
7
+ } = require("fs");
8
+ const { join, sep } = require("path");
9
+ const { spawnSync } = require("child_process");
10
+ const { tmpdir } = require("os");
11
+
12
+ const https = require("node:https");
13
+ const http = require("node:http");
14
+
15
+ const tmpDir = tmpdir();
16
+
17
+ const error = (msg) => {
18
+ console.error(msg);
19
+ process.exit(1);
20
+ };
21
+
22
+ function getProxyForUrl(urlString) {
23
+ const url = new URL(urlString);
24
+ const isHttps = url.protocol === "https:";
25
+
26
+ const noProxy = process.env.NO_PROXY || process.env.no_proxy || "";
27
+ if (noProxy === "*") return null;
28
+ if (noProxy) {
29
+ const hostname = url.hostname.toLowerCase();
30
+ const noProxyList = noProxy.split(",").map((s) => s.trim().toLowerCase());
31
+ for (const entry of noProxyList) {
32
+ if (hostname === entry || hostname.endsWith("." + entry)) {
33
+ return null;
34
+ }
35
+ }
36
+ }
37
+
38
+ const proxyEnv = isHttps
39
+ ? process.env.HTTPS_PROXY || process.env.https_proxy
40
+ : process.env.HTTP_PROXY || process.env.http_proxy;
41
+
42
+ if (!proxyEnv) return null;
43
+
44
+ const proxyUrl = new URL(proxyEnv);
45
+
46
+ let auth = null;
47
+ if (proxyUrl.username || proxyUrl.password) {
48
+ auth = `${proxyUrl.username}:${proxyUrl.password}`;
49
+ }
50
+
51
+ return {
52
+ hostname: proxyUrl.hostname,
53
+ port: proxyUrl.port || (proxyUrl.protocol === "https:" ? 443 : 80),
54
+ auth: auth,
55
+ };
56
+ }
57
+
58
+ function connectThroughProxy(proxy, target) {
59
+ return new Promise((resolve, reject) => {
60
+ const headers = {};
61
+ if (proxy.auth) {
62
+ headers["Proxy-Authorization"] =
63
+ "Basic " + Buffer.from(proxy.auth).toString("base64");
64
+ }
65
+
66
+ const connectReq = http.request({
67
+ hostname: proxy.hostname,
68
+ port: proxy.port,
69
+ method: "CONNECT",
70
+ path: `${target.hostname}:${target.port || 443}`,
71
+ headers,
72
+ });
73
+ connectReq.on("connect", (res, socket) => {
74
+ if (res.statusCode === 200) {
75
+ resolve(socket);
76
+ } else {
77
+ reject(new Error(`Proxy CONNECT failed with status ${res.statusCode}`));
78
+ }
79
+ });
80
+ connectReq.on("error", reject);
81
+ connectReq.end();
82
+ });
83
+ }
84
+
85
+ function download(urlString, maxRedirects) {
86
+ if (maxRedirects === undefined) maxRedirects = 5;
87
+ return new Promise((resolve, reject) => {
88
+ if (maxRedirects < 0) {
89
+ return reject(new Error("Too many redirects"));
90
+ }
91
+
92
+ const parsed = new URL(urlString);
93
+ const isHttps = parsed.protocol === "https:";
94
+ const mod = isHttps ? https : http;
95
+ const proxy = getProxyForUrl(urlString);
96
+
97
+ const doRequest = (extraOptions) => {
98
+ const options = Object.assign(
99
+ {
100
+ hostname: parsed.hostname,
101
+ port: parsed.port || (isHttps ? 443 : 80),
102
+ path: parsed.pathname + parsed.search,
103
+ method: "GET",
104
+ headers: { "User-Agent": "cargo-dist-npm-installer" },
105
+ },
106
+ extraOptions || {},
107
+ );
108
+
109
+ if (proxy && !isHttps) {
110
+ // HTTP through HTTP proxy: request the full URL via the proxy
111
+ options.hostname = proxy.hostname;
112
+ options.port = proxy.port;
113
+ options.path = urlString;
114
+ if (proxy.auth) {
115
+ options.headers["Proxy-Authorization"] =
116
+ "Basic " + Buffer.from(proxy.auth).toString("base64");
117
+ }
118
+ }
119
+
120
+ const req = mod.request(options, (res) => {
121
+ if (
122
+ res.statusCode >= 300 &&
123
+ res.statusCode < 400 &&
124
+ res.headers.location
125
+ ) {
126
+ res.resume();
127
+ const nextUrl = new URL(res.headers.location, urlString).toString();
128
+ return download(nextUrl, maxRedirects - 1).then(resolve, reject);
129
+ }
130
+ if (res.statusCode < 200 || res.statusCode >= 300) {
131
+ res.resume();
132
+ return reject(new Error(`HTTP ${res.statusCode} from ${urlString}`));
133
+ }
134
+ resolve(res);
135
+ });
136
+ req.on("error", reject);
137
+ req.end();
138
+ };
139
+
140
+ if (proxy && isHttps) {
141
+ connectThroughProxy(proxy, parsed).then(
142
+ (socket) => doRequest({ socket, agent: false }),
143
+ reject,
144
+ );
145
+ } else {
146
+ doRequest();
147
+ }
148
+ });
149
+ }
150
+
151
+ class Package {
152
+ constructor(platform, name, url, filename, zipExt, binaries) {
153
+ let errors = [];
154
+ if (typeof url !== "string") {
155
+ errors.push("url must be a string");
156
+ } else {
157
+ try {
158
+ new URL(url);
159
+ } catch (e) {
160
+ errors.push(e);
161
+ }
162
+ }
163
+ if (name && typeof name !== "string") {
164
+ errors.push("package name must be a string");
165
+ }
166
+ if (!name) {
167
+ errors.push("You must specify the name of your package");
168
+ }
169
+ if (binaries && typeof binaries !== "object") {
170
+ errors.push("binaries must be a string => string map");
171
+ }
172
+ if (!binaries) {
173
+ errors.push("You must specify the binaries in the package");
174
+ }
175
+
176
+ if (errors.length > 0) {
177
+ let errorMsg =
178
+ "One or more of the parameters you passed to the Binary constructor are invalid:\n";
179
+ errors.forEach((error) => {
180
+ errorMsg += error;
181
+ });
182
+ errorMsg +=
183
+ '\n\nCorrect usage: new Package("my-binary", "https://example.com/binary/download.tar.gz", {"my-binary": "my-binary"})';
184
+ error(errorMsg);
185
+ }
186
+
187
+ this.platform = platform;
188
+ this.url = url;
189
+ this.name = name;
190
+ this.filename = filename;
191
+ this.zipExt = zipExt;
192
+ this.installDirectory = join(__dirname, "node_modules", ".bin_real");
193
+ this.binaries = binaries;
194
+
195
+ if (!existsSync(this.installDirectory)) {
196
+ mkdirSync(this.installDirectory, { recursive: true });
197
+ }
198
+ }
199
+
200
+ exists() {
201
+ for (const binaryName in this.binaries) {
202
+ const binRelPath = this.binaries[binaryName];
203
+ const binPath = join(this.installDirectory, binRelPath);
204
+ if (!existsSync(binPath)) {
205
+ return false;
206
+ }
207
+ }
208
+ return true;
209
+ }
210
+
211
+ install(suppressLogs = false) {
212
+ if (this.exists()) {
213
+ if (!suppressLogs) {
214
+ console.error(
215
+ `${this.name} is already installed, skipping installation.`,
216
+ );
217
+ }
218
+ return Promise.resolve();
219
+ }
220
+
221
+ try {
222
+ rmSync(this.installDirectory, { recursive: true, force: true });
223
+ } catch {
224
+ // ignore - directory may not exist
225
+ }
226
+
227
+ mkdirSync(this.installDirectory, { recursive: true });
228
+
229
+ if (!suppressLogs) {
230
+ console.error(`Downloading release from ${this.url}`);
231
+ }
232
+
233
+ return download(this.url)
234
+ .then((res) => {
235
+ return new Promise((resolve, reject) => {
236
+ mkdtemp(`${tmpDir}${sep}`, (err, directory) => {
237
+ if (err) return reject(err);
238
+ let tempFile = join(directory, this.filename);
239
+ const sink = res.pipe(createWriteStream(tempFile));
240
+ sink.on("error", (err) => reject(err));
241
+ sink.on("close", () => {
242
+ if (/\.tar\.*/.test(this.zipExt)) {
243
+ const result = spawnSync("tar", [
244
+ "xf",
245
+ tempFile,
246
+ // The tarballs are stored with a leading directory
247
+ // component; we strip one component in the
248
+ // shell installers too.
249
+ "--strip-components",
250
+ "1",
251
+ "-C",
252
+ this.installDirectory,
253
+ ]);
254
+ if (result.status == 0) {
255
+ resolve();
256
+ } else if (result.error) {
257
+ reject(result.error);
258
+ } else {
259
+ reject(
260
+ new Error(
261
+ `An error occurred untarring the artifact: stdout: ${result.stdout}; stderr: ${result.stderr}`,
262
+ ),
263
+ );
264
+ }
265
+ } else if (this.zipExt == ".zip") {
266
+ let result;
267
+ if (this.platform.artifactName.includes("windows")) {
268
+ // Windows does not have "unzip" by default on many installations, instead
269
+ // we use Expand-Archive from powershell
270
+ result = spawnSync("powershell.exe", [
271
+ "-NoProfile",
272
+ "-NonInteractive",
273
+ "-Command",
274
+ `& {
275
+ param([string]$LiteralPath, [string]$DestinationPath)
276
+ Expand-Archive -LiteralPath $LiteralPath -DestinationPath $DestinationPath -Force
277
+ }`,
278
+ tempFile,
279
+ this.installDirectory,
280
+ ]);
281
+ } else {
282
+ result = spawnSync("unzip", [
283
+ "-q",
284
+ tempFile,
285
+ "-d",
286
+ this.installDirectory,
287
+ ]);
288
+ }
289
+
290
+ if (result.status == 0) {
291
+ resolve();
292
+ } else if (result.error) {
293
+ reject(result.error);
294
+ } else {
295
+ reject(
296
+ new Error(
297
+ `An error occurred unzipping the artifact: stdout: ${result.stdout}; stderr: ${result.stderr}`,
298
+ ),
299
+ );
300
+ }
301
+ } else {
302
+ reject(
303
+ new Error(`Unrecognized file extension: ${this.zipExt}`),
304
+ );
305
+ }
306
+ });
307
+ });
308
+ });
309
+ })
310
+ .then(() => {
311
+ if (!suppressLogs) {
312
+ console.error(`${this.name} has been installed!`);
313
+ }
314
+ })
315
+ .catch((e) => {
316
+ error(`Error fetching release: ${e.message}`);
317
+ });
318
+ }
319
+
320
+ run(binaryName) {
321
+ const promise = !this.exists() ? this.install(true) : Promise.resolve();
322
+
323
+ promise
324
+ .then(() => {
325
+ const [, , ...args] = process.argv;
326
+
327
+ const options = { cwd: process.cwd(), stdio: "inherit" };
328
+
329
+ const binRelPath = this.binaries[binaryName];
330
+ if (!binRelPath) {
331
+ error(`${binaryName} is not a known binary in ${this.name}`);
332
+ }
333
+ const binPath = join(this.installDirectory, binRelPath);
334
+ const result = spawnSync(binPath, args, options);
335
+
336
+ if (result.error) {
337
+ error(result.error);
338
+ }
339
+
340
+ process.exit(result.status);
341
+ })
342
+ .catch((e) => {
343
+ error(e.message);
344
+ });
345
+ }
346
+ }
347
+
348
+ module.exports.Package = Package;
package/binary.js ADDED
@@ -0,0 +1,124 @@
1
+ const { Package } = require("./binary-install");
2
+ const os = require("os");
3
+ const libc = require("detect-libc");
4
+
5
+ const error = (msg) => {
6
+ console.error(msg);
7
+ process.exit(1);
8
+ };
9
+
10
+ const {
11
+ name,
12
+ artifactDownloadUrls,
13
+ supportedPlatforms,
14
+ glibcMinimum,
15
+ } = require("./package.json");
16
+
17
+ // FIXME: implement NPM installer handling of fallback download URLs
18
+ const artifactDownloadUrl = artifactDownloadUrls[0];
19
+ const builderGlibcMajorVersion = glibcMinimum.major;
20
+ const builderGlibcMinorVersion = glibcMinimum.series;
21
+
22
+ const getPlatform = () => {
23
+ const rawOsType = os.type();
24
+ const rawArchitecture = os.arch();
25
+
26
+ // We want to use rust-style target triples as the canonical key
27
+ // for a platform, so translate the "os" library's concepts into rust ones
28
+ let osType = "";
29
+ switch (rawOsType) {
30
+ case "Windows_NT":
31
+ osType = "pc-windows-msvc";
32
+ break;
33
+ case "Darwin":
34
+ osType = "apple-darwin";
35
+ break;
36
+ case "Linux":
37
+ osType = "unknown-linux-gnu";
38
+ break;
39
+ }
40
+
41
+ let arch = "";
42
+ switch (rawArchitecture) {
43
+ case "x64":
44
+ arch = "x86_64";
45
+ break;
46
+ case "arm64":
47
+ arch = "aarch64";
48
+ break;
49
+ }
50
+
51
+ if (rawOsType === "Linux") {
52
+ if (libc.familySync() == "musl") {
53
+ osType = "unknown-linux-musl-dynamic";
54
+ } else if (libc.isNonGlibcLinuxSync()) {
55
+ console.warn(
56
+ "Your libc is neither glibc nor musl; trying static musl binary instead",
57
+ );
58
+ osType = "unknown-linux-musl-static";
59
+ } else {
60
+ let libcVersion = libc.versionSync();
61
+ let splitLibcVersion = libcVersion.split(".");
62
+ let libcMajorVersion = splitLibcVersion[0];
63
+ let libcMinorVersion = splitLibcVersion[1];
64
+ if (
65
+ libcMajorVersion != builderGlibcMajorVersion ||
66
+ libcMinorVersion < builderGlibcMinorVersion
67
+ ) {
68
+ // We can't run the glibc binaries, but we can run the static musl ones
69
+ // if they exist
70
+ console.warn(
71
+ "Your glibc isn't compatible; trying static musl binary instead",
72
+ );
73
+ osType = "unknown-linux-musl-static";
74
+ }
75
+ }
76
+ }
77
+
78
+ // Assume the above succeeded and build a target triple to look things up with.
79
+ // If any of it failed, this lookup will fail and we'll handle it like normal.
80
+ let targetTriple = `${arch}-${osType}`;
81
+ let platform = supportedPlatforms[targetTriple];
82
+
83
+ if (!platform) {
84
+ error(
85
+ `Platform with type "${rawOsType}" and architecture "${rawArchitecture}" is not supported by ${name}.\nYour system must be one of the following:\n\n${Object.keys(
86
+ supportedPlatforms,
87
+ ).join(",")}`,
88
+ );
89
+ }
90
+
91
+ return platform;
92
+ };
93
+
94
+ const getPackage = () => {
95
+ const platform = getPlatform();
96
+ const url = `${artifactDownloadUrl}/${platform.artifactName}`;
97
+ let filename = platform.artifactName;
98
+ let ext = platform.zipExt;
99
+ let binary = new Package(platform, name, url, filename, ext, platform.bins);
100
+
101
+ return binary;
102
+ };
103
+
104
+ const install = (suppressLogs) => {
105
+ if (!artifactDownloadUrl || artifactDownloadUrl.length === 0) {
106
+ console.warn("in demo mode, not installing binaries");
107
+ return;
108
+ }
109
+ const pkg = getPackage();
110
+
111
+ return pkg.install(suppressLogs);
112
+ };
113
+
114
+ const run = (binaryName) => {
115
+ const pkg = getPackage();
116
+
117
+ pkg.run(binaryName);
118
+ };
119
+
120
+ module.exports = {
121
+ install,
122
+ run,
123
+ getPackage,
124
+ };
package/install.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { install } = require("./binary");
4
+ install(false);
@@ -0,0 +1,52 @@
1
+ {
2
+ "lockfileVersion": 3,
3
+ "name": "rexy-cli",
4
+ "packages": {
5
+ "": {
6
+ "bin": {
7
+ "rexy": "run-rexy.js"
8
+ },
9
+ "dependencies": {
10
+ "detect-libc": "^2.1.2"
11
+ },
12
+ "devDependencies": {
13
+ "prettier": "^3.8.3"
14
+ },
15
+ "engines": {
16
+ "node": ">=14.14",
17
+ "npm": ">=6"
18
+ },
19
+ "hasInstallScript": true,
20
+ "license": "MIT",
21
+ "name": "rexy-cli",
22
+ "version": "0.2.0"
23
+ },
24
+ "node_modules/detect-libc": {
25
+ "engines": {
26
+ "node": ">=8"
27
+ },
28
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
29
+ "license": "Apache-2.0",
30
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
31
+ "version": "2.1.2"
32
+ },
33
+ "node_modules/prettier": {
34
+ "bin": {
35
+ "prettier": "bin/prettier.cjs"
36
+ },
37
+ "dev": true,
38
+ "engines": {
39
+ "node": ">=14"
40
+ },
41
+ "funding": {
42
+ "url": "https://github.com/prettier/prettier?sponsor=1"
43
+ },
44
+ "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==",
45
+ "license": "MIT",
46
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz",
47
+ "version": "3.8.3"
48
+ }
49
+ },
50
+ "requires": true,
51
+ "version": "0.2.0"
52
+ }
package/package.json ADDED
@@ -0,0 +1,90 @@
1
+ {
2
+ "artifactDownloadUrls": [
3
+ "https://github.com/sumbad/rexy/releases/download/0.2.0"
4
+ ],
5
+ "author": "sumbad",
6
+ "bin": {
7
+ "rexy": "run-rexy.js"
8
+ },
9
+ "dependencies": {
10
+ "detect-libc": "^2.1.2"
11
+ },
12
+ "description": "Launch a browser with a local MITM proxy for transparent dev redirects",
13
+ "devDependencies": {
14
+ "prettier": "^3.8.3"
15
+ },
16
+ "engines": {
17
+ "node": ">=14.14",
18
+ "npm": ">=6"
19
+ },
20
+ "glibcMinimum": {
21
+ "major": 2,
22
+ "series": 35
23
+ },
24
+ "homepage": "https://github.com/sumbad/rexy",
25
+ "license": "MIT",
26
+ "name": "rexy-cli",
27
+ "preferUnplugged": true,
28
+ "repository": "https://github.com/sumbad/rexy",
29
+ "scripts": {
30
+ "fmt": "prettier --write **/*.js",
31
+ "fmt:check": "prettier --check **/*.js",
32
+ "postinstall": "node ./install.js"
33
+ },
34
+ "supportedPlatforms": {
35
+ "aarch64-apple-darwin": {
36
+ "artifactName": "rexy-aarch64-apple-darwin.tar.xz",
37
+ "bins": {
38
+ "rexy": "rexy"
39
+ },
40
+ "zipExt": ".tar.xz"
41
+ },
42
+ "aarch64-pc-windows-msvc": {
43
+ "artifactName": "rexy-x86_64-pc-windows-msvc.zip",
44
+ "bins": {
45
+ "rexy": "rexy.exe"
46
+ },
47
+ "zipExt": ".zip"
48
+ },
49
+ "aarch64-unknown-linux-gnu": {
50
+ "artifactName": "rexy-aarch64-unknown-linux-gnu.tar.xz",
51
+ "bins": {
52
+ "rexy": "rexy"
53
+ },
54
+ "zipExt": ".tar.xz"
55
+ },
56
+ "x86_64-apple-darwin": {
57
+ "artifactName": "rexy-x86_64-apple-darwin.tar.xz",
58
+ "bins": {
59
+ "rexy": "rexy"
60
+ },
61
+ "zipExt": ".tar.xz"
62
+ },
63
+ "x86_64-pc-windows-gnu": {
64
+ "artifactName": "rexy-x86_64-pc-windows-msvc.zip",
65
+ "bins": {
66
+ "rexy": "rexy.exe"
67
+ },
68
+ "zipExt": ".zip"
69
+ },
70
+ "x86_64-pc-windows-msvc": {
71
+ "artifactName": "rexy-x86_64-pc-windows-msvc.zip",
72
+ "bins": {
73
+ "rexy": "rexy.exe"
74
+ },
75
+ "zipExt": ".zip"
76
+ },
77
+ "x86_64-unknown-linux-gnu": {
78
+ "artifactName": "rexy-x86_64-unknown-linux-gnu.tar.xz",
79
+ "bins": {
80
+ "rexy": "rexy"
81
+ },
82
+ "zipExt": ".tar.xz"
83
+ }
84
+ },
85
+ "version": "0.2.0",
86
+ "volta": {
87
+ "node": "18.14.1",
88
+ "npm": "9.5.0"
89
+ }
90
+ }
package/run-rexy.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { run } = require("./binary");
4
+ run("rexy");