@piperace/cli 0.3.5 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,10 @@
1
- # Piperace NPM Wrapper
1
+ # Piperace
2
2
 
3
- NPM wrapper around the Piperace Go binaries.
3
+ Expose your localhost to the internet in seconds — a self-hosted alternative to ngrok. Share a local server behind NAT or a firewall with a stable public URL and real TLS, using a single command.
4
+
5
+ 🌐 **[piperace.com](https://piperace.com)**
6
+
7
+ ![Expose localhost with a single command](https://piperace.com/images/piperace_noqr.jpg)
4
8
 
5
9
  ## Install
6
10
 
@@ -11,36 +15,36 @@ npm install -g @piperace/cli
11
15
  ## Usage
12
16
 
13
17
  ```bash
18
+ piperace http 3000 -d myapp # expose localhost:3000 → https://myapp.piperace.fun
19
+ piperace login # keep the same subdomain + a permanent token
20
+ piperace-gui # launch the desktop app
14
21
  piperace --version
15
- piperace http 3000 -d myapp
16
- piperace login
17
- piperace-gui
18
22
  ```
19
23
 
20
- ## How it works
24
+ Works out of the box — no account required. When a tunnel starts, a QR code is printed so you can open it on your phone. Run `piperace login` to keep a stable subdomain and a permanent token.
21
25
 
22
- On `npm install`, the `postinstall` script detects your platform/architecture and either:
26
+ ![Scan the QR code to open the tunnel on your phone](https://piperace.com/images/piperace_qr.png)
23
27
 
24
- 1. Copies the matching binary from `../piperace_client/dist/` (when installed inside the Piperace monorepo), or
25
- 2. Downloads it from `https://app.piperace.com/downloads/v<VERSION>/`.
28
+ ## Supported platforms
26
29
 
27
- The downloaded binary is stored in `binaries/` inside the package and is launched by the `bin/piperace` Node.js wrapper.
30
+ Both the CLI and the desktop GUI are available for:
28
31
 
29
- ## Supported platforms
32
+ | OS | Architecture |
33
+ |---------|------------------------|
34
+ | macOS | Apple Silicon (arm64) |
35
+ | macOS | Intel (x64) |
36
+ | Linux | arm64 |
37
+ | Linux | x64 |
38
+ | Windows | x64 |
39
+ | Windows | ia32 (CLI only) |
30
40
 
31
- | OS | Arch | Binary |
32
- |----------|-------|--------------------------------|
33
- | macOS | arm64 | `piperace-darwin-arm64` |
34
- | macOS | x64 | `piperace-darwin-amd64` |
35
- | Linux | arm64 | `piperace-linux-arm64` |
36
- | Linux | x64 | `piperace-linux-amd64` |
37
- | Windows | x64 | `piperace-client.exe` |
38
- | Windows | ia32 | `piperace-client-386.exe` |
41
+ The desktop app (`piperace-gui`) is set up automatically the first time you launch it.
39
42
 
40
43
  ## Environment variables
41
44
 
42
- - `PIPERACE_BINARY_PATH` — path to a manually downloaded binary to use instead of downloading.
43
- - `PIPERACE_DOWNLOAD_URL` — override the base download URL (default: `https://app.piperace.com/downloads`).
45
+ - `PIPERACE_ENV` — `development` or `production` (selects the API/tunnel endpoints).
46
+ - `PIPERACE_BINARY_PATH` — use a specific Piperace executable instead of the managed one.
47
+ - `PIPERACE_DOWNLOAD_URL` — override the base URL Piperace uses to fetch its components (default: `https://piperace.com/downloads`).
44
48
 
45
49
  ## Programmatic API
46
50
 
@@ -48,18 +52,14 @@ The downloaded binary is stored in `binaries/` inside the package and is launche
48
52
  const piperace = require('@piperace/cli');
49
53
 
50
54
  await piperace.run(['http', '3000', '-d', 'myapp'], {
51
- env: { PIPERACE_ENV: 'production' }
55
+ env: { PIPERACE_ENV: 'production' },
52
56
  });
53
57
  ```
54
58
 
55
- ## Publishing
56
-
57
- ```bash
58
- npm version patch
59
- npm publish --access public
60
- ```
61
-
62
59
  ## macOS note
63
60
 
64
- On macOS the downloaded binary may be quarantined. The install script automatically runs `xattr -cr` to remove the quarantine attribute. For a cleaner experience, distribute a code-signed and notarized binary.
61
+ If macOS shows an “unidentified developer” warning, Piperace clears the quarantine flag automatically on first run so the app can start.
62
+
63
+ ## License
65
64
 
65
+ MIT
package/lib/index.js CHANGED
@@ -18,6 +18,8 @@ const BINARY_MAP = {
18
18
  gui: {
19
19
  'darwin arm64': 'piperace-gui-darwin-arm64',
20
20
  'darwin x64': 'piperace-gui-darwin-amd64',
21
+ 'linux arm64': 'piperace-gui-linux-arm64',
22
+ 'linux x64': 'piperace-gui-linux-amd64',
21
23
  'win32 x64': 'piperace-gui.exe'
22
24
  }
23
25
  };
@@ -26,26 +28,61 @@ function platformKey() {
26
28
  return `${os.platform()} ${os.arch()}`;
27
29
  }
28
30
 
29
- function getBinaryPath(kind = 'cli') {
31
+ function binaryPathFor(kind) {
30
32
  const name = kind === 'gui' ? 'piperace-gui' : 'piperace';
31
- const binaryPath = path.join(BIN_DIR, name + (os.platform() === 'win32' ? '.exe' : ''));
33
+ return path.join(BIN_DIR, name + (os.platform() === 'win32' ? '.exe' : ''));
34
+ }
32
35
 
33
- if (!fs.existsSync(binaryPath)) {
34
- const expectedSource = BINARY_MAP[kind]?.[platformKey()];
35
- throw new Error(
36
- `Piperace ${kind} binary not found at ${binaryPath}.\n` +
37
- `Expected source binary: ${expectedSource || 'none for ' + platformKey()}\n` +
38
- `Try running: npm run postinstall\n` +
39
- `Or set PIPERACE_BINARY_PATH to the downloaded binary.`
40
- );
36
+ function binaryExists(binaryPath) {
37
+ try {
38
+ return fs.statSync(binaryPath).size > 1000;
39
+ } catch {
40
+ return false;
41
41
  }
42
+ }
43
+
44
+ function missingBinaryError(kind, binaryPath) {
45
+ const expectedSource = BINARY_MAP[kind]?.[platformKey()];
46
+ return new Error(
47
+ `Piperace ${kind} binary not found at ${binaryPath}.\n` +
48
+ `Expected source binary: ${expectedSource || 'none for ' + platformKey()}\n` +
49
+ `Try running: npm run postinstall\n` +
50
+ `Or set PIPERACE_BINARY_PATH to the downloaded binary.`
51
+ );
52
+ }
42
53
 
54
+ function getBinaryPath(kind = 'cli') {
55
+ const binaryPath = binaryPathFor(kind);
56
+ if (!binaryExists(binaryPath)) {
57
+ throw missingBinaryError(kind, binaryPath);
58
+ }
59
+ return binaryPath;
60
+ }
61
+
62
+ // Ensure the binary is present, downloading it on demand. The GUI binary is not
63
+ // fetched during `npm install`, so its first launch lazily downloads it here.
64
+ async function ensureBinaryPath(kind = 'cli') {
65
+ const binaryPath = binaryPathFor(kind);
66
+ if (binaryExists(binaryPath)) return binaryPath;
67
+
68
+ if (kind === 'gui') {
69
+ console.log('[piperace] First GUI launch — downloading the desktop binary (one-time)...');
70
+ }
71
+
72
+ // Reuse the installer's download/retry logic. The require.main guard in
73
+ // install.js keeps this from re-running the CLI install as a side effect.
74
+ const { installBinary } = require('../scripts/install.js');
75
+ await installBinary(kind);
76
+
77
+ if (!binaryExists(binaryPath)) {
78
+ throw missingBinaryError(kind, binaryPath);
79
+ }
43
80
  return binaryPath;
44
81
  }
45
82
 
46
- function run(args = [], options = {}) {
83
+ async function run(args = [], options = {}) {
47
84
  const kind = options.gui ? 'gui' : 'cli';
48
- const binaryPath = getBinaryPath(kind);
85
+ const binaryPath = await ensureBinaryPath(kind);
49
86
  const child = spawn(binaryPath, args, {
50
87
  stdio: options.stdio || 'inherit',
51
88
  env: { ...process.env, ...options.env },
@@ -71,6 +108,7 @@ function run(args = [], options = {}) {
71
108
 
72
109
  module.exports = {
73
110
  getBinaryPath,
111
+ ensureBinaryPath,
74
112
  run,
75
113
  platformKey
76
114
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@piperace/cli",
3
- "version": "0.3.5",
4
- "description": "Piperace — self-hosted ngrok alternative. NPM wrapper for the Go binary.",
3
+ "version": "0.3.7",
4
+ "description": "Piperace — expose your localhost to the internet in seconds. A self-hosted ngrok alternative.",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
7
7
  "piperace": "bin/piperace",
@@ -5,13 +5,15 @@ const path = require('path');
5
5
  const https = require('https');
6
6
  const http = require('http');
7
7
  const os = require('os');
8
- const crypto = require('crypto');
9
8
  const { execSync } = require('child_process');
10
9
 
11
10
  const VERSION = require('../package.json').version;
12
11
  const ROOT = path.join(__dirname, '..');
13
12
  const BIN_DIR = path.join(ROOT, 'binaries');
14
13
 
14
+ const DOWNLOAD_TIMEOUT_MS = 30000; // per-attempt socket timeout
15
+ const DOWNLOAD_RETRIES = 2; // extra attempts after the first
16
+
15
17
  // Map Node platform/arch to the binary names produced by the Go build.
16
18
  const BINARY_MAP = {
17
19
  cli: {
@@ -25,6 +27,8 @@ const BINARY_MAP = {
25
27
  gui: {
26
28
  'darwin arm64': 'piperace-gui-darwin-arm64',
27
29
  'darwin x64': 'piperace-gui-darwin-amd64',
30
+ 'linux arm64': 'piperace-gui-linux-arm64',
31
+ 'linux x64': 'piperace-gui-linux-amd64',
28
32
  'win32 x64': 'piperace-gui.exe'
29
33
  }
30
34
  };
@@ -65,32 +69,76 @@ function findLocalBinary(sourceName) {
65
69
  return null;
66
70
  }
67
71
 
68
- function downloadFile(url, dest) {
72
+ // Download a single file with a hard per-attempt timeout so a stalled origin
73
+ // connection can never hang the install indefinitely.
74
+ function downloadFile(url, dest, { timeout = DOWNLOAD_TIMEOUT_MS, redirects = 5 } = {}) {
69
75
  return new Promise((resolve, reject) => {
70
76
  const client = url.startsWith('https:') ? https : http;
71
77
  const file = fs.createWriteStream(dest);
78
+ let settled = false;
72
79
 
73
- client.get(url, (res) => {
80
+ const cleanup = () => {
81
+ file.destroy();
82
+ try { fs.unlinkSync(dest); } catch {}
83
+ };
84
+ const fail = (err) => {
85
+ if (settled) return;
86
+ settled = true;
87
+ cleanup();
88
+ reject(err);
89
+ };
90
+
91
+ const req = client.get(url, (res) => {
74
92
  if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
75
- file.destroy();
76
- fs.unlinkSync(dest);
77
- return downloadFile(res.headers.location, dest).then(resolve).catch(reject);
93
+ settled = true; // hand off to the redirect target
94
+ cleanup();
95
+ res.resume();
96
+ if (redirects <= 0) {
97
+ return reject(new Error(`too many redirects from ${url}`));
98
+ }
99
+ return downloadFile(res.headers.location, dest, { timeout, redirects: redirects - 1 })
100
+ .then(resolve).catch(reject);
78
101
  }
79
102
  if (res.statusCode !== 200) {
80
- file.destroy();
81
- fs.unlinkSync(dest);
82
- return reject(new Error(`HTTP ${res.statusCode} from ${url}`));
103
+ res.resume();
104
+ return fail(new Error(`HTTP ${res.statusCode} from ${url}`));
83
105
  }
84
106
 
107
+ res.on('error', fail);
85
108
  res.pipe(file);
86
- file.on('finish', () => file.close(resolve));
87
- }).on('error', (err) => {
88
- try { fs.unlinkSync(dest); } catch {}
89
- reject(err);
109
+ file.on('finish', () => {
110
+ if (settled) return;
111
+ settled = true;
112
+ file.close((err) => (err ? reject(err) : resolve()));
113
+ });
114
+ });
115
+
116
+ // Fires on connection-establish AND idle-read stalls.
117
+ req.setTimeout(timeout, () => {
118
+ req.destroy(new Error(`download timed out after ${timeout}ms: ${url}`));
90
119
  });
120
+ req.on('error', fail);
121
+ file.on('error', fail);
91
122
  });
92
123
  }
93
124
 
125
+ async function downloadWithRetry(url, dest, { timeout = DOWNLOAD_TIMEOUT_MS, retries = DOWNLOAD_RETRIES } = {}) {
126
+ let lastErr;
127
+ for (let attempt = 0; attempt <= retries; attempt++) {
128
+ if (attempt > 0) {
129
+ const delay = 1000 * attempt;
130
+ console.warn(`[piperace] download failed (${lastErr.message}); retry ${attempt}/${retries} in ${delay}ms...`);
131
+ await new Promise((r) => setTimeout(r, delay));
132
+ }
133
+ try {
134
+ return await downloadFile(url, dest, { timeout });
135
+ } catch (err) {
136
+ lastErr = err;
137
+ }
138
+ }
139
+ throw lastErr;
140
+ }
141
+
94
142
  async function installBinary(kind = 'cli') {
95
143
  const sourceName = binaryName(kind);
96
144
  if (!sourceName) {
@@ -107,7 +155,6 @@ async function installBinary(kind = 'cli') {
107
155
  try {
108
156
  fs.chmodSync(target, 0o755);
109
157
  } catch {}
110
- console.log(`[piperace] ${kind} binary already installed.`);
111
158
  return;
112
159
  }
113
160
 
@@ -122,12 +169,12 @@ async function installBinary(kind = 'cli') {
122
169
  }
123
170
 
124
171
  // 2. Fall back to downloading from the public CDN.
125
- const baseUrl = process.env.PIPERACE_DOWNLOAD_URL || 'https://app.piperace.com/downloads';
172
+ const baseUrl = process.env.PIPERACE_DOWNLOAD_URL || 'https://piperace.com/downloads';
126
173
  const url = `${baseUrl}/v${VERSION}/${sourceName}`;
127
174
  console.log(`[piperace] Downloading ${kind} binary from ${url}`);
128
175
 
129
176
  try {
130
- await downloadFile(url, target);
177
+ await downloadWithRetry(url, target);
131
178
  fs.chmodSync(target, 0o755);
132
179
  removeQuarantine(target);
133
180
  console.log(`[piperace] ${kind} binary installed successfully.`);
@@ -135,7 +182,8 @@ async function installBinary(kind = 'cli') {
135
182
  try { fs.unlinkSync(target); } catch {}
136
183
  console.error(`[piperace] Failed to download ${kind} binary: ${err.message}`);
137
184
  console.error(`[piperace] You can manually download ${sourceName} from ${baseUrl} and place it at ${target}`);
138
- // Do not fail npm install; the launcher will print a helpful message later.
185
+ // Do not fail npm install; the launcher will retry lazily and print a
186
+ // helpful message if the binary is still missing at run time.
139
187
  }
140
188
  }
141
189
 
@@ -148,7 +196,20 @@ function removeQuarantine(filePath) {
148
196
  }
149
197
  }
150
198
 
151
- (async () => {
152
- await installBinary('cli');
153
- await installBinary('gui');
154
- })();
199
+ module.exports = {
200
+ installBinary,
201
+ binaryName,
202
+ finalBinaryPath,
203
+ platformKey,
204
+ BINARY_MAP
205
+ };
206
+
207
+ // Only the lightweight CLI binary is fetched at install time. The much larger
208
+ // GUI binary is downloaded lazily on the first `piperace-gui` run (see
209
+ // lib/index.js), so `npm install` stays fast for the common CLI-only case.
210
+ if (require.main === module) {
211
+ // Never let a failure here break `npm install`; the launcher retries lazily.
212
+ Promise.resolve()
213
+ .then(() => installBinary('cli'))
214
+ .catch((err) => console.error(`[piperace] install skipped: ${err.message}`));
215
+ }