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 +2 -0
- package/CHANGELOG.md +30 -0
- package/LICENSE +21 -0
- package/README.md +200 -0
- package/binary-install.js +348 -0
- package/binary.js +124 -0
- package/install.js +4 -0
- package/npm-shrinkwrap.json +52 -0
- package/package.json +90 -0
- package/run-rexy.js +4 -0
package/.gitignore
ADDED
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
|
+

|
|
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,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