@piperace/cli 0.4.1 → 1.5.11

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
@@ -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-gui # launch the desktop app
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
- Both the CLI and the desktop GUI are available for:
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 (CLI only) |
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/downloads`).
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
- const name = kind === 'gui' ? 'piperace-gui' : 'piperace';
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. The GUI binary is not
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 CLI install as a side effect.
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 kind = options.gui ? 'gui' : 'cli';
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": "0.4.1",
3
+ "version": "1.5.11",
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",
@@ -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 kind === 'gui' ? 'piperace-gui' : 'piperace';
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 from the public CDN.
172
- const baseUrl = process.env.PIPERACE_DOWNLOAD_URL || 'https://piperace.com/downloads';
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 ${baseUrl} and place it at ${target}`);
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
- // 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.
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
  }
package/bin/piperace-gui DELETED
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env node
2
- const { run } = require('../lib/index.js');
3
-
4
- run(process.argv.slice(2), { gui: true }).catch((err) => {
5
- console.error(err.message);
6
- process.exit(1);
7
- });