@miraj181/ipingyou 2.1.9 → 2.1.18

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