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.
@@ -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
+ }