@miraj181/ipingyou 1.0.1 → 2.0.0
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 +36 -96
- package/package.json +11 -4
- package/src/cli.js +84 -4
- package/src/lib/chat.js +333 -0
- package/src/lib/cleanup.js +78 -0
- package/src/lib/config.js +41 -0
- package/src/lib/crypto.js +23 -14
- package/src/lib/platform.js +415 -53
- package/src/modes/client.js +451 -58
- package/src/modes/host.js +416 -119
- package/src/server.js +0 -143
package/src/lib/platform.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ============================================================
|
|
3
|
-
* Platform Detection & Dependency
|
|
3
|
+
* Platform Detection & Dependency Auto-Bootstrapper
|
|
4
4
|
* ============================================================
|
|
5
|
-
* Detects OS,
|
|
6
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
123
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
}
|