devtunnel-cli 3.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 +82 -0
- package/bin/README.md +40 -0
- package/package.json +56 -0
- package/src/core/RUN.js +41 -0
- package/src/core/index.js +394 -0
- package/src/core/proxy-server.js +72 -0
- package/src/core/setup-cloudflared.js +334 -0
- package/src/core/start.js +200 -0
- package/src/utils/folder-picker.js +140 -0
- package/src/utils/tunnel-helpers.js +35 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import https from 'https';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { dirname } from 'path';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = dirname(__filename);
|
|
10
|
+
|
|
11
|
+
const BIN_DIR = path.join(__dirname, '../../bin');
|
|
12
|
+
const CLOUDFLARED_VERSION = '2024.8.2'; // Latest stable version
|
|
13
|
+
|
|
14
|
+
// Binary URLs with multiple mirrors for reliability
|
|
15
|
+
const DOWNLOAD_URLS = {
|
|
16
|
+
win32: [
|
|
17
|
+
`https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-windows-amd64.exe`,
|
|
18
|
+
`https://cloudflared-releases.pages.dev/${CLOUDFLARED_VERSION}/cloudflared-windows-amd64.exe`
|
|
19
|
+
],
|
|
20
|
+
darwin: [
|
|
21
|
+
`https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-darwin-amd64`,
|
|
22
|
+
`https://cloudflared-releases.pages.dev/${CLOUDFLARED_VERSION}/cloudflared-darwin-amd64`
|
|
23
|
+
],
|
|
24
|
+
linux: [
|
|
25
|
+
`https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64`,
|
|
26
|
+
`https://cloudflared-releases.pages.dev/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64`
|
|
27
|
+
]
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Get platform display name
|
|
31
|
+
function getPlatformName() {
|
|
32
|
+
const platform = process.platform;
|
|
33
|
+
return platform === 'win32' ? 'Windows' : platform === 'darwin' ? 'macOS' : 'Linux';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getBinaryPath() {
|
|
37
|
+
const platform = process.platform;
|
|
38
|
+
const binName = platform === 'win32' ? 'cloudflared.exe' : 'cloudflared';
|
|
39
|
+
return path.join(BIN_DIR, platform, binName);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Check available disk space (basic check)
|
|
43
|
+
function hasEnoughDiskSpace() {
|
|
44
|
+
try {
|
|
45
|
+
const stats = fs.statfsSync ? fs.statfsSync(BIN_DIR) : null;
|
|
46
|
+
if (stats) {
|
|
47
|
+
const availableSpace = stats.bavail * stats.bsize;
|
|
48
|
+
const requiredSpace = 50 * 1024 * 1024; // 50MB
|
|
49
|
+
return availableSpace > requiredSpace;
|
|
50
|
+
}
|
|
51
|
+
return true; // Assume OK if we can't check
|
|
52
|
+
} catch {
|
|
53
|
+
return true; // Assume OK if check fails
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function downloadFile(url, dest, retryCount = 0) {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
// Create directory if needed
|
|
60
|
+
const dir = path.dirname(dest);
|
|
61
|
+
try {
|
|
62
|
+
if (!fs.existsSync(dir)) {
|
|
63
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
64
|
+
}
|
|
65
|
+
} catch (err) {
|
|
66
|
+
reject(new Error(`Cannot create directory: ${err.message}`));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Create temp file first
|
|
71
|
+
const tempDest = dest + '.download';
|
|
72
|
+
const file = fs.createWriteStream(tempDest);
|
|
73
|
+
|
|
74
|
+
const request = https.get(url, {
|
|
75
|
+
headers: {
|
|
76
|
+
'User-Agent': 'DevTunnel/3.0',
|
|
77
|
+
'Accept': '*/*'
|
|
78
|
+
},
|
|
79
|
+
timeout: 30000 // 30 second timeout
|
|
80
|
+
}, (response) => {
|
|
81
|
+
// Follow redirects
|
|
82
|
+
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
83
|
+
file.close();
|
|
84
|
+
fs.unlinkSync(tempDest);
|
|
85
|
+
downloadFile(response.headers.location, dest, retryCount)
|
|
86
|
+
.then(resolve)
|
|
87
|
+
.catch(reject);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (response.statusCode !== 200) {
|
|
92
|
+
file.close();
|
|
93
|
+
fs.unlinkSync(tempDest);
|
|
94
|
+
reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const totalSize = parseInt(response.headers['content-length'], 10);
|
|
99
|
+
let downloaded = 0;
|
|
100
|
+
let lastPercent = 0;
|
|
101
|
+
|
|
102
|
+
response.on('data', (chunk) => {
|
|
103
|
+
downloaded += chunk.length;
|
|
104
|
+
if (totalSize) {
|
|
105
|
+
const percent = Math.round((downloaded / totalSize) * 100);
|
|
106
|
+
if (percent !== lastPercent && percent % 5 === 0) {
|
|
107
|
+
const mb = (downloaded / 1024 / 1024).toFixed(1);
|
|
108
|
+
const totalMb = (totalSize / 1024 / 1024).toFixed(1);
|
|
109
|
+
process.stdout.write(`\r⏳ Downloading: ${percent}% (${mb}/${totalMb} MB)`);
|
|
110
|
+
lastPercent = percent;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
response.pipe(file);
|
|
116
|
+
|
|
117
|
+
file.on('finish', () => {
|
|
118
|
+
file.close(() => {
|
|
119
|
+
// Move temp file to final destination
|
|
120
|
+
try {
|
|
121
|
+
if (fs.existsSync(dest)) {
|
|
122
|
+
fs.unlinkSync(dest);
|
|
123
|
+
}
|
|
124
|
+
fs.renameSync(tempDest, dest);
|
|
125
|
+
|
|
126
|
+
console.log('\n✅ Download complete');
|
|
127
|
+
|
|
128
|
+
// Make executable on Unix-like systems
|
|
129
|
+
if (process.platform !== 'win32') {
|
|
130
|
+
try {
|
|
131
|
+
fs.chmodSync(dest, 0o755);
|
|
132
|
+
console.log('✅ Permissions set (executable)');
|
|
133
|
+
} catch (err) {
|
|
134
|
+
console.log('⚠️ Warning: Could not set executable permissions');
|
|
135
|
+
console.log(' Run: chmod +x ' + dest);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Verify file size
|
|
140
|
+
const stats = fs.statSync(dest);
|
|
141
|
+
if (stats.size < 1000000) { // Less than 1MB is suspicious
|
|
142
|
+
fs.unlinkSync(dest);
|
|
143
|
+
reject(new Error('Downloaded file is too small (corrupted)'));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
resolve();
|
|
148
|
+
} catch (err) {
|
|
149
|
+
reject(new Error(`Cannot finalize download: ${err.message}`));
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
request.on('timeout', () => {
|
|
156
|
+
request.destroy();
|
|
157
|
+
file.close();
|
|
158
|
+
if (fs.existsSync(tempDest)) fs.unlinkSync(tempDest);
|
|
159
|
+
reject(new Error('Download timeout (30 seconds)'));
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
request.on('error', (err) => {
|
|
163
|
+
file.close();
|
|
164
|
+
if (fs.existsSync(tempDest)) fs.unlinkSync(tempDest);
|
|
165
|
+
reject(err);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
file.on('error', (err) => {
|
|
169
|
+
file.close();
|
|
170
|
+
if (fs.existsSync(tempDest)) fs.unlinkSync(tempDest);
|
|
171
|
+
reject(new Error(`File write error: ${err.message}`));
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Try downloading from multiple URLs with retries
|
|
177
|
+
async function downloadWithRetry(urls, dest, maxRetries = 3) {
|
|
178
|
+
for (let urlIndex = 0; urlIndex < urls.length; urlIndex++) {
|
|
179
|
+
const url = urls[urlIndex];
|
|
180
|
+
console.log(`📥 Source: ${urlIndex === 0 ? 'GitHub' : 'Mirror'} (${urlIndex + 1}/${urls.length})`);
|
|
181
|
+
|
|
182
|
+
for (let retry = 0; retry < maxRetries; retry++) {
|
|
183
|
+
try {
|
|
184
|
+
if (retry > 0) {
|
|
185
|
+
console.log(`🔄 Retry ${retry}/${maxRetries - 1}...`);
|
|
186
|
+
await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2s before retry
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
await downloadFile(url, dest, retry);
|
|
190
|
+
return true; // Success!
|
|
191
|
+
|
|
192
|
+
} catch (err) {
|
|
193
|
+
const isLastRetry = retry === maxRetries - 1;
|
|
194
|
+
const isLastUrl = urlIndex === urls.length - 1;
|
|
195
|
+
|
|
196
|
+
if (err.message.includes('ENOTFOUND') || err.message.includes('ECONNREFUSED')) {
|
|
197
|
+
console.log(`\n❌ Network error: ${err.message}`);
|
|
198
|
+
} else if (err.message.includes('timeout')) {
|
|
199
|
+
console.log(`\n❌ Download timeout`);
|
|
200
|
+
} else {
|
|
201
|
+
console.log(`\n❌ Error: ${err.message}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (isLastRetry && isLastUrl) {
|
|
205
|
+
throw new Error(`All download attempts failed: ${err.message}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (isLastRetry) {
|
|
209
|
+
console.log('💡 Trying alternative source...\n');
|
|
210
|
+
break; // Try next URL
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
throw new Error('All download sources failed');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function setupCloudflared() {
|
|
220
|
+
const platform = process.platform;
|
|
221
|
+
const binaryPath = getBinaryPath();
|
|
222
|
+
|
|
223
|
+
console.log('\n╔════════════════════════════════════════╗');
|
|
224
|
+
console.log('║ 📦 Cloudflare Setup (First Run) ║');
|
|
225
|
+
console.log('╚════════════════════════════════════════╝\n');
|
|
226
|
+
|
|
227
|
+
// Check if binary already exists
|
|
228
|
+
if (fs.existsSync(binaryPath)) {
|
|
229
|
+
try {
|
|
230
|
+
// Verify it works
|
|
231
|
+
const testProc = spawn(binaryPath, ['--version'], { shell: true, stdio: 'pipe' });
|
|
232
|
+
const works = await new Promise((resolve) => {
|
|
233
|
+
testProc.on('close', (code) => resolve(code === 0));
|
|
234
|
+
testProc.on('error', () => resolve(false));
|
|
235
|
+
setTimeout(() => resolve(false), 5000);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
if (works) {
|
|
239
|
+
console.log('✅ Cloudflare already installed and working\n');
|
|
240
|
+
return binaryPath;
|
|
241
|
+
} else {
|
|
242
|
+
console.log('⚠️ Existing binary not working, re-downloading...\n');
|
|
243
|
+
fs.unlinkSync(binaryPath);
|
|
244
|
+
}
|
|
245
|
+
} catch {
|
|
246
|
+
console.log('⚠️ Existing binary corrupted, re-downloading...\n');
|
|
247
|
+
try {
|
|
248
|
+
fs.unlinkSync(binaryPath);
|
|
249
|
+
} catch {}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const urls = DOWNLOAD_URLS[platform];
|
|
254
|
+
if (!urls) {
|
|
255
|
+
console.error(`❌ ERROR: Platform "${platform}" not supported`);
|
|
256
|
+
console.error(' Supported: Windows, macOS, Linux\n');
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
console.log(`🖥️ Platform: ${getPlatformName()}`);
|
|
261
|
+
console.log(`📍 Install to: ${binaryPath}`);
|
|
262
|
+
console.log(`📊 Size: ~40 MB\n`);
|
|
263
|
+
|
|
264
|
+
// Check disk space
|
|
265
|
+
if (!hasEnoughDiskSpace()) {
|
|
266
|
+
console.error('❌ ERROR: Not enough disk space (need 50+ MB)\n');
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Check write permissions
|
|
271
|
+
try {
|
|
272
|
+
const dir = path.dirname(binaryPath);
|
|
273
|
+
if (!fs.existsSync(dir)) {
|
|
274
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
275
|
+
}
|
|
276
|
+
const testFile = path.join(dir, '.write-test');
|
|
277
|
+
fs.writeFileSync(testFile, 'test');
|
|
278
|
+
fs.unlinkSync(testFile);
|
|
279
|
+
} catch (err) {
|
|
280
|
+
console.error('❌ ERROR: Cannot write to installation directory');
|
|
281
|
+
console.error(` Location: ${path.dirname(binaryPath)}`);
|
|
282
|
+
console.error(` Reason: ${err.message}\n`);
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
console.log('📥 Starting download...\n');
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
await downloadWithRetry(urls, binaryPath);
|
|
290
|
+
|
|
291
|
+
// Final verification
|
|
292
|
+
console.log('\n🔍 Verifying installation...');
|
|
293
|
+
const testProc = spawn(binaryPath, ['--version'], { shell: true, stdio: 'pipe' });
|
|
294
|
+
const works = await new Promise((resolve) => {
|
|
295
|
+
testProc.on('close', (code) => resolve(code === 0));
|
|
296
|
+
testProc.on('error', () => resolve(false));
|
|
297
|
+
setTimeout(() => resolve(false), 5000);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
if (works) {
|
|
301
|
+
console.log('✅ Verification successful!');
|
|
302
|
+
console.log('✅ Cloudflare ready to use\n');
|
|
303
|
+
return binaryPath;
|
|
304
|
+
} else {
|
|
305
|
+
console.error('❌ Downloaded binary not working properly');
|
|
306
|
+
try {
|
|
307
|
+
fs.unlinkSync(binaryPath);
|
|
308
|
+
} catch {}
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
} catch (err) {
|
|
313
|
+
console.error('\n╔════════════════════════════════════════╗');
|
|
314
|
+
console.error('║ ❌ Installation Failed ║');
|
|
315
|
+
console.error('╚════════════════════════════════════════╝\n');
|
|
316
|
+
console.error(`Reason: ${err.message}\n`);
|
|
317
|
+
|
|
318
|
+
console.log('💡 Troubleshooting:');
|
|
319
|
+
console.log(' 1. Check internet connection');
|
|
320
|
+
console.log(' 2. Check firewall/antivirus settings');
|
|
321
|
+
console.log(' 3. Try again later');
|
|
322
|
+
console.log(' 4. Install manually: https://github.com/cloudflare/cloudflared/releases\n');
|
|
323
|
+
|
|
324
|
+
console.log('🔄 DevTunnel will use fallback tunnels (Ngrok/LocalTunnel)\n');
|
|
325
|
+
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Check if bundled cloudflared exists and is working
|
|
331
|
+
export function hasBundledCloudflared() {
|
|
332
|
+
const binaryPath = getBinaryPath();
|
|
333
|
+
return fs.existsSync(binaryPath);
|
|
334
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { join, dirname, basename } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import prompts from "prompts";
|
|
6
|
+
import { selectFolder } from "../utils/folder-picker.js";
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = dirname(__filename);
|
|
10
|
+
|
|
11
|
+
// Get project root directory dynamically (two levels up from src/core/)
|
|
12
|
+
const PROJECT_ROOT = dirname(dirname(__dirname));
|
|
13
|
+
|
|
14
|
+
// Helper to run command
|
|
15
|
+
function runCommand(command, args = [], cwd = process.cwd()) {
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
const proc = spawn(command, args, {
|
|
18
|
+
shell: true,
|
|
19
|
+
stdio: "pipe",
|
|
20
|
+
cwd: cwd
|
|
21
|
+
});
|
|
22
|
+
let output = "";
|
|
23
|
+
|
|
24
|
+
proc.stdout?.on("data", (data) => output += data.toString());
|
|
25
|
+
proc.stderr?.on("data", (data) => output += data.toString());
|
|
26
|
+
|
|
27
|
+
proc.on("close", (code) => resolve({ code, output }));
|
|
28
|
+
proc.on("error", () => resolve({ code: 1, output: "" }));
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Check if command exists
|
|
33
|
+
async function commandExists(command) {
|
|
34
|
+
const result = await runCommand("where", [command]);
|
|
35
|
+
return result.code === 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Main function
|
|
39
|
+
async function main() {
|
|
40
|
+
console.clear();
|
|
41
|
+
console.log("\n╔════════════════════════════════════════════╗");
|
|
42
|
+
console.log("║ ║");
|
|
43
|
+
console.log("║ 🚀 DevTunnel v3.0 ║");
|
|
44
|
+
console.log("║ ║");
|
|
45
|
+
console.log("║ Share local servers worldwide ║");
|
|
46
|
+
console.log("║ ║");
|
|
47
|
+
console.log("╚════════════════════════════════════════════╝\n");
|
|
48
|
+
|
|
49
|
+
// Step 1: Check Node.js
|
|
50
|
+
console.log("[1/4] Checking Node.js...");
|
|
51
|
+
if (!await commandExists("node")) {
|
|
52
|
+
console.log("❌ ERROR: Node.js not found!");
|
|
53
|
+
console.log("Install from: https://nodejs.org/");
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
console.log("✅ SUCCESS: Node.js installed\n");
|
|
57
|
+
|
|
58
|
+
// Step 2: Check Cloudflare (bundled or system-installed)
|
|
59
|
+
console.log("[2/4] Checking Cloudflare...");
|
|
60
|
+
|
|
61
|
+
// Import bundled cloudflared helpers
|
|
62
|
+
const { setupCloudflared, hasBundledCloudflared } = await import("./setup-cloudflared.js");
|
|
63
|
+
|
|
64
|
+
let cloudflareAvailable = false;
|
|
65
|
+
|
|
66
|
+
if (hasBundledCloudflared()) {
|
|
67
|
+
console.log("✅ SUCCESS: Using bundled Cloudflare (no install needed)");
|
|
68
|
+
cloudflareAvailable = true;
|
|
69
|
+
} else if (await commandExists("cloudflared")) {
|
|
70
|
+
console.log("✅ SUCCESS: Cloudflare installed on system");
|
|
71
|
+
cloudflareAvailable = true;
|
|
72
|
+
} else {
|
|
73
|
+
console.log("📦 First time setup - Downloading Cloudflare...");
|
|
74
|
+
console.log("💡 This only happens once (~40MB, 10-30 seconds)\n");
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const bundledPath = await setupCloudflared();
|
|
78
|
+
|
|
79
|
+
if (bundledPath) {
|
|
80
|
+
console.log("✅ SUCCESS: Cloudflare ready to use");
|
|
81
|
+
cloudflareAvailable = true;
|
|
82
|
+
} else {
|
|
83
|
+
console.log("⚠️ Could not download Cloudflare");
|
|
84
|
+
console.log("🔄 Will use alternative tunnel services\n");
|
|
85
|
+
}
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.log(`⚠️ Setup error: ${err.message}`);
|
|
88
|
+
console.log("🔄 Will use alternative tunnel services\n");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Show what's available
|
|
93
|
+
if (!cloudflareAvailable) {
|
|
94
|
+
console.log("💡 DevTunnel has multi-service fallback:");
|
|
95
|
+
console.log(" → Cloudflare (fastest, no password)");
|
|
96
|
+
console.log(" → Ngrok (fast alternative)");
|
|
97
|
+
console.log(" → LocalTunnel (backup option)");
|
|
98
|
+
console.log("");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Step 3: Check dependencies
|
|
102
|
+
console.log("[3/4] Checking dependencies...");
|
|
103
|
+
const nodeModulesPath = join(PROJECT_ROOT, "node_modules");
|
|
104
|
+
if (!existsSync(nodeModulesPath)) {
|
|
105
|
+
console.log("📦 Installing dependencies...\n");
|
|
106
|
+
// Run npm install in the project root directory
|
|
107
|
+
const result = await runCommand("npm", ["install"], PROJECT_ROOT);
|
|
108
|
+
if (result.code !== 0) {
|
|
109
|
+
console.log("\n❌ ERROR: npm install failed");
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
console.log("\n✅ SUCCESS: Dependencies installed");
|
|
113
|
+
} else {
|
|
114
|
+
console.log("✅ SUCCESS: Dependencies already installed");
|
|
115
|
+
}
|
|
116
|
+
console.log("");
|
|
117
|
+
|
|
118
|
+
// Step 4: Select folder using native OS dialog
|
|
119
|
+
console.log("[4/4] Select your project folder...");
|
|
120
|
+
console.log("⏳ Opening folder picker...\n");
|
|
121
|
+
|
|
122
|
+
const projectPath = await selectFolder();
|
|
123
|
+
|
|
124
|
+
if (!projectPath || projectPath.length === 0) {
|
|
125
|
+
console.log("❌ ERROR: No folder selected");
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const projectName = basename(projectPath);
|
|
130
|
+
console.log(`✅ Selected: ${projectPath}\n`);
|
|
131
|
+
|
|
132
|
+
// Get port
|
|
133
|
+
const portResponse = await prompts({
|
|
134
|
+
type: "number",
|
|
135
|
+
name: "port",
|
|
136
|
+
message: "Enter your dev server port:",
|
|
137
|
+
initial: 5173
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (!portResponse.port) {
|
|
141
|
+
console.log("❌ ERROR: No port entered");
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const devPort = portResponse.port;
|
|
146
|
+
const proxyPort = devPort + 1000; // Use port 1000 higher for proxy
|
|
147
|
+
|
|
148
|
+
console.log("\n╔════════════════════════════════════════════╗");
|
|
149
|
+
console.log("║ 🔧 Configuration ║");
|
|
150
|
+
console.log("╠════════════════════════════════════════════╣");
|
|
151
|
+
console.log(`║ 📦 Project: ${projectName.padEnd(28)} ║`);
|
|
152
|
+
console.log(`║ 🎯 Dev Server: localhost:${devPort.toString().padEnd(17)} ║`);
|
|
153
|
+
console.log(`║ 🔌 Proxy Port: ${proxyPort.toString().padEnd(28)} ║`);
|
|
154
|
+
console.log("╚════════════════════════════════════════════╝\n");
|
|
155
|
+
|
|
156
|
+
// Start proxy server
|
|
157
|
+
console.log("⚡ Starting services...\n");
|
|
158
|
+
const proxyPath = join(__dirname, "proxy-server.js");
|
|
159
|
+
const proxyProcess = spawn("node", [proxyPath, devPort.toString(), proxyPort.toString(), projectName], {
|
|
160
|
+
stdio: "inherit",
|
|
161
|
+
shell: false
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Wait for proxy to start
|
|
165
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
166
|
+
|
|
167
|
+
// Run main tunnel app (connects to proxy port)
|
|
168
|
+
// Use shell: false to properly handle paths with spaces
|
|
169
|
+
const indexPath = join(__dirname, "index.js");
|
|
170
|
+
const tunnelProcess = spawn("node", [indexPath, proxyPort.toString(), projectName, projectPath], {
|
|
171
|
+
stdio: "inherit",
|
|
172
|
+
shell: false
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Handle cleanup
|
|
176
|
+
const cleanup = () => {
|
|
177
|
+
console.log("\n🛑 Shutting down...");
|
|
178
|
+
proxyProcess.kill();
|
|
179
|
+
tunnelProcess.kill();
|
|
180
|
+
process.exit(0);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
tunnelProcess.on("close", (code) => {
|
|
184
|
+
cleanup();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
proxyProcess.on("close", () => {
|
|
188
|
+
cleanup();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Handle Ctrl+C
|
|
192
|
+
process.on("SIGINT", cleanup);
|
|
193
|
+
process.on("SIGTERM", cleanup);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Run
|
|
197
|
+
main().catch((error) => {
|
|
198
|
+
console.error("\n❌ ERROR:", error.message);
|
|
199
|
+
process.exit(1);
|
|
200
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { platform } from "os";
|
|
3
|
+
import { writeFileSync, readFileSync, unlinkSync, existsSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { tmpdir } from "os";
|
|
6
|
+
|
|
7
|
+
// Cross-platform native folder picker
|
|
8
|
+
export async function selectFolder() {
|
|
9
|
+
const os = platform();
|
|
10
|
+
const tempFile = join(tmpdir(), `folder-picker-${Date.now()}.txt`);
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
if (os === "win32") {
|
|
14
|
+
// Windows - Use MODERN OpenFileDialog (like website file uploads)
|
|
15
|
+
const script = `
|
|
16
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
17
|
+
[System.Windows.Forms.Application]::EnableVisualStyles()
|
|
18
|
+
|
|
19
|
+
$dialog = New-Object System.Windows.Forms.OpenFileDialog
|
|
20
|
+
$dialog.Title = "Select your project folder"
|
|
21
|
+
$dialog.Filter = "All files (*.*)|*.*"
|
|
22
|
+
$dialog.CheckFileExists = $false
|
|
23
|
+
$dialog.CheckPathExists = $true
|
|
24
|
+
$dialog.ValidateNames = $false
|
|
25
|
+
$dialog.FileName = "Folder Selection"
|
|
26
|
+
$dialog.Multiselect = $false
|
|
27
|
+
$dialog.InitialDirectory = [Environment]::GetFolderPath("UserProfile")
|
|
28
|
+
|
|
29
|
+
$result = $dialog.ShowDialog()
|
|
30
|
+
if ($result -eq [System.Windows.Forms.DialogResult]::OK) {
|
|
31
|
+
$folderPath = Split-Path -Parent $dialog.FileName
|
|
32
|
+
if (-not $folderPath) {
|
|
33
|
+
$folderPath = $dialog.FileName
|
|
34
|
+
}
|
|
35
|
+
$folderPath | Out-File -FilePath "${tempFile.replace(/\\/g, '\\\\')}" -Encoding UTF8 -NoNewline
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
await runPowerShell(script);
|
|
40
|
+
|
|
41
|
+
} else if (os === "darwin") {
|
|
42
|
+
// macOS - Use osascript
|
|
43
|
+
const script = `
|
|
44
|
+
set folderPath to choose folder with prompt "Select your project folder"
|
|
45
|
+
set posixPath to POSIX path of folderPath
|
|
46
|
+
do shell script "echo " & quoted form of posixPath & " > '${tempFile}'"
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
await runCommand("osascript", ["-e", script]);
|
|
50
|
+
|
|
51
|
+
} else {
|
|
52
|
+
// Linux - Try zenity first, then kdialog
|
|
53
|
+
try {
|
|
54
|
+
await runCommand("zenity", [
|
|
55
|
+
"--file-selection",
|
|
56
|
+
"--directory",
|
|
57
|
+
"--title=Select your project folder"
|
|
58
|
+
], tempFile);
|
|
59
|
+
} catch {
|
|
60
|
+
await runCommand("kdialog", [
|
|
61
|
+
"--getexistingdirectory",
|
|
62
|
+
process.env.HOME || "/",
|
|
63
|
+
"--title", "Select your project folder"
|
|
64
|
+
], tempFile);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Read the selected folder
|
|
69
|
+
if (existsSync(tempFile)) {
|
|
70
|
+
const folderPath = readFileSync(tempFile, "utf8").trim();
|
|
71
|
+
unlinkSync(tempFile);
|
|
72
|
+
return folderPath;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return null;
|
|
76
|
+
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error("Folder picker error:", error.message);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Run PowerShell command
|
|
84
|
+
function runPowerShell(script) {
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
const proc = spawn("powershell", [
|
|
87
|
+
"-NoProfile",
|
|
88
|
+
"-NonInteractive",
|
|
89
|
+
"-ExecutionPolicy", "Bypass",
|
|
90
|
+
"-Command", script
|
|
91
|
+
], {
|
|
92
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
93
|
+
shell: false
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
let stderr = "";
|
|
97
|
+
proc.stderr?.on("data", (data) => stderr += data.toString());
|
|
98
|
+
|
|
99
|
+
proc.on("close", (code) => {
|
|
100
|
+
if (code === 0) {
|
|
101
|
+
resolve();
|
|
102
|
+
} else {
|
|
103
|
+
reject(new Error(stderr || `PowerShell exited with code ${code}`));
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
proc.on("error", reject);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Run generic command
|
|
112
|
+
function runCommand(command, args, outputFile) {
|
|
113
|
+
return new Promise((resolve, reject) => {
|
|
114
|
+
const proc = spawn(command, args, {
|
|
115
|
+
stdio: outputFile ? ["ignore", "pipe", "pipe"] : "pipe",
|
|
116
|
+
shell: true
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
let stdout = "";
|
|
120
|
+
|
|
121
|
+
if (outputFile) {
|
|
122
|
+
proc.stdout?.on("data", (data) => {
|
|
123
|
+
stdout += data.toString();
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
proc.on("close", (code) => {
|
|
128
|
+
if (code === 0) {
|
|
129
|
+
if (outputFile && stdout) {
|
|
130
|
+
writeFileSync(outputFile, stdout.trim());
|
|
131
|
+
}
|
|
132
|
+
resolve();
|
|
133
|
+
} else {
|
|
134
|
+
reject(new Error(`Command failed with code ${code}`));
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
proc.on("error", reject);
|
|
139
|
+
});
|
|
140
|
+
}
|