@openbuilder/cli 0.31.11

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.
Files changed (78) hide show
  1. package/README.md +1053 -0
  2. package/bin/openbuilder.js +31 -0
  3. package/dist/chunks/Banner-D4tqKfzA.js +113 -0
  4. package/dist/chunks/Banner-D4tqKfzA.js.map +1 -0
  5. package/dist/chunks/auto-update-Dj3lWPWO.js +350 -0
  6. package/dist/chunks/auto-update-Dj3lWPWO.js.map +1 -0
  7. package/dist/chunks/build-D0qYqIq0.js +116 -0
  8. package/dist/chunks/build-D0qYqIq0.js.map +1 -0
  9. package/dist/chunks/cleanup-qVTsA3tk.js +141 -0
  10. package/dist/chunks/cleanup-qVTsA3tk.js.map +1 -0
  11. package/dist/chunks/cli-error-BjQwvWtK.js +140 -0
  12. package/dist/chunks/cli-error-BjQwvWtK.js.map +1 -0
  13. package/dist/chunks/config-BGP1jZJ4.js +167 -0
  14. package/dist/chunks/config-BGP1jZJ4.js.map +1 -0
  15. package/dist/chunks/config-manager-BkbjtN-H.js +133 -0
  16. package/dist/chunks/config-manager-BkbjtN-H.js.map +1 -0
  17. package/dist/chunks/database-BvAbD4sP.js +68 -0
  18. package/dist/chunks/database-BvAbD4sP.js.map +1 -0
  19. package/dist/chunks/database-setup-BYjIRAmT.js +253 -0
  20. package/dist/chunks/database-setup-BYjIRAmT.js.map +1 -0
  21. package/dist/chunks/exports-ij9sv4UM.js +7793 -0
  22. package/dist/chunks/exports-ij9sv4UM.js.map +1 -0
  23. package/dist/chunks/init-CZoN6soU.js +468 -0
  24. package/dist/chunks/init-CZoN6soU.js.map +1 -0
  25. package/dist/chunks/init-tui-BNzk_7Yx.js +1127 -0
  26. package/dist/chunks/init-tui-BNzk_7Yx.js.map +1 -0
  27. package/dist/chunks/logger-ZpJi7chw.js +38 -0
  28. package/dist/chunks/logger-ZpJi7chw.js.map +1 -0
  29. package/dist/chunks/main-tui-Cq1hLCx-.js +644 -0
  30. package/dist/chunks/main-tui-Cq1hLCx-.js.map +1 -0
  31. package/dist/chunks/manager-CvGX9qqe.js +1161 -0
  32. package/dist/chunks/manager-CvGX9qqe.js.map +1 -0
  33. package/dist/chunks/port-allocator-BRFzgH9b.js +749 -0
  34. package/dist/chunks/port-allocator-BRFzgH9b.js.map +1 -0
  35. package/dist/chunks/process-killer-CaUL7Kpl.js +87 -0
  36. package/dist/chunks/process-killer-CaUL7Kpl.js.map +1 -0
  37. package/dist/chunks/prompts-1QbE_bRr.js +128 -0
  38. package/dist/chunks/prompts-1QbE_bRr.js.map +1 -0
  39. package/dist/chunks/repo-cloner-CpOQjFSo.js +219 -0
  40. package/dist/chunks/repo-cloner-CpOQjFSo.js.map +1 -0
  41. package/dist/chunks/repo-detector-B_oj696o.js +66 -0
  42. package/dist/chunks/repo-detector-B_oj696o.js.map +1 -0
  43. package/dist/chunks/run-D23hg4xy.js +630 -0
  44. package/dist/chunks/run-D23hg4xy.js.map +1 -0
  45. package/dist/chunks/runner-logger-instance-nDWv2h2T.js +899 -0
  46. package/dist/chunks/runner-logger-instance-nDWv2h2T.js.map +1 -0
  47. package/dist/chunks/spinner-BJL9zWAJ.js +53 -0
  48. package/dist/chunks/spinner-BJL9zWAJ.js.map +1 -0
  49. package/dist/chunks/start-BygPCbvw.js +1708 -0
  50. package/dist/chunks/start-BygPCbvw.js.map +1 -0
  51. package/dist/chunks/start-traditional-uoLZXdxm.js +255 -0
  52. package/dist/chunks/start-traditional-uoLZXdxm.js.map +1 -0
  53. package/dist/chunks/status-cS8YwtUx.js +97 -0
  54. package/dist/chunks/status-cS8YwtUx.js.map +1 -0
  55. package/dist/chunks/theme-DhorI2Hb.js +44 -0
  56. package/dist/chunks/theme-DhorI2Hb.js.map +1 -0
  57. package/dist/chunks/upgrade-CT6w0lKp.js +323 -0
  58. package/dist/chunks/upgrade-CT6w0lKp.js.map +1 -0
  59. package/dist/chunks/useBuildState-CdBSu9y_.js +331 -0
  60. package/dist/chunks/useBuildState-CdBSu9y_.js.map +1 -0
  61. package/dist/cli/index.js +694 -0
  62. package/dist/cli/index.js.map +1 -0
  63. package/dist/index.js +14358 -0
  64. package/dist/index.js.map +1 -0
  65. package/dist/instrument.js +64226 -0
  66. package/dist/instrument.js.map +1 -0
  67. package/dist/templates.json +295 -0
  68. package/package.json +98 -0
  69. package/scripts/install-vendor-deps.js +34 -0
  70. package/scripts/install-vendor.js +167 -0
  71. package/scripts/prepare-release.js +71 -0
  72. package/templates/config.template.json +18 -0
  73. package/templates.json +295 -0
  74. package/vendor/ai-sdk-provider-claude-code-LOCAL.tgz +0 -0
  75. package/vendor/sentry-core-LOCAL.tgz +0 -0
  76. package/vendor/sentry-nextjs-LOCAL.tgz +0 -0
  77. package/vendor/sentry-node-LOCAL.tgz +0 -0
  78. package/vendor/sentry-node-core-LOCAL.tgz +0 -0
@@ -0,0 +1,1161 @@
1
+ // OpenBuilder CLI - Built with Rollup
2
+ import { execSync, spawn } from 'node:child_process';
3
+ import { EventEmitter } from 'node:events';
4
+ import { platform, arch } from 'node:os';
5
+ import { existsSync, mkdirSync, chmodSync } from 'node:fs';
6
+ import { resolve } from 'node:path';
7
+ import http from 'http';
8
+ import httpProxy from 'http-proxy';
9
+ import zlib from 'zlib';
10
+
11
+ const CLOUDFLARED_GITHUB = 'https://github.com/cloudflare/cloudflared/releases/latest/download';
12
+ /**
13
+ * Get the local bin directory for cloudflared installation
14
+ */
15
+ function getBinDir() {
16
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '/tmp';
17
+ const binDir = resolve(homeDir, '.openbuilder', 'bin');
18
+ if (!existsSync(binDir)) {
19
+ mkdirSync(binDir, { recursive: true });
20
+ }
21
+ return binDir;
22
+ }
23
+ /**
24
+ * Detect the appropriate cloudflared binary name for this platform
25
+ */
26
+ function getCloudflaredBinaryName() {
27
+ const plat = platform();
28
+ const architecture = arch();
29
+ if (plat === 'darwin') {
30
+ if (architecture === 'arm64') {
31
+ return 'cloudflared-darwin-arm64.tgz';
32
+ }
33
+ return 'cloudflared-darwin-amd64.tgz';
34
+ }
35
+ else if (plat === 'linux') {
36
+ if (architecture === 'arm64') {
37
+ return 'cloudflared-linux-arm64';
38
+ }
39
+ return 'cloudflared-linux-amd64';
40
+ }
41
+ else if (plat === 'win32') {
42
+ return 'cloudflared-windows-amd64.exe';
43
+ }
44
+ throw new Error(`Unsupported platform: ${plat} ${architecture}`);
45
+ }
46
+ /**
47
+ * Check if cloudflared is already installed (globally or locally)
48
+ */
49
+ function checkExistingInstallation() {
50
+ // Check global installation
51
+ try {
52
+ execSync('cloudflared --version', { stdio: 'ignore' });
53
+ return 'cloudflared';
54
+ }
55
+ catch {
56
+ // Not installed globally
57
+ }
58
+ // Check local installation
59
+ const binDir = getBinDir();
60
+ const localPath = resolve(binDir, 'cloudflared');
61
+ if (existsSync(localPath)) {
62
+ return localPath;
63
+ }
64
+ return null;
65
+ }
66
+ /**
67
+ * Download and install cloudflared binary
68
+ */
69
+ async function downloadCloudflared() {
70
+ const binDir = getBinDir();
71
+ const binaryName = getCloudflaredBinaryName();
72
+ const downloadUrl = `${CLOUDFLARED_GITHUB}/${binaryName}`;
73
+ const plat = platform();
74
+ console.log(`📦 Downloading cloudflared from ${downloadUrl}...`);
75
+ if (plat === 'darwin') {
76
+ // macOS - download and extract tarball
77
+ const tarPath = resolve(binDir, 'cloudflared.tgz');
78
+ const extractDir = resolve(binDir, 'cloudflared-extract');
79
+ execSync(`curl -L "${downloadUrl}" -o "${tarPath}"`, { stdio: 'inherit' });
80
+ // Create extraction directory
81
+ if (!existsSync(extractDir)) {
82
+ mkdirSync(extractDir, { recursive: true });
83
+ }
84
+ // Extract
85
+ execSync(`tar -xzf "${tarPath}" -C "${extractDir}"`, { stdio: 'inherit' });
86
+ // Find the cloudflared binary in extracted files
87
+ const extractedBinary = resolve(extractDir, 'cloudflared');
88
+ const targetPath = resolve(binDir, 'cloudflared');
89
+ // Move to bin directory
90
+ execSync(`mv "${extractedBinary}" "${targetPath}"`, { stdio: 'inherit' });
91
+ // Cleanup
92
+ execSync(`rm -rf "${tarPath}" "${extractDir}"`, { stdio: 'ignore' });
93
+ // Make executable
94
+ chmodSync(targetPath, 0o755);
95
+ return targetPath;
96
+ }
97
+ else if (plat === 'linux') {
98
+ // Linux - download binary directly
99
+ const targetPath = resolve(binDir, 'cloudflared');
100
+ execSync(`curl -L "${downloadUrl}" -o "${targetPath}"`, { stdio: 'inherit' });
101
+ chmodSync(targetPath, 0o755);
102
+ return targetPath;
103
+ }
104
+ else if (plat === 'win32') {
105
+ // Windows - download .exe
106
+ const targetPath = resolve(binDir, 'cloudflared.exe');
107
+ execSync(`curl -L "${downloadUrl}" -o "${targetPath}"`, { stdio: 'inherit' });
108
+ return targetPath;
109
+ }
110
+ throw new Error(`Unsupported platform: ${plat}`);
111
+ }
112
+ /**
113
+ * Ensure cloudflared is installed and return the path to the binary
114
+ */
115
+ async function ensureCloudflared(silent = false) {
116
+ // Check if already installed
117
+ const existing = checkExistingInstallation();
118
+ if (existing) {
119
+ if (!silent) {
120
+ console.log(`✅ cloudflared found: ${existing}`);
121
+ }
122
+ return existing;
123
+ }
124
+ if (!silent) {
125
+ console.log('📦 cloudflared not found, installing...');
126
+ }
127
+ try {
128
+ const path = await downloadCloudflared();
129
+ if (!silent) {
130
+ console.log(`✅ cloudflared installed to: ${path}`);
131
+ }
132
+ return path;
133
+ }
134
+ catch (error) {
135
+ console.error('❌ Failed to install cloudflared:', error);
136
+ throw new Error('Failed to install cloudflared. Please install manually: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation/');
137
+ }
138
+ }
139
+
140
+ // src/lib/selection/hmr-proxy-script.ts
141
+ var SELECTION_SCRIPT = `
142
+ (function() {
143
+
144
+ // Selection state - DORMANT by default
145
+ let isInspectorActive = false;
146
+ let inspectorStyle = null;
147
+ let highlightedElement = null;
148
+ let highlightOverlay = null;
149
+ let mouseHandler = null;
150
+ let clickHandler = null;
151
+
152
+ function getProxyPrefix() {
153
+ try {
154
+ var parts = window.location.pathname.split('/').filter(Boolean);
155
+ var projectsIndex = parts.indexOf('projects');
156
+ if (projectsIndex === -1) {
157
+ return null;
158
+ }
159
+
160
+ var projectId = parts[projectsIndex + 1];
161
+ if (!projectId) {
162
+ return null;
163
+ }
164
+
165
+ return '/api/projects/' + projectId + '/proxy?path=';
166
+ } catch (error) {
167
+ console.warn('\u26A0\uFE0F [OpenBuilder CSS] Unable to derive proxy prefix:', error);
168
+ return null;
169
+ }
170
+ }
171
+
172
+ var proxyPrefix = getProxyPrefix();
173
+
174
+ function rewriteStylesheetHref(link) {
175
+ if (!proxyPrefix || !link) {
176
+ return false;
177
+ }
178
+
179
+ var href = link.getAttribute('href');
180
+ if (!href) {
181
+ return false;
182
+ }
183
+
184
+ var trimmed = href.trim();
185
+
186
+ if (
187
+ trimmed.indexOf('proxy?path=') !== -1 ||
188
+ trimmed.indexOf('http://') === 0 ||
189
+ trimmed.indexOf('https://') === 0 ||
190
+ trimmed.indexOf('//') === 0 ||
191
+ trimmed.indexOf('data:') === 0
192
+ ) {
193
+ return false;
194
+ }
195
+
196
+ if (trimmed.charAt(0) === '/') {
197
+ var proxiedHref = proxyPrefix + encodeURIComponent(trimmed);
198
+ if (link.getAttribute('href') !== proxiedHref) {
199
+ link.setAttribute('href', proxiedHref);
200
+ console.log('\u{1F3A8} [OpenBuilder CSS] rewrote stylesheet href to proxy:', proxiedHref);
201
+ return true;
202
+ }
203
+ }
204
+
205
+ return false;
206
+ }
207
+
208
+ // Debug helper: track when stylesheets are added/loaded inside the iframe
209
+ function monitorStylesheets() {
210
+ const loggedLinks = new WeakSet();
211
+ const loggedStyles = new WeakSet();
212
+
213
+ const logLink = (link, phase) => {
214
+ if (!link) return;
215
+ const href = link.getAttribute('href') || '(no href)';
216
+ console.log('\u{1F3A8} [OpenBuilder CSS]', phase + ' stylesheet:', href);
217
+ };
218
+
219
+ const logStyle = (style, phase) => {
220
+ if (!style) return;
221
+ const sample = (style.textContent || '')
222
+ .replace(/s+/g, ' ')
223
+ .trim()
224
+ .slice(0, 140);
225
+ console.log('\u{1F3A8} [OpenBuilder CSS]', phase + ' inline style', sample);
226
+ };
227
+
228
+ const attachLinkListeners = (link, phase) => {
229
+ if (!link || loggedLinks.has(link)) return;
230
+ loggedLinks.add(link);
231
+ const rewritten = rewriteStylesheetHref(link);
232
+ const phaseLabel = rewritten ? phase + ' (rewritten)' : phase;
233
+ logLink(link, phaseLabel);
234
+
235
+ link.addEventListener(
236
+ 'load',
237
+ () => {
238
+ rewriteStylesheetHref(link);
239
+ logLink(link, 'loaded');
240
+ },
241
+ { once: true }
242
+ );
243
+
244
+ link.addEventListener(
245
+ 'error',
246
+ () => {
247
+ rewriteStylesheetHref(link);
248
+ logLink(link, 'error loading');
249
+ },
250
+ { once: true }
251
+ );
252
+ };
253
+
254
+ const recordStyleElement = (style, phase) => {
255
+ if (!style || loggedStyles.has(style)) return;
256
+ loggedStyles.add(style);
257
+ logStyle(style, phase);
258
+ };
259
+
260
+ // Observe new link/style nodes appended to the document
261
+ const observer = new MutationObserver((mutations) => {
262
+ mutations.forEach((mutation) => {
263
+ mutation.addedNodes.forEach((node) => {
264
+ if (!(node instanceof HTMLElement)) return;
265
+
266
+ if (node.tagName === 'LINK' && (node.getAttribute('rel') || '').includes('stylesheet')) {
267
+ attachLinkListeners(node, 'added');
268
+ }
269
+
270
+ if (node.tagName === 'STYLE') {
271
+ recordStyleElement(node, 'added');
272
+ }
273
+ });
274
+ });
275
+ });
276
+
277
+ try {
278
+ const head = document.head || document.documentElement;
279
+ if (head) {
280
+ observer.observe(head, { childList: true, subtree: true });
281
+ }
282
+ } catch (e) {
283
+ console.warn('\u26A0\uFE0F [OpenBuilder CSS] Failed to observe stylesheet mutations:', e);
284
+ }
285
+
286
+ // Log existing stylesheet/link elements when script runs
287
+ document
288
+ .querySelectorAll('link[rel~="stylesheet"], style')
289
+ .forEach((node) => {
290
+ if (node.tagName === 'LINK') {
291
+ attachLinkListeners(node, 'existing');
292
+ } else if (node.tagName === 'STYLE') {
293
+ recordStyleElement(node, 'existing');
294
+ }
295
+ });
296
+
297
+ // Capture late load events (when link is in DOM before listener added)
298
+ document.addEventListener(
299
+ 'load',
300
+ (event) => {
301
+ const target = event.target;
302
+ if (
303
+ target instanceof HTMLLinkElement &&
304
+ (target.getAttribute('rel') || '').includes('stylesheet')
305
+ ) {
306
+ attachLinkListeners(target, 'load event');
307
+ }
308
+ },
309
+ true
310
+ );
311
+ }
312
+
313
+ monitorStylesheets();
314
+
315
+ // Create highlight overlay
316
+ function createHighlightOverlay() {
317
+ if (highlightOverlay) return highlightOverlay;
318
+
319
+ const overlay = document.createElement('div');
320
+ overlay.id = '__openbuilder-highlight';
321
+ overlay.style.cssText = \`
322
+ position: absolute;
323
+ pointer-events: none;
324
+ border: 2px solid #7553FF;
325
+ background: rgba(117, 83, 255, 0.1);
326
+ z-index: 999999;
327
+ transition: all 0.1s ease;
328
+ box-shadow: 0 0 0 1px rgba(117, 83, 255, 0.3), 0 0 20px rgba(117, 83, 255, 0.4);
329
+ \`;
330
+ document.body.appendChild(overlay);
331
+ highlightOverlay = overlay;
332
+ return overlay;
333
+ }
334
+
335
+ // Remove highlight overlay
336
+ function removeHighlightOverlay() {
337
+ if (highlightOverlay) {
338
+ highlightOverlay.remove();
339
+ highlightOverlay = null;
340
+ }
341
+ }
342
+
343
+ // Highlight element on hover
344
+ function highlightElement(element) {
345
+ if (!element || !isInspectorActive) {
346
+ removeHighlightOverlay();
347
+ return;
348
+ }
349
+
350
+ const rect = element.getBoundingClientRect();
351
+ const overlay = createHighlightOverlay();
352
+
353
+ overlay.style.left = rect.left + window.scrollX + 'px';
354
+ overlay.style.top = rect.top + window.scrollY + 'px';
355
+ overlay.style.width = rect.width + 'px';
356
+ overlay.style.height = rect.height + 'px';
357
+
358
+ highlightedElement = element;
359
+ }
360
+
361
+ // Generate unique CSS selector for element
362
+ function generateSelector(element) {
363
+ // Strategy 1: data-testid (best)
364
+ const testId = element.getAttribute('data-testid');
365
+ if (testId) {
366
+ return \`[data-testid="\${testId}"]\`;
367
+ }
368
+
369
+ // Strategy 2: ID (good)
370
+ if (element.id) {
371
+ return \`#\${element.id}\`;
372
+ }
373
+
374
+ // Strategy 3: Class + tag (ok) - but skip classes with colons (Tailwind responsive)
375
+ const classes = Array.from(element.classList)
376
+ .filter(c => !c.match(/^(hover:|focus:|active:|group-|animate-|transition-)/))
377
+ .filter(c => !c.includes(':')) // Skip Tailwind responsive classes
378
+ .slice(0, 3) // Limit to first 3 classes
379
+ .join('.');
380
+
381
+ if (classes) {
382
+ const tagName = element.tagName.toLowerCase();
383
+
384
+ try {
385
+ // Check if unique enough
386
+ const selector = \`\${tagName}.\${classes}\`;
387
+ const matches = document.querySelectorAll(selector);
388
+
389
+ if (matches.length === 1) {
390
+ return selector;
391
+ }
392
+
393
+ // Add nth-child if multiple matches
394
+ const parent = element.parentElement;
395
+ if (parent) {
396
+ const siblings = Array.from(parent.children);
397
+ const index = siblings.indexOf(element) + 1;
398
+ return \`\${selector}:nth-child(\${index})\`;
399
+ }
400
+
401
+ return selector;
402
+ } catch (err) {
403
+ console.warn('Invalid selector, falling back to path:', err);
404
+ }
405
+ }
406
+
407
+ // Strategy 4: Full path (fallback)
408
+ return getFullPath(element);
409
+ }
410
+
411
+ // Get full CSS path to element
412
+ function getFullPath(element) {
413
+ const path = [];
414
+ let current = element;
415
+
416
+ while (current && current !== document.body) {
417
+ let selector = current.tagName.toLowerCase();
418
+
419
+ if (current.id) {
420
+ selector += \`#\${current.id}\`;
421
+ path.unshift(selector);
422
+ break;
423
+ }
424
+
425
+ const parent = current.parentElement;
426
+ if (parent) {
427
+ const siblings = Array.from(parent.children).filter(
428
+ child => child.tagName === current.tagName
429
+ );
430
+
431
+ if (siblings.length > 1) {
432
+ const index = siblings.indexOf(current) + 1;
433
+ selector += \`:nth-of-type(\${index})\`;
434
+ }
435
+ }
436
+
437
+ path.unshift(selector);
438
+ current = current.parentElement;
439
+ }
440
+
441
+ return path.join(' > ');
442
+ }
443
+
444
+ // Capture element data and click position
445
+ function captureElementData(element, clickEvent) {
446
+ const rect = element.getBoundingClientRect();
447
+
448
+ return {
449
+ selector: generateSelector(element),
450
+ tagName: element.tagName.toLowerCase(),
451
+ className: element.className,
452
+ id: element.id,
453
+ textContent: element.textContent?.trim().slice(0, 100),
454
+ innerHTML: element.innerHTML?.slice(0, 200),
455
+ attributes: Array.from(element.attributes).reduce((acc, attr) => {
456
+ acc[attr.name] = attr.value;
457
+ return acc;
458
+ }, {}),
459
+ boundingRect: {
460
+ top: rect.top,
461
+ left: rect.left,
462
+ width: rect.width,
463
+ height: rect.height,
464
+ },
465
+ clickPosition: {
466
+ x: clickEvent.clientX,
467
+ y: clickEvent.clientY,
468
+ },
469
+ computedStyles: {
470
+ backgroundColor: window.getComputedStyle(element).backgroundColor,
471
+ color: window.getComputedStyle(element).color,
472
+ fontSize: window.getComputedStyle(element).fontSize,
473
+ fontFamily: window.getComputedStyle(element).fontFamily,
474
+ }
475
+ };
476
+ }
477
+
478
+ // Mouse move handler (hover preview)
479
+ function handleMouseMove(e) {
480
+ if (!isInspectorActive) return;
481
+
482
+ const element = e.target;
483
+ if (element && element !== highlightedElement) {
484
+ highlightElement(element);
485
+ }
486
+ }
487
+
488
+ // Click handler (select element)
489
+ function handleClick(e) {
490
+ console.log('\u{1F5B1}\uFE0F Click detected, selection mode:', isInspectorActive);
491
+
492
+ if (!isInspectorActive) return;
493
+
494
+ e.preventDefault();
495
+ e.stopPropagation();
496
+
497
+ const element = e.target;
498
+ const data = captureElementData(element, e);
499
+
500
+ console.log('\u{1F3AF} Element captured:', data);
501
+ console.log(' Click position:', data.clickPosition);
502
+ console.log('\u{1F4E4} Sending postMessage to parent...');
503
+
504
+ // Send to parent window
505
+ window.parent.postMessage({
506
+ type: 'openbuilder:element-selected',
507
+ data,
508
+ }, '*');
509
+
510
+ console.log('\u2705 Message sent to parent');
511
+
512
+ // Disable selection mode after selection
513
+ setInspectorActive(false);
514
+ }
515
+
516
+ // Activate/deactivate inspector (DORMANT PATTERN)
517
+ function setInspectorActive(active) {
518
+ isInspectorActive = active;
519
+
520
+ if (active) {
521
+ // Add inspector styles ONLY when activated
522
+ if (!inspectorStyle) {
523
+ inspectorStyle = document.createElement('style');
524
+ inspectorStyle.textContent = \`
525
+ .inspector-active * {
526
+ cursor: crosshair !important;
527
+ }
528
+ .inspector-highlight {
529
+ outline: 2px solid #7553FF !important;
530
+ outline-offset: -2px !important;
531
+ background-color: rgba(117, 83, 255, 0.1) !important;
532
+ }
533
+ \`;
534
+ document.head.appendChild(inspectorStyle);
535
+ }
536
+
537
+ document.body.classList.add('inspector-active');
538
+
539
+ // Add event listeners ONLY when activated
540
+ if (!mouseHandler) {
541
+ mouseHandler = handleMouseMove;
542
+ clickHandler = handleClick;
543
+ document.addEventListener('mousemove', mouseHandler, true);
544
+ document.addEventListener('click', clickHandler, true);
545
+ console.log('\u2705 Inspector event listeners attached');
546
+ }
547
+ } else {
548
+ document.body.classList.remove('inspector-active');
549
+
550
+ // Remove highlight
551
+ if (highlightedElement) {
552
+ highlightedElement = null;
553
+ }
554
+ removeHighlightOverlay();
555
+
556
+ // Remove event listeners when deactivated
557
+ if (mouseHandler) {
558
+ document.removeEventListener('mousemove', mouseHandler, true);
559
+ document.removeEventListener('click', clickHandler, true);
560
+ mouseHandler = null;
561
+ clickHandler = null;
562
+ console.log('\u{1F9F9} Inspector event listeners removed');
563
+ }
564
+
565
+ // Remove styles
566
+ if (inspectorStyle) {
567
+ inspectorStyle.remove();
568
+ inspectorStyle = null;
569
+ }
570
+ }
571
+
572
+ }
573
+
574
+ // Listen for activation/deactivation from parent
575
+ window.addEventListener('message', (e) => {
576
+ if (e.data.type === 'openbuilder:toggle-selection-mode') {
577
+ setInspectorActive(e.data.enabled);
578
+ }
579
+ });
580
+
581
+ // Announce ready to parent
582
+ window.parent.postMessage({ type: 'openbuilder:ready' }, '*');
583
+ })();
584
+ `;
585
+
586
+ /**
587
+ * Injection Proxy for Remote Runner Support
588
+ *
589
+ * This proxy sits between the dev server and the Cloudflare tunnel,
590
+ * injecting the element selection script into HTML responses.
591
+ *
592
+ * This enables the "select element" feature to work when:
593
+ * - Frontend is hosted remotely (e.g., Vercel)
594
+ * - Runner is running locally on user's machine
595
+ * - Traffic flows through Cloudflare tunnel
596
+ *
597
+ * Without this proxy, the selection script can't be injected because
598
+ * the iframe loads from a different origin (the tunnel URL).
599
+ */
600
+ const DEFAULT_PROXY_PORT = 4000;
601
+ /**
602
+ * Create an injection proxy that forwards requests to a dev server
603
+ * while injecting the selection script into HTML responses.
604
+ */
605
+ async function createInjectionProxy(options) {
606
+ const { targetPort, proxyPort = DEFAULT_PROXY_PORT, onError, log = console.log } = options;
607
+ const targetUrl = `http://localhost:${targetPort}`;
608
+ // Create proxy with WebSocket support
609
+ const proxy = httpProxy.createProxyServer({
610
+ target: targetUrl,
611
+ ws: true,
612
+ selfHandleResponse: true, // We need to modify HTML responses
613
+ changeOrigin: true,
614
+ });
615
+ const server = http.createServer((req, res) => {
616
+ // Forward the request through proxy
617
+ proxy.web(req, res, {}, (err) => {
618
+ if (err) {
619
+ onError?.(err);
620
+ // Try to send error response if headers not sent
621
+ if (!res.headersSent) {
622
+ res.writeHead(502, { 'Content-Type': 'text/plain' });
623
+ res.end(`Proxy error: ${err.message}`);
624
+ }
625
+ }
626
+ });
627
+ });
628
+ // Handle WebSocket upgrades (critical for HMR in Vite/Next.js/etc)
629
+ server.on('upgrade', (req, socket, head) => {
630
+ proxy.ws(req, socket, head, {}, (err) => {
631
+ if (err) {
632
+ onError?.(err);
633
+ socket.destroy();
634
+ }
635
+ });
636
+ });
637
+ // Intercept responses to inject script into HTML
638
+ proxy.on('proxyRes', (proxyRes, req, res) => {
639
+ const contentType = proxyRes.headers['content-type'] || '';
640
+ const isHtml = contentType.includes('text/html');
641
+ if (!isHtml) {
642
+ // Pass through non-HTML responses unchanged
643
+ // Copy all headers
644
+ res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
645
+ proxyRes.pipe(res);
646
+ return;
647
+ }
648
+ // Handle HTML - collect chunks and inject script
649
+ const chunks = [];
650
+ const encoding = proxyRes.headers['content-encoding'];
651
+ proxyRes.on('data', (chunk) => {
652
+ chunks.push(chunk);
653
+ });
654
+ proxyRes.on('end', () => {
655
+ try {
656
+ let body = Buffer.concat(chunks);
657
+ // Decompress if needed
658
+ if (encoding === 'gzip') {
659
+ try {
660
+ body = zlib.gunzipSync(body);
661
+ }
662
+ catch {
663
+ // If decompression fails, pass through unchanged
664
+ const headers = { ...proxyRes.headers };
665
+ res.writeHead(proxyRes.statusCode || 200, headers);
666
+ res.end(Buffer.concat(chunks));
667
+ return;
668
+ }
669
+ }
670
+ else if (encoding === 'deflate') {
671
+ try {
672
+ body = zlib.inflateSync(body);
673
+ }
674
+ catch {
675
+ const headers = { ...proxyRes.headers };
676
+ res.writeHead(proxyRes.statusCode || 200, headers);
677
+ res.end(Buffer.concat(chunks));
678
+ return;
679
+ }
680
+ }
681
+ else if (encoding === 'br') {
682
+ try {
683
+ body = zlib.brotliDecompressSync(body);
684
+ }
685
+ catch {
686
+ const headers = { ...proxyRes.headers };
687
+ res.writeHead(proxyRes.statusCode || 200, headers);
688
+ res.end(Buffer.concat(chunks));
689
+ return;
690
+ }
691
+ }
692
+ let html = body.toString('utf-8');
693
+ // Inject selection script before </body> or </html> or at end
694
+ const scriptTag = `<script>${SELECTION_SCRIPT}</script>`;
695
+ if (html.toLowerCase().includes('</body>')) {
696
+ html = html.replace(/<\/body>/i, `${scriptTag}</body>`);
697
+ }
698
+ else if (html.toLowerCase().includes('</html>')) {
699
+ html = html.replace(/<\/html>/i, `${scriptTag}</html>`);
700
+ }
701
+ else {
702
+ // Fallback: append at end
703
+ html += scriptTag;
704
+ }
705
+ // Prepare response
706
+ const responseBody = Buffer.from(html, 'utf-8');
707
+ // Copy headers, update content-length, remove encoding (we decompressed)
708
+ const headers = { ...proxyRes.headers };
709
+ delete headers['content-encoding'];
710
+ delete headers['transfer-encoding'];
711
+ headers['content-length'] = responseBody.length;
712
+ res.writeHead(proxyRes.statusCode || 200, headers);
713
+ res.end(responseBody);
714
+ }
715
+ catch (err) {
716
+ onError?.(err instanceof Error ? err : new Error(String(err)));
717
+ // On error, try to pass through original response
718
+ if (!res.headersSent) {
719
+ res.writeHead(proxyRes.statusCode || 500, proxyRes.headers);
720
+ res.end(Buffer.concat(chunks));
721
+ }
722
+ }
723
+ });
724
+ proxyRes.on('error', (err) => {
725
+ onError?.(err);
726
+ if (!res.headersSent) {
727
+ res.writeHead(502, { 'Content-Type': 'text/plain' });
728
+ res.end(`Proxy response error: ${err.message}`);
729
+ }
730
+ });
731
+ });
732
+ // Handle proxy errors
733
+ proxy.on('error', (err, req, res) => {
734
+ onError?.(err);
735
+ if (res && 'writeHead' in res && !res.headersSent) {
736
+ res.writeHead(502, { 'Content-Type': 'text/plain' });
737
+ res.end(`Proxy error: ${err.message}`);
738
+ }
739
+ });
740
+ // Start server and return promise
741
+ return new Promise((resolve, reject) => {
742
+ server.on('error', (err) => {
743
+ if (err.code === 'EADDRINUSE') {
744
+ reject(new Error(`Injection proxy port ${proxyPort} is already in use`));
745
+ }
746
+ else {
747
+ reject(err);
748
+ }
749
+ });
750
+ server.listen(proxyPort, '127.0.0.1', () => {
751
+ log(`[injection-proxy] Started on port ${proxyPort} → localhost:${targetPort}`);
752
+ resolve({
753
+ port: proxyPort,
754
+ server,
755
+ close: () => new Promise((resolveClose) => {
756
+ // Add timeout to prevent hanging if connections don't close
757
+ const CLOSE_TIMEOUT_MS = 2000;
758
+ let resolved = false;
759
+ const timeoutId = setTimeout(() => {
760
+ if (!resolved) {
761
+ resolved = true;
762
+ log(`[injection-proxy] Force closing after ${CLOSE_TIMEOUT_MS}ms timeout`);
763
+ // Force destroy the server if it hasn't closed
764
+ try {
765
+ server.closeAllConnections?.();
766
+ }
767
+ catch {
768
+ // closeAllConnections may not be available in older Node versions
769
+ }
770
+ resolveClose();
771
+ }
772
+ }, CLOSE_TIMEOUT_MS);
773
+ proxy.close(() => {
774
+ server.close(() => {
775
+ if (!resolved) {
776
+ resolved = true;
777
+ clearTimeout(timeoutId);
778
+ log(`[injection-proxy] Stopped`);
779
+ }
780
+ resolveClose();
781
+ });
782
+ });
783
+ }),
784
+ });
785
+ });
786
+ });
787
+ }
788
+ /**
789
+ * Check if a port is available
790
+ */
791
+ async function isPortAvailable(port) {
792
+ return new Promise((resolve) => {
793
+ const server = http.createServer();
794
+ server.once('error', () => resolve(false));
795
+ server.once('listening', () => {
796
+ server.close(() => resolve(true));
797
+ });
798
+ server.listen(port, '127.0.0.1');
799
+ });
800
+ }
801
+ /**
802
+ * Find an available port starting from the given port
803
+ */
804
+ async function findAvailablePort(startPort, maxAttempts = 10) {
805
+ for (let i = 0; i < maxAttempts; i++) {
806
+ const port = startPort + i;
807
+ if (await isPortAvailable(port)) {
808
+ return port;
809
+ }
810
+ }
811
+ throw new Error(`No available port found starting from ${startPort}`);
812
+ }
813
+
814
+ /** Default port for the injection proxy */
815
+ const DEFAULT_INJECTION_PROXY_PORT = 4000;
816
+ class TunnelManager extends EventEmitter {
817
+ constructor() {
818
+ super(...arguments);
819
+ this.tunnels = new Map();
820
+ this.cloudflaredPath = null;
821
+ this.silent = false; // Suppress console output
822
+ }
823
+ /**
824
+ * Set silent mode (for TUI)
825
+ */
826
+ setSilent(silent) {
827
+ this.silent = silent;
828
+ }
829
+ /**
830
+ * Conditional logging
831
+ */
832
+ log(...args) {
833
+ if (!this.silent) {
834
+ console.log(...args);
835
+ }
836
+ }
837
+ /**
838
+ * Create a tunnel for a specific port
839
+ * Returns the public tunnel URL
840
+ *
841
+ * The tunnel is created through an injection proxy that adds the element
842
+ * selection script to HTML responses. This enables the "select element"
843
+ * feature to work when the frontend is hosted remotely.
844
+ */
845
+ async createTunnel(port, maxRetries = 5) {
846
+ // Check if tunnel already exists for this port
847
+ if (this.tunnels.has(port)) {
848
+ const existing = this.tunnels.get(port);
849
+ this.log(`🔗 Tunnel already exists for port ${port}: ${existing.url}`);
850
+ return existing.url;
851
+ }
852
+ // Ensure cloudflared is installed
853
+ if (!this.cloudflaredPath) {
854
+ this.cloudflaredPath = await ensureCloudflared(this.silent);
855
+ }
856
+ // Step 1: Start injection proxy
857
+ // This proxy injects the element selection script into HTML responses
858
+ let injectionProxy;
859
+ let proxyPort = DEFAULT_INJECTION_PROXY_PORT;
860
+ try {
861
+ // Find an available port for the proxy
862
+ proxyPort = await findAvailablePort(DEFAULT_INJECTION_PROXY_PORT);
863
+ injectionProxy = await createInjectionProxy({
864
+ targetPort: port,
865
+ proxyPort,
866
+ onError: (err) => this.log(`[injection-proxy] Error: ${err.message}`),
867
+ log: (...args) => this.log(...args),
868
+ });
869
+ this.log(`✅ Injection proxy started: localhost:${proxyPort} → localhost:${port}`);
870
+ }
871
+ catch (err) {
872
+ // Fallback: tunnel directly to dev server (selection won't work but preview will)
873
+ this.log(`⚠️ Injection proxy failed to start: ${err instanceof Error ? err.message : String(err)}`);
874
+ this.log(`⚠️ Falling back to direct tunnel (element selection will not work)`);
875
+ proxyPort = port; // Fall back to direct connection
876
+ }
877
+ // Step 2: Create tunnel to proxy port (or dev server if proxy failed)
878
+ const tunnelTargetPort = injectionProxy ? proxyPort : port;
879
+ // Try creating tunnel with smart retries
880
+ const errors = [];
881
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
882
+ try {
883
+ return await this._createTunnelAttempt(port, tunnelTargetPort, injectionProxy);
884
+ }
885
+ catch (error) {
886
+ const errorMsg = error?.message || String(error);
887
+ errors.push(errorMsg);
888
+ console.error(`Tunnel creation attempt ${attempt}/${maxRetries} failed:`, errorMsg);
889
+ // Check if this is a permanent error (fail fast)
890
+ if (this._isPermanentError(errorMsg)) {
891
+ // Clean up injection proxy on permanent failure
892
+ if (injectionProxy) {
893
+ await injectionProxy.close().catch(() => { });
894
+ }
895
+ throw new Error(`Permanent failure: ${errorMsg}`);
896
+ }
897
+ if (attempt === maxRetries) {
898
+ // Clean up injection proxy on final failure
899
+ if (injectionProxy) {
900
+ await injectionProxy.close().catch(() => { });
901
+ }
902
+ throw new Error(`Failed to create tunnel after ${maxRetries} attempts: ${errors.join('; ')}`);
903
+ }
904
+ // Exponential backoff with jitter to prevent thundering herd
905
+ const baseDelay = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s, 16s
906
+ const jitter = Math.random() * 1000; // 0-1s random jitter
907
+ const delay = baseDelay + jitter;
908
+ this.log(`Retrying in ${Math.round(delay)}ms...`);
909
+ await new Promise(resolve => setTimeout(resolve, delay));
910
+ }
911
+ }
912
+ // Clean up injection proxy on failure
913
+ if (injectionProxy) {
914
+ await injectionProxy.close().catch(() => { });
915
+ }
916
+ throw new Error('Tunnel creation failed after all retries');
917
+ }
918
+ /**
919
+ * Check if an error is permanent (no point retrying)
920
+ */
921
+ _isPermanentError(errorMsg) {
922
+ const permanentErrors = [
923
+ 'port already in use',
924
+ 'cloudflared not found',
925
+ 'permission denied',
926
+ 'cannot find',
927
+ 'enoent',
928
+ 'eacces',
929
+ ];
930
+ const lowerMsg = errorMsg.toLowerCase();
931
+ return permanentErrors.some(err => lowerMsg.includes(err));
932
+ }
933
+ /**
934
+ * Extract tunnel URL from cloudflared output
935
+ */
936
+ _extractTunnelUrl(output) {
937
+ // Format: "Your quick Tunnel has been created! Visit it at: https://xxx.trycloudflare.com"
938
+ // Or just: "https://xxx.trycloudflare.com"
939
+ const match = output.match(/https:\/\/[a-zA-Z0-9-]+\.trycloudflare\.com/);
940
+ return match ? match[0] : null;
941
+ }
942
+ /**
943
+ * Verify tunnel is actually responding (async, non-blocking)
944
+ * This runs in background and only logs results
945
+ */
946
+ async _verifyTunnelReady(url, maxWaitMs = 15000) {
947
+ const startTime = Date.now();
948
+ const checkInterval = 1000; // Check every 1 second (less aggressive)
949
+ this.log(`🔍 [Background] Verifying tunnel is ready: ${url}`);
950
+ while (Date.now() - startTime < maxWaitMs) {
951
+ try {
952
+ const controller = new AbortController();
953
+ const timeoutId = setTimeout(() => controller.abort(), 2000);
954
+ const response = await fetch(url, {
955
+ method: 'HEAD',
956
+ signal: controller.signal,
957
+ });
958
+ clearTimeout(timeoutId);
959
+ // Any response (even errors) means tunnel is connected
960
+ // We just need to verify it's resolving and routing
961
+ if (response.status < 500 || response.ok) {
962
+ const elapsed = Date.now() - startTime;
963
+ this.log(`✅ [Background] Tunnel verified in ${elapsed}ms`);
964
+ return true;
965
+ }
966
+ }
967
+ catch (error) {
968
+ // Expected while DNS propagates or tunnel initializes
969
+ // Will keep retrying silently
970
+ }
971
+ await new Promise(resolve => setTimeout(resolve, checkInterval));
972
+ }
973
+ this.log(`⏱️ [Background] Verification timeout after ${maxWaitMs}ms (tunnel may still work)`);
974
+ return false;
975
+ }
976
+ /**
977
+ * Single attempt to create a tunnel
978
+ * @param devServerPort - The original dev server port (used as key in tunnels map)
979
+ * @param tunnelTargetPort - The port to tunnel to (proxy port or dev server port if proxy failed)
980
+ * @param injectionProxy - Optional injection proxy instance for cleanup
981
+ */
982
+ async _createTunnelAttempt(devServerPort, tunnelTargetPort, injectionProxy) {
983
+ return new Promise((resolve, reject) => {
984
+ const isUsingProxy = tunnelTargetPort !== devServerPort;
985
+ this.log(`[tunnel] Creating tunnel for port ${tunnelTargetPort}${isUsingProxy ? ` (proxy for dev server on ${devServerPort})` : ''}...`);
986
+ // Direct binary execution with unbuffered streams
987
+ const proc = spawn(this.cloudflaredPath, [
988
+ 'tunnel',
989
+ '--url', `http://localhost:${tunnelTargetPort}`,
990
+ '--no-autoupdate',
991
+ ], {
992
+ cwd: process.cwd(),
993
+ stdio: ['ignore', 'pipe', 'pipe'],
994
+ });
995
+ // Set streams to unbuffered mode immediately for responsive output
996
+ if (proc.stdout) {
997
+ proc.stdout.setEncoding('utf8');
998
+ proc.stdout.resume();
999
+ }
1000
+ if (proc.stderr) {
1001
+ proc.stderr.setEncoding('utf8');
1002
+ proc.stderr.resume();
1003
+ }
1004
+ this.log(`[tunnel] Cloudflared spawned with PID: ${proc.pid}`);
1005
+ let resolved = false;
1006
+ let tunnelUrl = null;
1007
+ let tunnelRegistered = false;
1008
+ const timeout = setTimeout(() => {
1009
+ if (!resolved) {
1010
+ proc.kill();
1011
+ reject(new Error('Tunnel creation timeout (30s)'));
1012
+ }
1013
+ }, 30000);
1014
+ // Shared handler for both stdout and stderr
1015
+ const handleOutput = async (data) => {
1016
+ const output = data.toString();
1017
+ // Step 1: Extract URL
1018
+ if (!tunnelUrl) {
1019
+ const url = this._extractTunnelUrl(output);
1020
+ if (url) {
1021
+ tunnelUrl = url;
1022
+ this.log(`✅ Tunnel URL received: ${url} → localhost:${tunnelTargetPort}${isUsingProxy ? ` → localhost:${devServerPort}` : ''}`);
1023
+ // Store with dev server port as key, but include proxy info
1024
+ this.tunnels.set(devServerPort, {
1025
+ url,
1026
+ port: devServerPort,
1027
+ proxyPort: tunnelTargetPort,
1028
+ process: proc,
1029
+ injectionProxy,
1030
+ });
1031
+ }
1032
+ }
1033
+ // Step 2: Wait for tunnel registration (edge connection established)
1034
+ if (tunnelUrl && !tunnelRegistered && output.includes('Registered tunnel connection')) {
1035
+ tunnelRegistered = true;
1036
+ this.log(`✅ Tunnel registered with Cloudflare edge (DNS propagating)`);
1037
+ // Wait 3 seconds after registration for DNS to propagate
1038
+ this.log(`⏳ Waiting 3 seconds for DNS to fully propagate...`);
1039
+ await new Promise(r => setTimeout(r, 3000));
1040
+ // Step 3: Return only after registration + 3 second buffer
1041
+ if (!resolved) {
1042
+ resolved = true;
1043
+ clearTimeout(timeout);
1044
+ this.log(`✅ Tunnel ready: ${tunnelUrl}`);
1045
+ if (isUsingProxy) {
1046
+ this.log(`✅ Element selection enabled via injection proxy`);
1047
+ }
1048
+ // Note: Backend verification skipped for localhost tunnels
1049
+ // The tunnel connects localhost to Cloudflare - backend can't verify it
1050
+ // Frontend will verify DNS before loading in iframe
1051
+ resolve(tunnelUrl);
1052
+ }
1053
+ }
1054
+ };
1055
+ proc.stdout.on('data', handleOutput);
1056
+ proc.stderr.on('data', (data) => {
1057
+ const output = data.toString();
1058
+ // Log errors (cloudflared uses stderr for all output)
1059
+ // Only show real errors, not shutdown messages
1060
+ const lower = output.toLowerCase();
1061
+ if ((lower.includes('error') || lower.includes('fatal')) &&
1062
+ !lower.includes('context canceled') &&
1063
+ !lower.includes('connection terminated') &&
1064
+ !lower.includes('no more connections active')) {
1065
+ this.log(`[cloudflared:${devServerPort}] ${output.trim()}`);
1066
+ }
1067
+ // Check for tunnel URL in stderr too
1068
+ handleOutput(data);
1069
+ });
1070
+ proc.on('exit', (code, signal) => {
1071
+ this.log(`Tunnel exited for port ${devServerPort} with code ${code} signal ${signal}`);
1072
+ // Clean up injection proxy when tunnel exits
1073
+ const tunnel = this.tunnels.get(devServerPort);
1074
+ if (tunnel?.injectionProxy) {
1075
+ tunnel.injectionProxy.close().catch(() => { });
1076
+ }
1077
+ this.tunnels.delete(devServerPort);
1078
+ this.emit('tunnel-closed', devServerPort);
1079
+ });
1080
+ proc.on('error', (error) => {
1081
+ if (!resolved) {
1082
+ clearTimeout(timeout);
1083
+ reject(error);
1084
+ }
1085
+ });
1086
+ });
1087
+ }
1088
+ /**
1089
+ * Close tunnel for a specific port
1090
+ */
1091
+ async closeTunnel(port) {
1092
+ const tunnel = this.tunnels.get(port);
1093
+ if (!tunnel) {
1094
+ this.log(`No tunnel found for port ${port}`);
1095
+ return;
1096
+ }
1097
+ this.log(`🔗 Closing tunnel for port ${port}...`);
1098
+ // Close injection proxy first (if exists) with timeout
1099
+ if (tunnel.injectionProxy) {
1100
+ try {
1101
+ // Use Promise.race to enforce timeout - injection proxy close can hang
1102
+ // if there are active HTTP keep-alive connections
1103
+ const PROXY_CLOSE_TIMEOUT_MS = 3000;
1104
+ await Promise.race([
1105
+ tunnel.injectionProxy.close(),
1106
+ new Promise((resolve) => setTimeout(() => {
1107
+ this.log(`⚠️ Injection proxy close timed out after ${PROXY_CLOSE_TIMEOUT_MS}ms, continuing...`);
1108
+ resolve();
1109
+ }, PROXY_CLOSE_TIMEOUT_MS))
1110
+ ]);
1111
+ this.log(`✅ Injection proxy closed for port ${port}`);
1112
+ }
1113
+ catch (err) {
1114
+ this.log(`⚠️ Error closing injection proxy: ${err instanceof Error ? err.message : String(err)}`);
1115
+ }
1116
+ }
1117
+ return new Promise((resolve) => {
1118
+ const timeout = setTimeout(() => {
1119
+ // Force kill if not dead after 1 second
1120
+ if (!tunnel.process.killed) {
1121
+ tunnel.process.kill('SIGKILL');
1122
+ }
1123
+ this.tunnels.delete(port);
1124
+ resolve();
1125
+ }, 1000);
1126
+ tunnel.process.once('exit', () => {
1127
+ clearTimeout(timeout);
1128
+ this.tunnels.delete(port);
1129
+ resolve();
1130
+ });
1131
+ // Send SIGTERM
1132
+ tunnel.process.kill('SIGTERM');
1133
+ });
1134
+ }
1135
+ /**
1136
+ * Close all active tunnels
1137
+ */
1138
+ async closeAll() {
1139
+ this.log(`🔗 Closing ${this.tunnels.size} active tunnel(s)...`);
1140
+ const ports = Array.from(this.tunnels.keys());
1141
+ await Promise.all(ports.map(port => this.closeTunnel(port)));
1142
+ }
1143
+ /**
1144
+ * Get tunnel URL for a specific port (if exists)
1145
+ */
1146
+ getTunnelUrl(port) {
1147
+ const tunnel = this.tunnels.get(port);
1148
+ return tunnel ? tunnel.url : null;
1149
+ }
1150
+ /**
1151
+ * Get all active tunnels
1152
+ */
1153
+ getActiveTunnels() {
1154
+ return Array.from(this.tunnels.values()).map(({ port, url }) => ({ port, url }));
1155
+ }
1156
+ }
1157
+ // Singleton instance
1158
+ const tunnelManager = new TunnelManager();
1159
+
1160
+ export { TunnelManager, tunnelManager };
1161
+ //# sourceMappingURL=manager-CvGX9qqe.js.map