@piperace/cli 0.3.5 → 0.3.6

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/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,6 +1,6 @@
1
1
  {
2
2
  "name": "@piperace/cli",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "Piperace — self-hosted ngrok alternative. NPM wrapper for the Go binary.",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -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
 
@@ -127,7 +174,7 @@ async function installBinary(kind = 'cli') {
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
+ }