@miraj181/ipingyou 1.0.2 → 2.0.1

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.
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * ============================================================
3
- * Platform Detection & Dependency Checker
3
+ * Platform Detection & Dependency Auto-Bootstrapper
4
4
  * ============================================================
5
- * Detects OS, checks for ssh/cloudflared, and provides
6
- * automated installation guidance per platform.
5
+ * Detects OS, ensures download tools exist, then auto-installs
6
+ * ssh/cloudflared if missing. Bootstraps curl/wget first if
7
+ * even those are absent.
7
8
  * ============================================================
8
9
  */
9
10
 
@@ -11,14 +12,20 @@ import { execaCommand } from 'execa';
11
12
  import chalk from 'chalk';
12
13
  import ora from 'ora';
13
14
  import os from 'node:os';
15
+ import { createWriteStream } from 'node:fs';
16
+ import { pipeline } from 'node:stream/promises';
17
+ import { chmod, mkdir, stat, rename } from 'node:fs/promises';
18
+ import { join } from 'node:path';
19
+
20
+ // ─── OS Detection ────────────────────────────────────────────
14
21
 
15
22
  /**
16
23
  * Detect the current operating system.
17
- * @returns {{ platform: string, isLinux: boolean, isMac: boolean, isWindows: boolean, distro: string|null }}
24
+ * @returns {{ platform: string, isLinux: boolean, isMac: boolean, isWindows: boolean, distro: string|null, arch: string, hostname: string }}
18
25
  */
19
26
  export function detectOS() {
20
27
  const platform = process.platform;
21
- const result = {
28
+ return {
22
29
  platform,
23
30
  isLinux: platform === 'linux',
24
31
  isMac: platform === 'darwin',
@@ -27,8 +34,6 @@ export function detectOS() {
27
34
  arch: os.arch(),
28
35
  hostname: os.hostname(),
29
36
  };
30
-
31
- return result;
32
37
  }
33
38
 
34
39
  /**
@@ -52,6 +57,8 @@ export async function detectLinuxDistro() {
52
57
  return 'unknown';
53
58
  }
54
59
 
60
+ // ─── Utility Helpers ─────────────────────────────────────────
61
+
55
62
  /**
56
63
  * Check if a command exists on PATH.
57
64
  * @param {string} cmd
@@ -76,6 +83,333 @@ export async function hasSudo() {
76
83
  return commandExists('sudo');
77
84
  }
78
85
 
86
+ /**
87
+ * Run a shell command with a spinner.
88
+ * @param {string} label
89
+ * @param {string} cmd
90
+ * @param {object} [opts]
91
+ * @returns {Promise<boolean>}
92
+ */
93
+ async function runWithSpinner(label, cmd, opts = {}) {
94
+ const spinner = ora(label).start();
95
+ try {
96
+ await execaCommand(cmd, { stdio: 'pipe', reject: true, ...opts });
97
+ spinner.succeed(label.replace('...', '') + chalk.green(' ✓'));
98
+ return true;
99
+ } catch (err) {
100
+ spinner.fail(label.replace('...', '') + chalk.red(` ✗ ${err.shortMessage || err.message}`));
101
+ return false;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Resolve the CPU architecture for download URLs.
107
+ * @returns {string}
108
+ */
109
+ function resolveArch() {
110
+ const arch = os.arch();
111
+ switch (arch) {
112
+ case 'x64': return 'amd64';
113
+ case 'arm64': return 'arm64';
114
+ case 'arm': return 'arm';
115
+ default: return arch;
116
+ }
117
+ }
118
+
119
+ // ─── Download Tool Bootstrap ─────────────────────────────────
120
+
121
+ /**
122
+ * Find the first available download tool.
123
+ * @returns {Promise<string|null>} 'curl' | 'wget' | 'powershell' | null
124
+ */
125
+ async function findDownloader() {
126
+ for (const tool of ['curl', 'wget']) {
127
+ if (await commandExists(tool)) return tool;
128
+ }
129
+ // Windows fallback: PowerShell is almost always present
130
+ if (process.platform === 'win32') {
131
+ if (await commandExists('powershell')) return 'powershell';
132
+ }
133
+ return null;
134
+ }
135
+
136
+ /**
137
+ * Ensure at least one download tool (curl/wget) is installed.
138
+ * If none exist, install curl using the system package manager.
139
+ * @returns {Promise<string>} The download tool name that is now available.
140
+ * @throws {Error} If no downloader can be provisioned.
141
+ */
142
+ async function ensureDownloader() {
143
+ let tool = await findDownloader();
144
+ if (tool) return tool;
145
+
146
+ console.log(chalk.yellow('\n ⚠️ No download tool found (curl, wget). Bootstrapping curl...\n'));
147
+
148
+ const osInfo = detectOS();
149
+
150
+ if (osInfo.isLinux) {
151
+ const distro = await detectLinuxDistro();
152
+ const sudo = (await hasSudo()) ? 'sudo ' : '';
153
+
154
+ const installCmds = {
155
+ debian: `${sudo}apt-get update -qq && ${sudo}apt-get install -y curl`,
156
+ arch: `${sudo}pacman -Sy --noconfirm curl`,
157
+ fedora: `${sudo}dnf install -y curl`,
158
+ };
159
+
160
+ const cmd = installCmds[distro];
161
+ if (cmd) {
162
+ const ok = await runWithSpinner(` Installing ${chalk.cyan('curl')} via ${distro} package manager...`, cmd);
163
+ if (ok) return 'curl';
164
+ }
165
+ } else if (osInfo.isMac) {
166
+ // macOS: curl ships with Xcode CLT. If truly missing, try xcode-select.
167
+ const ok = await runWithSpinner(
168
+ ` Installing ${chalk.cyan('Xcode Command Line Tools')} (includes curl)...`,
169
+ 'xcode-select --install'
170
+ );
171
+ if (ok || await commandExists('curl')) return 'curl';
172
+ } else if (osInfo.isWindows) {
173
+ // Windows: try winget to install curl
174
+ if (await commandExists('winget')) {
175
+ const ok = await runWithSpinner(
176
+ ` Installing ${chalk.cyan('curl')} via winget...`,
177
+ 'winget install --id cURL.cURL --accept-source-agreements --accept-package-agreements -e'
178
+ );
179
+ if (ok) return 'curl';
180
+ }
181
+ // PowerShell fallback is always available on modern Windows
182
+ if (await commandExists('powershell')) return 'powershell';
183
+ }
184
+
185
+ throw new Error(
186
+ 'Could not provision a download tool (curl/wget). Please install curl manually and retry.'
187
+ );
188
+ }
189
+
190
+ // ─── Download Helpers ────────────────────────────────────────
191
+
192
+ /**
193
+ * Download a file using whichever tool is available.
194
+ * @param {string} url
195
+ * @param {string} destPath
196
+ * @param {string} downloader 'curl' | 'wget' | 'powershell'
197
+ * @returns {Promise<boolean>}
198
+ */
199
+ async function downloadFile(url, destPath, downloader) {
200
+ const cmds = {
201
+ curl: `curl -fsSL -o "${destPath}" "${url}"`,
202
+ wget: `wget -q -O "${destPath}" "${url}"`,
203
+ powershell: `powershell -Command "Invoke-WebRequest -Uri '${url}' -OutFile '${destPath}'"`,
204
+ };
205
+
206
+ return runWithSpinner(
207
+ ` Downloading ${chalk.cyan(url.split('/').pop())}...`,
208
+ cmds[downloader]
209
+ );
210
+ }
211
+
212
+ // ─── Cloudflared Installer ───────────────────────────────────
213
+
214
+ /**
215
+ * Build the cloudflared download URL for the current platform.
216
+ * @param {{ isLinux: boolean, isMac: boolean, isWindows: boolean }} osInfo
217
+ * @returns {{ url: string, filename: string }|null}
218
+ */
219
+ function cloudflaredDownloadUrl(osInfo) {
220
+ const arch = resolveArch();
221
+ const base = 'https://github.com/cloudflare/cloudflared/releases/latest/download';
222
+
223
+ if (osInfo.isLinux) {
224
+ return { url: `${base}/cloudflared-linux-${arch}`, filename: 'cloudflared' };
225
+ }
226
+ if (osInfo.isMac) {
227
+ // Homebrew is preferred on macOS — but provide direct download as fallback
228
+ return { url: `${base}/cloudflared-darwin-${arch}.tgz`, filename: 'cloudflared-darwin.tgz' };
229
+ }
230
+ if (osInfo.isWindows) {
231
+ return { url: `${base}/cloudflared-windows-${arch}.exe`, filename: 'cloudflared.exe' };
232
+ }
233
+ return null;
234
+ }
235
+
236
+ /**
237
+ * Install cloudflared binary from official GitHub releases.
238
+ * @param {{ isLinux: boolean, isMac: boolean, isWindows: boolean }} osInfo
239
+ * @param {string} downloader
240
+ * @returns {Promise<boolean>}
241
+ */
242
+ async function installCloudflared(osInfo, downloader) {
243
+ // ── macOS: prefer Homebrew ─────────────────────────────────
244
+ if (osInfo.isMac && await commandExists('brew')) {
245
+ console.log(chalk.yellow(' 🍺 Installing cloudflared via Homebrew...'));
246
+ return runWithSpinner(
247
+ ` ${chalk.cyan('brew install cloudflared')}...`,
248
+ 'brew install cloudflared'
249
+ );
250
+ }
251
+
252
+ // ── Linux: try native package managers first ───────────────
253
+ if (osInfo.isLinux) {
254
+ const distro = await detectLinuxDistro();
255
+ const sudo = (await hasSudo()) ? 'sudo ' : '';
256
+
257
+ // Debian/Ubuntu: official Cloudflare apt repo
258
+ if (distro === 'debian') {
259
+ const aptOk = await runWithSpinner(
260
+ ` Adding Cloudflare APT repo & installing ${chalk.cyan('cloudflared')}...`,
261
+ [
262
+ `${sudo}mkdir -p --mode=0755 /usr/share/keyrings`,
263
+ `curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | ${sudo}tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null`,
264
+ `echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main" | ${sudo}tee /etc/apt/sources.list.d/cloudflared.list`,
265
+ `${sudo}apt-get update -qq`,
266
+ `${sudo}apt-get install -y cloudflared`,
267
+ ].join(' && ')
268
+ );
269
+ if (aptOk) return true;
270
+ // Fall through to binary download if apt method fails
271
+ }
272
+ }
273
+
274
+ // ── Windows: try winget first ──────────────────────────────
275
+ if (osInfo.isWindows && await commandExists('winget')) {
276
+ const ok = await runWithSpinner(
277
+ ` Installing ${chalk.cyan('cloudflared')} via winget...`,
278
+ 'winget install --id Cloudflare.cloudflared --accept-source-agreements --accept-package-agreements -e'
279
+ );
280
+ if (ok) return true;
281
+ }
282
+
283
+ // ── Fallback: direct binary download ───────────────────────
284
+ const dl = cloudflaredDownloadUrl(osInfo);
285
+ if (!dl) {
286
+ console.log(chalk.red(' ✗ Unsupported platform for cloudflared auto-install.'));
287
+ return false;
288
+ }
289
+
290
+ const tmpDir = join(os.tmpdir(), 'ipingyou-bootstrap');
291
+ try { await mkdir(tmpDir, { recursive: true }); } catch { /* exists */ }
292
+ const destPath = join(tmpDir, dl.filename);
293
+
294
+ const ok = await downloadFile(dl.url, destPath, downloader);
295
+ if (!ok) return false;
296
+
297
+ // Handle macOS .tgz extraction
298
+ if (dl.filename.endsWith('.tgz')) {
299
+ const extractOk = await runWithSpinner(
300
+ ` Extracting ${chalk.cyan('cloudflared')} archive...`,
301
+ `tar -xzf "${destPath}" -C "${tmpDir}"`
302
+ );
303
+ if (!extractOk) return false;
304
+ }
305
+
306
+ // Move binary to a PATH location
307
+ if (osInfo.isLinux || osInfo.isMac) {
308
+ const binaryPath = dl.filename.endsWith('.tgz')
309
+ ? join(tmpDir, 'cloudflared')
310
+ : destPath;
311
+
312
+ try { await chmod(binaryPath, 0o755); } catch { /* ignore */ }
313
+
314
+ const sudo = (await hasSudo()) ? 'sudo ' : '';
315
+ const installPath = '/usr/local/bin/cloudflared';
316
+
317
+ return runWithSpinner(
318
+ ` Moving ${chalk.cyan('cloudflared')} to ${chalk.dim(installPath)}...`,
319
+ `${sudo}mv "${binaryPath}" "${installPath}"`
320
+ );
321
+ }
322
+
323
+ if (osInfo.isWindows) {
324
+ // Place in user's local app data
325
+ const winDir = join(os.homedir(), 'AppData', 'Local', 'cloudflared');
326
+ try { await mkdir(winDir, { recursive: true }); } catch { /* exists */ }
327
+ const finalPath = join(winDir, 'cloudflared.exe');
328
+
329
+ try {
330
+ await rename(destPath, finalPath);
331
+ console.log(chalk.green(` ✓ cloudflared installed at ${chalk.dim(finalPath)}`));
332
+ console.log(chalk.yellow(` ⚠️ Add ${chalk.dim(winDir)} to your PATH to use cloudflared globally.`));
333
+ return true;
334
+ } catch (err) {
335
+ console.log(chalk.red(` ✗ Failed to move cloudflared: ${err.message}`));
336
+ return false;
337
+ }
338
+ }
339
+
340
+ return false;
341
+ }
342
+
343
+ // ─── OpenSSH Installer ───────────────────────────────────────
344
+
345
+ /**
346
+ * Install OpenSSH on the current platform.
347
+ * @param {{ isLinux: boolean, isMac: boolean, isWindows: boolean }} osInfo
348
+ * @returns {Promise<boolean>}
349
+ */
350
+ async function installOpenSSH(osInfo) {
351
+ if (osInfo.isMac) {
352
+ // macOS ships with ssh; if missing, it means Xcode CLT isn't installed
353
+ console.log(chalk.dim(' ℹ️ macOS ships with SSH by default.'));
354
+ console.log(chalk.dim(' If missing, enable in: System Preferences → Sharing → Remote Login'));
355
+ // Try xcode-select as last resort
356
+ if (!(await commandExists('ssh'))) {
357
+ return runWithSpinner(
358
+ ` Installing ${chalk.cyan('Xcode Command Line Tools')} (includes ssh)...`,
359
+ 'xcode-select --install'
360
+ );
361
+ }
362
+ return true;
363
+ }
364
+
365
+ if (osInfo.isLinux) {
366
+ const distro = await detectLinuxDistro();
367
+ const sudo = (await hasSudo()) ? 'sudo ' : '';
368
+
369
+ const cmds = {
370
+ debian: `${sudo}apt-get update -qq && ${sudo}apt-get install -y openssh-client openssh-server`,
371
+ arch: `${sudo}pacman -Sy --noconfirm openssh`,
372
+ fedora: `${sudo}dnf install -y openssh-clients openssh-server`,
373
+ };
374
+
375
+ const cmd = cmds[distro];
376
+ if (cmd) {
377
+ return runWithSpinner(` Installing ${chalk.cyan('openssh')} via ${distro} package manager...`, cmd);
378
+ }
379
+
380
+ console.log(chalk.red(' ✗ Unknown Linux distro — please install openssh manually.'));
381
+ return false;
382
+ }
383
+
384
+ if (osInfo.isWindows) {
385
+ // Try winget first
386
+ if (await commandExists('winget')) {
387
+ const ok = await runWithSpinner(
388
+ ` Installing ${chalk.cyan('OpenSSH')} via winget...`,
389
+ 'winget install --id Microsoft.OpenSSH.Client --accept-source-agreements --accept-package-agreements -e'
390
+ );
391
+ if (ok) return true;
392
+ }
393
+
394
+ // Fallback: PowerShell Add-WindowsCapability
395
+ if (await commandExists('powershell')) {
396
+ const ok = await runWithSpinner(
397
+ ` Installing ${chalk.cyan('OpenSSH')} via PowerShell...`,
398
+ 'powershell -Command "Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0"'
399
+ );
400
+ if (ok) return true;
401
+ }
402
+
403
+ console.log(chalk.yellow(' ⚠️ Could not auto-install OpenSSH on Windows.'));
404
+ console.log(chalk.dim(' Manual: Settings → Apps → Optional Features → OpenSSH Client'));
405
+ return false;
406
+ }
407
+
408
+ return false;
409
+ }
410
+
411
+ // ─── Main Dependency Check ───────────────────────────────────
412
+
79
413
  /**
80
414
  * Install a dependency on the current platform.
81
415
  * @param {string} pkg — package name (e.g. 'openssh-server', 'cloudflared')
@@ -108,7 +442,14 @@ export async function installDependency(pkg, distro) {
108
442
  }
109
443
 
110
444
  /**
111
- * Run full dependency check for ssh and cloudflared.
445
+ * Run full dependency check with auto-bootstrap.
446
+ *
447
+ * Pipeline:
448
+ * 1. Detect OS
449
+ * 2. Check ssh + cloudflared
450
+ * 3. If anything is missing → ensure a download tool exists (bootstrap curl if needed)
451
+ * 4. Auto-install missing deps using the best method per platform
452
+ *
112
453
  * @returns {Promise<{ ssh: boolean, cloudflared: boolean }>}
113
454
  */
114
455
  export async function checkDependencies() {
@@ -119,61 +460,82 @@ export async function checkDependencies() {
119
460
  console.log(chalk.bold(' 🔍 Dependency Check'));
120
461
  console.log(chalk.dim(' ─────────────────────────────────'));
121
462
 
122
- // Check SSH
123
- const sshCmd = osInfo.isWindows ? 'ssh' : 'ssh';
124
- results.ssh = await commandExists(sshCmd);
463
+ // ── Step 1: Probe for existing binaries ─────────────────────
464
+ results.ssh = await commandExists('ssh');
125
465
  console.log(` ${results.ssh ? chalk.green('✓') : chalk.red('✗')} ssh ${results.ssh ? chalk.dim('found') : chalk.red('missing')}`);
126
466
 
127
- // Check cloudflared
128
467
  results.cloudflared = await commandExists('cloudflared');
129
468
  console.log(` ${results.cloudflared ? chalk.green('✓') : chalk.red('✗')} cloudflared ${results.cloudflared ? chalk.dim('found') : chalk.red('missing')}`);
130
469
 
131
470
  console.log('');
132
471
 
133
- // Auto-install logic for missing deps
472
+ // ── Step 2: Nothing missing? We're done ─────────────────────
473
+ if (results.ssh && results.cloudflared) {
474
+ console.log(chalk.green(' ✅ All dependencies satisfied!\n'));
475
+ return results;
476
+ }
477
+
478
+ // ── Step 3: Bootstrap a download tool ───────────────────────
479
+ let downloader;
480
+ try {
481
+ console.log(chalk.bold(' 🔧 Bootstrapping download tools...'));
482
+
483
+ // Check what download tools exist
484
+ const hasCurl = await commandExists('curl');
485
+ const hasWget = await commandExists('wget');
486
+ const hasWinget = osInfo.isWindows ? await commandExists('winget') : false;
487
+
488
+ console.log(` ${hasCurl ? chalk.green('✓') : chalk.red('✗')} curl ${hasCurl ? chalk.dim('found') : chalk.red('missing')}`);
489
+ console.log(` ${hasWget ? chalk.green('✓') : chalk.red('✗')} wget ${hasWget ? chalk.dim('found') : chalk.red('missing')}`);
490
+ if (osInfo.isWindows) {
491
+ console.log(` ${hasWinget ? chalk.green('✓') : chalk.red('✗')} winget ${hasWinget ? chalk.dim('found') : chalk.red('missing')}`);
492
+ }
493
+ console.log('');
494
+
495
+ downloader = await ensureDownloader();
496
+ console.log(chalk.green(` ✓ Using ${chalk.cyan(downloader)} as download tool\n`));
497
+ } catch (err) {
498
+ console.log(chalk.red(`\n ✗ ${err.message}`));
499
+ console.log(chalk.dim(' Cannot proceed with auto-installation.\n'));
500
+ return results;
501
+ }
502
+
503
+ // ── Step 4: Install missing dependencies ────────────────────
504
+ console.log(chalk.bold(' ⚡ Auto-installing missing dependencies...\n'));
505
+
506
+ if (!results.ssh) {
507
+ const installed = await installOpenSSH(osInfo);
508
+ results.ssh = installed || (await commandExists('ssh'));
509
+ console.log('');
510
+ }
511
+
512
+ if (!results.cloudflared) {
513
+ const installed = await installCloudflared(osInfo, downloader);
514
+ results.cloudflared = installed || (await commandExists('cloudflared'));
515
+ console.log('');
516
+ }
517
+
518
+ // ── Final report ────────────────────────────────────────────
519
+ console.log(chalk.dim(' ─────────────────────────────────'));
520
+ console.log(chalk.bold(' 📋 Final Status'));
521
+ console.log(` ${results.ssh ? chalk.green('✓') : chalk.red('✗')} ssh ${results.ssh ? chalk.green('ready') : chalk.red('unavailable')}`);
522
+ console.log(` ${results.cloudflared ? chalk.green('✓') : chalk.red('✗')} cloudflared ${results.cloudflared ? chalk.green('ready') : chalk.red('unavailable')}`);
523
+
134
524
  if (!results.ssh || !results.cloudflared) {
135
- if (osInfo.isLinux) {
136
- const distro = await detectLinuxDistro();
137
- const canSudo = await hasSudo();
138
-
139
- if (canSudo) {
140
- console.log(chalk.yellow(' Attempting automatic installation...'));
141
- console.log('');
142
-
143
- if (!results.ssh) {
144
- const sshPkg = distro === 'arch' ? 'openssh' : 'openssh-server';
145
- results.ssh = await installDependency(sshPkg, distro);
146
- }
147
-
148
- if (!results.cloudflared) {
149
- // cloudflared isn't always in default repos
150
- console.log(chalk.yellow(' ℹ️ cloudflared must be installed manually:'));
151
- console.log(chalk.dim(' https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/'));
152
- }
153
- } else {
154
- console.log(chalk.yellow(' ⚠️ sudo not available — cannot auto-install.'));
155
- console.log(chalk.dim(' Install ssh and cloudflared manually.'));
156
- }
157
- } else if (osInfo.isMac) {
158
- if (!results.cloudflared) {
159
- console.log(chalk.yellow(' ℹ️ Install cloudflared via Homebrew:'));
160
- console.log(chalk.cyan(' brew install cloudflared'));
161
- }
162
- if (!results.ssh) {
163
- console.log(chalk.dim(' ℹ️ macOS ships with SSH by default. Enable it in:'));
164
- console.log(chalk.dim(' System Preferences → Sharing → Remote Login'));
165
- }
166
- } else if (osInfo.isWindows) {
167
- console.log(chalk.yellow(' ⚠️ Windows detected — manual install required:'));
168
- if (!results.ssh) {
169
- console.log(chalk.cyan(' winget install Microsoft.OpenSSH.Client'));
170
- console.log(chalk.dim(' Or enable via: Settings → Apps → Optional Features → OpenSSH'));
171
- }
172
- if (!results.cloudflared) {
173
- console.log(chalk.cyan(' winget install Cloudflare.cloudflared'));
174
- }
525
+ console.log('');
526
+ console.log(chalk.yellow(' ⚠️ Some dependencies could not be installed automatically.'));
527
+ console.log(chalk.dim(' Please install them manually and re-run the tool.'));
528
+
529
+ if (!results.cloudflared) {
530
+ console.log(chalk.dim(' cloudflared https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/'));
175
531
  }
532
+ if (!results.ssh) {
533
+ console.log(chalk.dim(' openssh → https://www.openssh.com/portable.html'));
534
+ }
535
+ } else {
536
+ console.log(chalk.green('\n ✅ All dependencies satisfied!'));
176
537
  }
177
538
 
539
+ console.log('');
178
540
  return results;
179
541
  }