@piperace/cli 0.4.1 → 1.5.10
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 +4 -6
- package/lib/index.js +4 -18
- package/package.json +2 -3
- package/scripts/install.js +163 -15
- package/bin/piperace-gui +0 -7
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ npm install -g @piperace/cli
|
|
|
17
17
|
```bash
|
|
18
18
|
piperace http 3000 -d myapp # expose localhost:3000 → https://myapp.piperace.fun
|
|
19
19
|
piperace login # keep the same subdomain + a permanent token
|
|
20
|
-
piperace
|
|
20
|
+
piperace ui # open the Piperace Control Center (web UI) in your browser
|
|
21
21
|
piperace --version
|
|
22
22
|
```
|
|
23
23
|
|
|
@@ -27,7 +27,7 @@ Works out of the box — no account required. When a tunnel starts, a QR code is
|
|
|
27
27
|
|
|
28
28
|
## Supported platforms
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
The CLI is available for:
|
|
31
31
|
|
|
32
32
|
| OS | Architecture |
|
|
33
33
|
|---------|------------------------|
|
|
@@ -36,15 +36,13 @@ Both the CLI and the desktop GUI are available for:
|
|
|
36
36
|
| Linux | arm64 |
|
|
37
37
|
| Linux | x64 |
|
|
38
38
|
| Windows | x64 |
|
|
39
|
-
| Windows | ia32
|
|
40
|
-
|
|
41
|
-
The desktop app (`piperace-gui`) is set up automatically the first time you launch it.
|
|
39
|
+
| Windows | ia32 |
|
|
42
40
|
|
|
43
41
|
## Environment variables
|
|
44
42
|
|
|
45
43
|
- `PIPERACE_ENV` — `development` or `production` (selects the API/tunnel endpoints).
|
|
46
44
|
- `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
|
|
45
|
+
- `PIPERACE_DOWNLOAD_URL` — override the base URL Piperace uses to fetch its components (default: `https://piperace-downloads.s3.us-east-1.amazonaws.com`).
|
|
48
46
|
|
|
49
47
|
## Programmatic API
|
|
50
48
|
|
package/lib/index.js
CHANGED
|
@@ -14,13 +14,6 @@ const BINARY_MAP = {
|
|
|
14
14
|
'linux x64': 'piperace-linux-amd64',
|
|
15
15
|
'win32 x64': 'piperace-client.exe',
|
|
16
16
|
'win32 ia32': 'piperace-client-386.exe'
|
|
17
|
-
},
|
|
18
|
-
gui: {
|
|
19
|
-
'darwin arm64': 'piperace-gui-darwin-arm64',
|
|
20
|
-
'darwin x64': 'piperace-gui-darwin-amd64',
|
|
21
|
-
'linux arm64': 'piperace-gui-linux-arm64',
|
|
22
|
-
'linux x64': 'piperace-gui-linux-amd64',
|
|
23
|
-
'win32 x64': 'piperace-gui.exe'
|
|
24
17
|
}
|
|
25
18
|
};
|
|
26
19
|
|
|
@@ -29,8 +22,7 @@ function platformKey() {
|
|
|
29
22
|
}
|
|
30
23
|
|
|
31
24
|
function binaryPathFor(kind) {
|
|
32
|
-
|
|
33
|
-
return path.join(BIN_DIR, name + (os.platform() === 'win32' ? '.exe' : ''));
|
|
25
|
+
return path.join(BIN_DIR, 'piperace' + (os.platform() === 'win32' ? '.exe' : ''));
|
|
34
26
|
}
|
|
35
27
|
|
|
36
28
|
function binaryExists(binaryPath) {
|
|
@@ -59,18 +51,13 @@ function getBinaryPath(kind = 'cli') {
|
|
|
59
51
|
return binaryPath;
|
|
60
52
|
}
|
|
61
53
|
|
|
62
|
-
// Ensure the binary is present, downloading it on demand.
|
|
63
|
-
// fetched during `npm install`, so its first launch lazily downloads it here.
|
|
54
|
+
// Ensure the binary is present, downloading it on demand.
|
|
64
55
|
async function ensureBinaryPath(kind = 'cli') {
|
|
65
56
|
const binaryPath = binaryPathFor(kind);
|
|
66
57
|
if (binaryExists(binaryPath)) return binaryPath;
|
|
67
58
|
|
|
68
|
-
if (kind === 'gui') {
|
|
69
|
-
console.log('[piperace] First GUI launch — downloading the desktop binary (one-time)...');
|
|
70
|
-
}
|
|
71
|
-
|
|
72
59
|
// Reuse the installer's download/retry logic. The require.main guard in
|
|
73
|
-
// install.js keeps this from re-running the
|
|
60
|
+
// install.js keeps this from re-running the install as a side effect.
|
|
74
61
|
const { installBinary } = require('../scripts/install.js');
|
|
75
62
|
await installBinary(kind);
|
|
76
63
|
|
|
@@ -81,8 +68,7 @@ async function ensureBinaryPath(kind = 'cli') {
|
|
|
81
68
|
}
|
|
82
69
|
|
|
83
70
|
async function run(args = [], options = {}) {
|
|
84
|
-
const
|
|
85
|
-
const binaryPath = await ensureBinaryPath(kind);
|
|
71
|
+
const binaryPath = await ensureBinaryPath('cli');
|
|
86
72
|
const child = spawn(binaryPath, args, {
|
|
87
73
|
stdio: options.stdio || 'inherit',
|
|
88
74
|
// Mark the install channel so the binary can show an npm-specific update
|
package/package.json
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@piperace/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.5.10",
|
|
4
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
|
-
"piperace": "bin/piperace"
|
|
8
|
-
"piperace-gui": "bin/piperace-gui"
|
|
7
|
+
"piperace": "bin/piperace"
|
|
9
8
|
},
|
|
10
9
|
"scripts": {
|
|
11
10
|
"postinstall": "node scripts/install.js",
|
package/scripts/install.js
CHANGED
|
@@ -11,6 +11,15 @@ const VERSION = require('../package.json').version;
|
|
|
11
11
|
const ROOT = path.join(__dirname, '..');
|
|
12
12
|
const BIN_DIR = path.join(ROOT, 'binaries');
|
|
13
13
|
|
|
14
|
+
// Where published release artifacts live: <BASE_URL>/<version>/<file> and
|
|
15
|
+
// <BASE_URL>/latest/<file>, uploaded by `mage deploy` (see magefile.go). This
|
|
16
|
+
// is S3 directly rather than the backend — the binaries are ~110 MB a release
|
|
17
|
+
// and have no business travelling through the app servers. Keep the default in
|
|
18
|
+
// sync with config/environments/base.go (DownloadsURL), which is the same root
|
|
19
|
+
// the Go CLI uses to fetch the menu-bar app.
|
|
20
|
+
const BASE_URL = (process.env.PIPERACE_DOWNLOAD_URL ||
|
|
21
|
+
'https://piperace-downloads.s3.us-east-1.amazonaws.com').replace(/\/+$/, '');
|
|
22
|
+
|
|
14
23
|
const DOWNLOAD_TIMEOUT_MS = 30000; // per-attempt socket timeout
|
|
15
24
|
const DOWNLOAD_RETRIES = 2; // extra attempts after the first
|
|
16
25
|
|
|
@@ -23,13 +32,6 @@ const BINARY_MAP = {
|
|
|
23
32
|
'linux x64': 'piperace-linux-amd64',
|
|
24
33
|
'win32 x64': 'piperace-client.exe',
|
|
25
34
|
'win32 ia32': 'piperace-client-386.exe'
|
|
26
|
-
},
|
|
27
|
-
gui: {
|
|
28
|
-
'darwin arm64': 'piperace-gui-darwin-arm64',
|
|
29
|
-
'darwin x64': 'piperace-gui-darwin-amd64',
|
|
30
|
-
'linux arm64': 'piperace-gui-linux-arm64',
|
|
31
|
-
'linux x64': 'piperace-gui-linux-amd64',
|
|
32
|
-
'win32 x64': 'piperace-gui.exe'
|
|
33
35
|
}
|
|
34
36
|
};
|
|
35
37
|
|
|
@@ -44,7 +46,7 @@ function binaryName(kind = 'cli') {
|
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
function finalBinaryName(kind = 'cli') {
|
|
47
|
-
return
|
|
49
|
+
return 'piperace';
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
function finalBinaryPath(kind = 'cli') {
|
|
@@ -168,9 +170,8 @@ async function installBinary(kind = 'cli') {
|
|
|
168
170
|
return;
|
|
169
171
|
}
|
|
170
172
|
|
|
171
|
-
// 2. Fall back to downloading
|
|
172
|
-
const
|
|
173
|
-
const url = `${baseUrl}/${VERSION}/${sourceName}`;
|
|
173
|
+
// 2. Fall back to downloading the published release.
|
|
174
|
+
const url = `${BASE_URL}/${VERSION}/${sourceName}`;
|
|
174
175
|
console.log(`[piperace] Downloading ${kind} binary from ${url}`);
|
|
175
176
|
|
|
176
177
|
try {
|
|
@@ -181,7 +182,7 @@ async function installBinary(kind = 'cli') {
|
|
|
181
182
|
} catch (err) {
|
|
182
183
|
try { fs.unlinkSync(target); } catch {}
|
|
183
184
|
console.error(`[piperace] Failed to download ${kind} binary: ${err.message}`);
|
|
184
|
-
console.error(`[piperace] You can manually download ${sourceName} from ${
|
|
185
|
+
console.error(`[piperace] You can manually download ${sourceName} from ${BASE_URL} and place it at ${target}`);
|
|
185
186
|
// Do not fail npm install; the launcher will retry lazily and print a
|
|
186
187
|
// helpful message if the binary is still missing at run time.
|
|
187
188
|
}
|
|
@@ -196,20 +197,167 @@ function removeQuarantine(filePath) {
|
|
|
196
197
|
}
|
|
197
198
|
}
|
|
198
199
|
|
|
200
|
+
// macOS-only: also install the packaged menu-bar app (Piperace.app, built by
|
|
201
|
+
// `mage tray:mac*` — see magefile.go) into /Applications, so npm users get
|
|
202
|
+
// the same double-clickable, Spotlight-launchable icon the CLI itself
|
|
203
|
+
// installs lazily for raw-binary users (pkg/trayapp). Best-effort: never
|
|
204
|
+
// throws, never fails the npm install.
|
|
205
|
+
const TRAY_ARCH_MAP = { arm64: 'arm64', x64: 'amd64' };
|
|
206
|
+
|
|
207
|
+
function trayAppDest() {
|
|
208
|
+
for (const dir of ['/Applications', path.join(os.homedir(), 'Applications')]) {
|
|
209
|
+
if (canWriteDir(dir)) return path.join(dir, 'Piperace.app');
|
|
210
|
+
}
|
|
211
|
+
return path.join(os.homedir(), 'Applications', 'Piperace.app');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function canWriteDir(dir) {
|
|
215
|
+
try {
|
|
216
|
+
if (!fs.existsSync(dir)) {
|
|
217
|
+
if (dir === '/Applications') return false; // never create the root Applications folder itself
|
|
218
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
219
|
+
}
|
|
220
|
+
const probe = path.join(dir, `.piperace-write-test-${process.pid}`);
|
|
221
|
+
fs.writeFileSync(probe, '');
|
|
222
|
+
fs.unlinkSync(probe);
|
|
223
|
+
return true;
|
|
224
|
+
} catch {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// installedTrayVersion reads CFBundleShortVersionString from an installed
|
|
230
|
+
// bundle ('' when it is absent or unreadable — a hand-made bundle we leave be).
|
|
231
|
+
function installedTrayVersion(dest) {
|
|
232
|
+
try {
|
|
233
|
+
return execSync(`defaults read "${path.join(dest, 'Contents', 'Info')}" CFBundleShortVersionString`,
|
|
234
|
+
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
235
|
+
} catch {
|
|
236
|
+
return '';
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function trayAppRunning(dest) {
|
|
241
|
+
try {
|
|
242
|
+
execSync(`pgrep -f "${path.join(dest, 'Contents', 'MacOS')}/"`, { stdio: 'ignore' });
|
|
243
|
+
return true;
|
|
244
|
+
} catch {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// quitTrayApp stops a running menu-bar app so its bundle can be replaced.
|
|
250
|
+
// Without this an upgrade leaves the old icon running beside the new one —
|
|
251
|
+
// two icons, two dashboards, both fighting for port 443.
|
|
252
|
+
function quitTrayApp(dest) {
|
|
253
|
+
const pattern = `${path.join(dest, 'Contents', 'MacOS')}/`;
|
|
254
|
+
try { execSync(`pkill -f "${pattern}"`, { stdio: 'ignore' }); } catch {}
|
|
255
|
+
for (let i = 0; i < 25 && trayAppRunning(dest); i++) {
|
|
256
|
+
try { execSync('sleep 0.2'); } catch {}
|
|
257
|
+
}
|
|
258
|
+
if (trayAppRunning(dest)) {
|
|
259
|
+
try { execSync(`pkill -9 -f "${pattern}"`, { stdio: 'ignore' }); } catch {}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function installMacTrayApp() {
|
|
264
|
+
if (os.platform() !== 'darwin') return;
|
|
265
|
+
|
|
266
|
+
const dest = trayAppDest();
|
|
267
|
+
const installed = fs.existsSync(dest) ? installedTrayVersion(dest) : null;
|
|
268
|
+
// Present and already this version (or unreadable, i.e. not ours) — leave it.
|
|
269
|
+
if (installed !== null && (installed === VERSION || installed === '')) return;
|
|
270
|
+
|
|
271
|
+
const arch = TRAY_ARCH_MAP[os.arch()];
|
|
272
|
+
if (!arch) {
|
|
273
|
+
console.warn(`[piperace] No prebuilt menu-bar app for darwin/${os.arch()}; skipping.`);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const zipName = `Piperace-darwin-${arch}.app.zip`;
|
|
277
|
+
const tmpZip = path.join(os.tmpdir(), `piperace-tray-${process.pid}.app.zip`);
|
|
278
|
+
|
|
279
|
+
let downloaded = false;
|
|
280
|
+
for (const dir of [VERSION, 'latest']) {
|
|
281
|
+
const url = `${BASE_URL}/${dir}/${zipName}`;
|
|
282
|
+
try {
|
|
283
|
+
console.log(`[piperace] Downloading menu-bar app from ${url}`);
|
|
284
|
+
await downloadWithRetry(url, tmpZip);
|
|
285
|
+
downloaded = true;
|
|
286
|
+
break;
|
|
287
|
+
} catch (err) {
|
|
288
|
+
console.warn(`[piperace] ${url} unavailable (${err.message})`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (!downloaded) {
|
|
292
|
+
console.warn('[piperace] Could not download the menu-bar app — the CLI still works fine; `piperace ui` retries this automatically later.');
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
let tmpDir;
|
|
297
|
+
try {
|
|
298
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'piperace-tray-'));
|
|
299
|
+
// ditto (not a JS zip lib) so the code signature ditto/codesign embedded
|
|
300
|
+
// at build time survives extraction intact.
|
|
301
|
+
execSync(`ditto -x -k "${tmpZip}" "${tmpDir}"`, { stdio: 'ignore' });
|
|
302
|
+
const extracted = path.join(tmpDir, 'Piperace.app');
|
|
303
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
304
|
+
|
|
305
|
+
// Stop the old icon before swapping the bundle, and put it back after, so
|
|
306
|
+
// an upgrade hands over instead of running two copies side by side.
|
|
307
|
+
const wasRunning = fs.existsSync(dest) && trayAppRunning(dest);
|
|
308
|
+
if (wasRunning) quitTrayApp(dest);
|
|
309
|
+
|
|
310
|
+
// `cp -R` into an existing app directory copies INSIDE it (leaving a
|
|
311
|
+
// nested Piperace.app/Piperace.app that macOS ignores while it keeps
|
|
312
|
+
// launching the stale outer bundle), so move the old one aside first and
|
|
313
|
+
// only drop it once the new one is in place.
|
|
314
|
+
const aside = `${dest}.replaced`;
|
|
315
|
+
fs.rmSync(aside, { recursive: true, force: true });
|
|
316
|
+
if (fs.existsSync(dest)) fs.renameSync(dest, aside);
|
|
317
|
+
try {
|
|
318
|
+
execSync(`cp -R "${extracted}" "${dest}"`, { stdio: 'ignore' });
|
|
319
|
+
} catch (err) {
|
|
320
|
+
if (fs.existsSync(aside)) fs.renameSync(aside, dest); // put the working one back
|
|
321
|
+
throw err;
|
|
322
|
+
}
|
|
323
|
+
fs.rmSync(aside, { recursive: true, force: true });
|
|
324
|
+
removeQuarantine(dest);
|
|
325
|
+
registerWithLaunchServices(dest);
|
|
326
|
+
if (wasRunning) {
|
|
327
|
+
try { execSync(`open -g -a "${dest}"`, { stdio: 'ignore' }); } catch {}
|
|
328
|
+
}
|
|
329
|
+
console.log(`[piperace] Installed ${dest} — find it in Spotlight/Applications for a menu-bar icon.`);
|
|
330
|
+
} catch (err) {
|
|
331
|
+
console.warn(`[piperace] Failed to install the menu-bar app: ${err.message}`);
|
|
332
|
+
} finally {
|
|
333
|
+
try { fs.unlinkSync(tmpZip); } catch {}
|
|
334
|
+
if (tmpDir) { try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {} }
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// registerWithLaunchServices makes Finder, Spotlight and Launchpad pick the
|
|
339
|
+
// bundle (and its icon) up now instead of whenever the system next rescans.
|
|
340
|
+
function registerWithLaunchServices(dest) {
|
|
341
|
+
const lsregister = '/System/Library/Frameworks/CoreServices.framework/Frameworks/' +
|
|
342
|
+
'LaunchServices.framework/Support/lsregister';
|
|
343
|
+
try { execSync(`"${lsregister}" -f -R -trusted "${dest}"`, { stdio: 'ignore' }); } catch {}
|
|
344
|
+
try { fs.utimesSync(dest, new Date(), new Date()); } catch {}
|
|
345
|
+
}
|
|
346
|
+
|
|
199
347
|
module.exports = {
|
|
200
348
|
installBinary,
|
|
349
|
+
installMacTrayApp,
|
|
201
350
|
binaryName,
|
|
202
351
|
finalBinaryPath,
|
|
203
352
|
platformKey,
|
|
204
353
|
BINARY_MAP
|
|
205
354
|
};
|
|
206
355
|
|
|
207
|
-
//
|
|
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.
|
|
356
|
+
// Fetch the CLI binary (and, on macOS, the menu-bar app) at install time.
|
|
210
357
|
if (require.main === module) {
|
|
211
358
|
// Never let a failure here break `npm install`; the launcher retries lazily.
|
|
212
359
|
Promise.resolve()
|
|
213
360
|
.then(() => installBinary('cli'))
|
|
361
|
+
.then(() => installMacTrayApp())
|
|
214
362
|
.catch((err) => console.error(`[piperace] install skipped: ${err.message}`));
|
|
215
363
|
}
|