capdebug 0.2.0 → 0.3.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.
Files changed (133) hide show
  1. package/README.md +30 -0
  2. package/android/build/.transforms/105231053f5053e70f17f15178bed793/results.bin +1 -0
  3. package/android/build/.transforms/7c2b02c40854250ff97be5e9e723091e/results.bin +1 -0
  4. package/android/build/.transforms/ca1c0f6bb67b5238fd83a40c3a1c0ddd/results.bin +1 -0
  5. package/android/build/.transforms/ca1c0f6bb67b5238fd83a40c3a1c0ddd/transformed/classes/classes_dex/classes.dex +0 -0
  6. package/android/build/intermediates/aapt_friendly_merged_manifests/debug/aapt/AndroidManifest.xml +9 -0
  7. package/android/build/intermediates/aapt_friendly_merged_manifests/debug/aapt/output-metadata.json +18 -0
  8. package/android/build/intermediates/aar_metadata/debug/aar-metadata.properties +6 -0
  9. package/android/build/intermediates/annotation_processor_list/debug/annotationProcessors.json +1 -0
  10. package/android/build/intermediates/compile_library_classes_jar/debug/classes.jar +0 -0
  11. package/android/build/intermediates/compile_r_class_jar/debug/R.jar +0 -0
  12. package/android/build/intermediates/compile_symbol_list/debug/R.txt +0 -0
  13. package/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties +1 -0
  14. package/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml +2 -0
  15. package/android/build/intermediates/incremental/mergeDebugJniLibFolders/merger.xml +2 -0
  16. package/android/build/intermediates/incremental/mergeDebugShaders/merger.xml +2 -0
  17. package/android/build/intermediates/incremental/packageDebugAssets/merger.xml +2 -0
  18. package/android/build/intermediates/java_res/debug/out/META-INF/capdebug_debug.kotlin_module +0 -0
  19. package/android/build/intermediates/local_only_symbol_list/debug/R-def.txt +2 -0
  20. package/android/build/intermediates/manifest_merge_blame_file/debug/manifest-merger-blame-debug-report.txt +11 -0
  21. package/android/build/intermediates/merged_manifest/debug/AndroidManifest.xml +9 -0
  22. package/android/build/intermediates/navigation_json/debug/navigation.json +1 -0
  23. package/android/build/intermediates/runtime_library_classes_jar/debug/classes.jar +0 -0
  24. package/android/build/intermediates/symbol_list_with_package_name/debug/package-aware-r.txt +1 -0
  25. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab +0 -0
  26. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream +0 -0
  27. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream.len +0 -0
  28. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.len +0 -0
  29. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.values.at +0 -0
  30. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i +0 -0
  31. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i.len +0 -0
  32. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab +0 -0
  33. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream +0 -0
  34. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream.len +0 -0
  35. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.len +0 -0
  36. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.values.at +0 -0
  37. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i +0 -0
  38. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i.len +0 -0
  39. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab +0 -0
  40. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream +0 -0
  41. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream.len +0 -0
  42. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.len +0 -0
  43. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.values.at +0 -0
  44. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i +0 -0
  45. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i.len +0 -0
  46. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab +0 -0
  47. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream +0 -0
  48. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream.len +0 -0
  49. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.len +0 -0
  50. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.values.at +0 -0
  51. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i +0 -0
  52. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i.len +0 -0
  53. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab +0 -0
  54. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream +0 -0
  55. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream.len +0 -0
  56. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.len +0 -0
  57. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.at +0 -0
  58. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i +0 -0
  59. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i.len +0 -0
  60. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab +0 -0
  61. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream +0 -0
  62. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream.len +0 -0
  63. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.len +0 -0
  64. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.values.at +0 -0
  65. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i +0 -0
  66. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i.len +0 -0
  67. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab +0 -0
  68. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.keystream +0 -0
  69. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.keystream.len +0 -0
  70. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.len +0 -0
  71. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.values.at +0 -0
  72. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab_i +0 -0
  73. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab_i.len +0 -0
  74. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab +0 -0
  75. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.keystream +0 -0
  76. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.keystream.len +0 -0
  77. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.len +0 -0
  78. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.values.at +0 -0
  79. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab_i +0 -0
  80. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab_i.len +0 -0
  81. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/counters.tab +2 -0
  82. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab +0 -0
  83. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream +0 -0
  84. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream.len +0 -0
  85. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.len +0 -0
  86. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.values.at +0 -0
  87. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i +0 -0
  88. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i.len +0 -0
  89. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab +0 -0
  90. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream +0 -0
  91. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream.len +0 -0
  92. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.len +0 -0
  93. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.values.at +0 -0
  94. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i.len +0 -0
  95. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab +0 -0
  96. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream +0 -0
  97. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream.len +0 -0
  98. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.len +0 -0
  99. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.values.at +0 -0
  100. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab_i +0 -0
  101. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab_i.len +0 -0
  102. package/android/build/kotlin/compileDebugKotlin/cacheable/last-build.bin +0 -0
  103. package/android/build/kotlin/compileDebugKotlin/classpath-snapshot/shrunk-classpath-snapshot.bin +0 -0
  104. package/android/build/kotlin/compileDebugKotlin/local-state/build-history.bin +0 -0
  105. package/android/build/outputs/logs/manifest-merger-debug-report.txt +20 -0
  106. package/android/build/tmp/kotlin-classes/debug/META-INF/capdebug_debug.kotlin_module +0 -0
  107. package/android/build/tmp/kotlin-classes/debug/com/capdebug/plugin/CapDebugPlugin$Companion.class +0 -0
  108. package/android/build/tmp/kotlin-classes/debug/com/capdebug/plugin/CapDebugPlugin.class +0 -0
  109. package/bin/capdebug.js +8 -0
  110. package/dist/capacitor/deep-link.d.ts +4 -0
  111. package/dist/cli/index.d.ts +1 -0
  112. package/dist/cli/qr.d.ts +15 -0
  113. package/dist/cli/server.d.ts +34 -0
  114. package/dist/cli.js +1868 -0
  115. package/dist/cli.js.map +1 -0
  116. package/dist/index.d.ts +13 -0
  117. package/dist/plugin.cjs.js +866 -0
  118. package/dist/plugin.cjs.js.map +1 -1
  119. package/dist/plugin.js +866 -0
  120. package/dist/plugin.js.map +1 -1
  121. package/dist/plugin.mjs +859 -1
  122. package/dist/plugin.mjs.map +1 -1
  123. package/dist/remote/buffer.d.ts +13 -0
  124. package/dist/remote/desktop-viewer-html.d.ts +10 -0
  125. package/dist/remote/pairing-page-html.d.ts +8 -0
  126. package/dist/remote/pairing.d.ts +34 -0
  127. package/dist/remote/protocol.d.ts +14 -0
  128. package/dist/remote/remote-manager.d.ts +19 -0
  129. package/dist/remote/signaling.d.ts +27 -0
  130. package/dist/remote/transport.d.ts +15 -0
  131. package/dist/remote/types.d.ts +69 -0
  132. package/dist/remote/webrtc-transport.d.ts +47 -0
  133. package/package.json +5 -1
package/dist/cli.js ADDED
@@ -0,0 +1,1868 @@
1
+ 'use strict';
2
+
3
+ var child_process = require('child_process');
4
+ var readline = require('readline');
5
+ var http = require('http');
6
+ var os = require('os');
7
+
8
+ function _interopNamespaceDefault(e) {
9
+ var n = Object.create(null);
10
+ if (e) {
11
+ Object.keys(e).forEach(function (k) {
12
+ if (k !== 'default') {
13
+ var d = Object.getOwnPropertyDescriptor(e, k);
14
+ Object.defineProperty(n, k, d.get ? d : {
15
+ enumerable: true,
16
+ get: function () { return e[k]; }
17
+ });
18
+ }
19
+ });
20
+ }
21
+ n.default = e;
22
+ return Object.freeze(n);
23
+ }
24
+
25
+ var readline__namespace = /*#__PURE__*/_interopNamespaceDefault(readline);
26
+ var http__namespace = /*#__PURE__*/_interopNamespaceDefault(http);
27
+ var os__namespace = /*#__PURE__*/_interopNamespaceDefault(os);
28
+
29
+ class PairingHelper {
30
+ /**
31
+ * Creates a new pairing session with given TTL (default 5 minutes).
32
+ */
33
+ static createSession(ttlMs = 5 * 60 * 1e3) {
34
+ const now = Date.now();
35
+ return {
36
+ id: this.generateSessionId(),
37
+ createdAt: now,
38
+ expiresAt: now + ttlMs,
39
+ status: "waiting"
40
+ };
41
+ }
42
+ /**
43
+ * Generates a short, human-readable session ID (e.g. "K7P4-M2" or "7HQ2-9X").
44
+ */
45
+ static generateSessionId() {
46
+ const chars = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
47
+ const bytes = this.getRandomBytes(4);
48
+ let id = "";
49
+ for (let i = 0; i < 4; i++) {
50
+ id += chars[bytes[i] % chars.length];
51
+ }
52
+ const suffix = this.getRandomBytes(2);
53
+ id += "-" + chars[suffix[0] % chars.length] + chars[suffix[1] % chars.length];
54
+ return id;
55
+ }
56
+ /**
57
+ * Generates a cryptographically strong 128-bit random secret string.
58
+ */
59
+ static generateSecret() {
60
+ const bytes = this.getRandomBytes(16);
61
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
62
+ }
63
+ /**
64
+ * Formats a web pairing URL where the secret is stored strictly in the URL fragment (#SECRET).
65
+ */
66
+ static formatPairingUrl(baseUrl, sessionId, secret) {
67
+ const cleanBase = baseUrl.replace(/\/+$/, "");
68
+ return `${cleanBase}/pair?s=${encodeURIComponent(sessionId)}#${secret}`;
69
+ }
70
+ /**
71
+ * Formats a Capacitor mobile deep link URL.
72
+ */
73
+ static formatDeepLinkUrl(sessionId, secret, signalingUrl) {
74
+ let url = `capdebug://pair?session=${encodeURIComponent(sessionId)}&secret=${encodeURIComponent(secret)}`;
75
+ if (signalingUrl) {
76
+ url += `&server=${encodeURIComponent(signalingUrl)}`;
77
+ }
78
+ return url;
79
+ }
80
+ /**
81
+ * Parses a web pairing URL or Capacitor deep-link URL into session and secret components.
82
+ */
83
+ static parseUrl(urlStr) {
84
+ if (!urlStr || typeof urlStr !== "string") return null;
85
+ try {
86
+ if (urlStr.startsWith("capdebug://")) {
87
+ const queryIdx = urlStr.indexOf("?");
88
+ if (queryIdx === -1) return null;
89
+ const search = urlStr.substring(queryIdx + 1);
90
+ const params = new URLSearchParams(search);
91
+ const sessionId2 = params.get("session") || params.get("s");
92
+ const secret2 = params.get("secret") || params.get("sec") || "";
93
+ const signalingUrl2 = params.get("server") || params.get("sig") || void 0;
94
+ if (sessionId2 && secret2) {
95
+ return { sessionId: sessionId2, secret: secret2, signalingUrl: signalingUrl2, rawUrl: urlStr };
96
+ }
97
+ return null;
98
+ }
99
+ const url = new URL(urlStr);
100
+ let sessionId = url.searchParams.get("s") || url.searchParams.get("session");
101
+ if (!sessionId && url.pathname.includes("/s/")) {
102
+ const parts = url.pathname.split("/s/");
103
+ if (parts[1]) {
104
+ sessionId = decodeURIComponent(parts[1].split("/")[0]);
105
+ }
106
+ }
107
+ let secret = url.hash.replace(/^#/, "");
108
+ if (!secret) {
109
+ secret = url.searchParams.get("secret") || url.searchParams.get("sec") || "";
110
+ }
111
+ const signalingUrl = url.searchParams.get("server") || `${url.protocol}//${url.host}`;
112
+ if (sessionId && secret) {
113
+ return {
114
+ sessionId: sessionId.trim(),
115
+ secret: secret.trim(),
116
+ signalingUrl,
117
+ rawUrl: urlStr
118
+ };
119
+ }
120
+ return null;
121
+ } catch {
122
+ return null;
123
+ }
124
+ }
125
+ static getRandomBytes(length) {
126
+ const bytes = new Uint8Array(length);
127
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
128
+ crypto.getRandomValues(bytes);
129
+ } else {
130
+ for (let i = 0; i < length; i++) {
131
+ bytes[i] = Math.floor(Math.random() * 256);
132
+ }
133
+ }
134
+ return bytes;
135
+ }
136
+ }
137
+
138
+ function getDesktopViewerHtml(options) {
139
+ return `<!DOCTYPE html>
140
+ <html lang="en">
141
+ <head>
142
+ <meta charset="UTF-8">
143
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
144
+ <title>CapDebug Remote Viewer</title>
145
+ <style>
146
+ :root {
147
+ --bg: #0b0f19;
148
+ --surface: #111827;
149
+ --surface-elevated: #1f293d;
150
+ --border: rgba(255, 255, 255, 0.08);
151
+ --border-focus: rgba(56, 189, 248, 0.5);
152
+ --accent: #38bdf8;
153
+ --accent-glow: rgba(56, 189, 248, 0.2);
154
+ --text: #f8fafc;
155
+ --text-muted: #94a3b8;
156
+ --text-dim: #64748b;
157
+ --green: #10b981;
158
+ --amber: #f59e0b;
159
+ --rose: #f43f5e;
160
+ --purple: #a855f7;
161
+ --blue: #3b82f6;
162
+ }
163
+ * { box-sizing: border-box; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; }
164
+ body {
165
+ background-color: var(--bg);
166
+ color: var(--text);
167
+ height: 100vh;
168
+ display: flex;
169
+ flex-direction: column;
170
+ overflow: hidden;
171
+ }
172
+
173
+ /* Header */
174
+ header {
175
+ background: var(--surface);
176
+ border-bottom: 1px solid var(--border);
177
+ padding: 12px 20px;
178
+ display: flex;
179
+ align-items: center;
180
+ justify-content: space-between;
181
+ gap: 16px;
182
+ flex-shrink: 0;
183
+ }
184
+ .brand-section {
185
+ display: flex;
186
+ align-items: center;
187
+ gap: 12px;
188
+ }
189
+ .logo {
190
+ font-weight: 800;
191
+ font-size: 16px;
192
+ letter-spacing: -0.5px;
193
+ display: flex;
194
+ align-items: center;
195
+ gap: 8px;
196
+ color: #fff;
197
+ }
198
+ .logo-badge {
199
+ background: rgba(56, 189, 248, 0.15);
200
+ color: var(--accent);
201
+ border: 1px solid rgba(56, 189, 248, 0.3);
202
+ padding: 2px 8px;
203
+ border-radius: 6px;
204
+ font-size: 11px;
205
+ font-weight: 700;
206
+ letter-spacing: 0.5px;
207
+ }
208
+ .status-badge {
209
+ display: inline-flex;
210
+ align-items: center;
211
+ gap: 6px;
212
+ padding: 4px 10px;
213
+ border-radius: 999px;
214
+ font-size: 12px;
215
+ font-weight: 600;
216
+ background: rgba(255, 255, 255, 0.05);
217
+ border: 1px solid var(--border);
218
+ }
219
+ .status-dot {
220
+ width: 8px;
221
+ height: 8px;
222
+ border-radius: 50%;
223
+ background: var(--text-dim);
224
+ }
225
+ .status-dot.waiting { background: var(--amber); box-shadow: 0 0 8px var(--amber); animation: pulse 1.5s infinite; }
226
+ .status-dot.connected { background: var(--green); box-shadow: 0 0 8px var(--green); }
227
+ .status-dot.failed { background: var(--rose); box-shadow: 0 0 8px var(--rose); }
228
+
229
+ @keyframes pulse {
230
+ 0%, 100% { opacity: 1; transform: scale(1); }
231
+ 50% { opacity: 0.4; transform: scale(1.3); }
232
+ }
233
+
234
+ .meta-metrics {
235
+ display: flex;
236
+ align-items: center;
237
+ gap: 12px;
238
+ font-size: 12px;
239
+ color: var(--text-muted);
240
+ }
241
+ .metric-pill {
242
+ background: rgba(255, 255, 255, 0.04);
243
+ border: 1px solid var(--border);
244
+ padding: 3px 8px;
245
+ border-radius: 6px;
246
+ display: flex;
247
+ align-items: center;
248
+ gap: 4px;
249
+ }
250
+ .metric-pill strong { color: var(--text); }
251
+
252
+ .header-actions {
253
+ display: flex;
254
+ align-items: center;
255
+ gap: 8px;
256
+ }
257
+ .btn {
258
+ background: var(--surface-elevated);
259
+ color: var(--text);
260
+ border: 1px solid var(--border);
261
+ padding: 6px 12px;
262
+ border-radius: 8px;
263
+ font-size: 12px;
264
+ font-weight: 600;
265
+ cursor: pointer;
266
+ display: inline-flex;
267
+ align-items: center;
268
+ gap: 6px;
269
+ transition: all 0.15s;
270
+ }
271
+ .btn:hover {
272
+ background: rgba(255, 255, 255, 0.1);
273
+ border-color: rgba(255, 255, 255, 0.2);
274
+ }
275
+ .btn-primary {
276
+ background: linear-gradient(135deg, #0284c7, #38bdf8);
277
+ color: white;
278
+ border: none;
279
+ }
280
+ .btn-danger {
281
+ background: rgba(244, 63, 94, 0.15);
282
+ border-color: rgba(244, 63, 94, 0.3);
283
+ color: #fda4af;
284
+ }
285
+ .btn-danger:hover {
286
+ background: rgba(244, 63, 94, 0.3);
287
+ }
288
+
289
+ /* Subheader & Filters */
290
+ .controls-bar {
291
+ background: var(--surface);
292
+ border-bottom: 1px solid var(--border);
293
+ padding: 8px 20px;
294
+ display: flex;
295
+ align-items: center;
296
+ justify-content: space-between;
297
+ gap: 16px;
298
+ flex-shrink: 0;
299
+ }
300
+ .tabs-group {
301
+ display: flex;
302
+ align-items: center;
303
+ gap: 4px;
304
+ overflow-x: auto;
305
+ }
306
+ .tab-btn {
307
+ background: transparent;
308
+ border: none;
309
+ color: var(--text-muted);
310
+ padding: 6px 12px;
311
+ border-radius: 6px;
312
+ font-size: 13px;
313
+ font-weight: 600;
314
+ cursor: pointer;
315
+ transition: all 0.15s;
316
+ white-space: nowrap;
317
+ }
318
+ .tab-btn:hover {
319
+ color: var(--text);
320
+ background: rgba(255, 255, 255, 0.04);
321
+ }
322
+ .tab-btn.active {
323
+ color: var(--accent);
324
+ background: rgba(56, 189, 248, 0.12);
325
+ }
326
+
327
+ .filter-group {
328
+ display: flex;
329
+ align-items: center;
330
+ gap: 8px;
331
+ }
332
+ .search-input {
333
+ background: var(--bg);
334
+ border: 1px solid var(--border);
335
+ color: var(--text);
336
+ padding: 6px 12px;
337
+ border-radius: 6px;
338
+ font-size: 12px;
339
+ width: 220px;
340
+ outline: none;
341
+ transition: border 0.15s;
342
+ }
343
+ .search-input:focus {
344
+ border-color: var(--border-focus);
345
+ }
346
+ .level-select {
347
+ background: var(--bg);
348
+ border: 1px solid var(--border);
349
+ color: var(--text);
350
+ padding: 6px 10px;
351
+ border-radius: 6px;
352
+ font-size: 12px;
353
+ outline: none;
354
+ cursor: pointer;
355
+ }
356
+
357
+ /* Main Container */
358
+ .main-content {
359
+ flex: 1;
360
+ display: flex;
361
+ overflow: hidden;
362
+ position: relative;
363
+ }
364
+
365
+ /* Waiting / Empty Overlay */
366
+ .overlay-banner {
367
+ position: absolute;
368
+ top: 0;
369
+ left: 0;
370
+ right: 0;
371
+ bottom: 0;
372
+ background: rgba(11, 15, 25, 0.9);
373
+ backdrop-filter: blur(8px);
374
+ display: flex;
375
+ flex-direction: column;
376
+ align-items: center;
377
+ justify-content: center;
378
+ z-index: 50;
379
+ text-align: center;
380
+ padding: 24px;
381
+ }
382
+ .overlay-card {
383
+ background: var(--surface);
384
+ border: 1px solid var(--border);
385
+ border-radius: 20px;
386
+ padding: 36px 32px;
387
+ max-width: 440px;
388
+ width: 100%;
389
+ box-shadow: 0 20px 50px rgba(0,0,0,0.5);
390
+ }
391
+ .overlay-card h2 {
392
+ font-size: 22px;
393
+ font-weight: 700;
394
+ margin-bottom: 8px;
395
+ }
396
+ .overlay-card p {
397
+ color: var(--text-muted);
398
+ font-size: 14px;
399
+ line-height: 1.5;
400
+ margin-bottom: 24px;
401
+ }
402
+ .session-code {
403
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
404
+ font-size: 20px;
405
+ font-weight: 700;
406
+ color: var(--accent);
407
+ background: rgba(56, 189, 248, 0.08);
408
+ border: 1px solid rgba(56, 189, 248, 0.25);
409
+ padding: 10px 16px;
410
+ border-radius: 12px;
411
+ margin-bottom: 20px;
412
+ display: inline-block;
413
+ letter-spacing: 2px;
414
+ }
415
+
416
+ /* Log Stream */
417
+ .logs-container {
418
+ flex: 1;
419
+ overflow-y: auto;
420
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
421
+ font-size: 12px;
422
+ }
423
+ .log-row {
424
+ display: flex;
425
+ align-items: flex-start;
426
+ gap: 12px;
427
+ padding: 8px 20px;
428
+ border-bottom: 1px solid rgba(255, 255, 255, 0.03);
429
+ transition: background 0.1s;
430
+ cursor: pointer;
431
+ }
432
+ .log-row:hover {
433
+ background: rgba(255, 255, 255, 0.02);
434
+ }
435
+ .log-row.selected {
436
+ background: rgba(56, 189, 248, 0.08);
437
+ }
438
+ .log-time {
439
+ color: var(--text-dim);
440
+ white-space: nowrap;
441
+ flex-shrink: 0;
442
+ }
443
+ .pill {
444
+ font-size: 10px;
445
+ font-weight: 700;
446
+ padding: 2px 6px;
447
+ border-radius: 4px;
448
+ text-transform: uppercase;
449
+ flex-shrink: 0;
450
+ }
451
+ .pill-source { background: rgba(255, 255, 255, 0.08); color: var(--text-muted); }
452
+ .pill-level-debug { background: rgba(148, 163, 184, 0.15); color: #cbd5e1; }
453
+ .pill-level-info { background: rgba(56, 189, 248, 0.15); color: #38bdf8; }
454
+ .pill-level-warn { background: rgba(245, 158, 11, 0.15); color: #f59e0b; }
455
+ .pill-level-error { background: rgba(244, 63, 94, 0.15); color: #f43f5e; }
456
+
457
+ .log-msg {
458
+ flex: 1;
459
+ word-break: break-word;
460
+ color: var(--text);
461
+ }
462
+ .log-duration {
463
+ color: var(--accent);
464
+ font-size: 11px;
465
+ margin-left: 8px;
466
+ }
467
+
468
+ /* Detail Inspector Pane */
469
+ .details-pane {
470
+ width: 420px;
471
+ border-left: 1px solid var(--border);
472
+ background: var(--surface);
473
+ display: flex;
474
+ flex-direction: column;
475
+ overflow: hidden;
476
+ }
477
+ .details-header {
478
+ padding: 12px 16px;
479
+ border-bottom: 1px solid var(--border);
480
+ display: flex;
481
+ align-items: center;
482
+ justify-content: space-between;
483
+ font-weight: 600;
484
+ font-size: 13px;
485
+ }
486
+ .details-body {
487
+ flex: 1;
488
+ overflow-y: auto;
489
+ padding: 16px;
490
+ }
491
+ .json-tree {
492
+ background: var(--bg);
493
+ border: 1px solid var(--border);
494
+ border-radius: 8px;
495
+ padding: 12px;
496
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
497
+ font-size: 12px;
498
+ color: #cbd5e1;
499
+ white-space: pre-wrap;
500
+ word-break: break-all;
501
+ }
502
+
503
+ /* Diagnostics Drawer */
504
+ .diag-modal {
505
+ display: none;
506
+ position: fixed;
507
+ top: 50%;
508
+ left: 50%;
509
+ transform: translate(-50%, -50%);
510
+ background: var(--surface);
511
+ border: 1px solid var(--border);
512
+ border-radius: 16px;
513
+ padding: 24px;
514
+ width: 500px;
515
+ max-width: 90vw;
516
+ box-shadow: 0 20px 60px rgba(0,0,0,0.7);
517
+ z-index: 100;
518
+ }
519
+ .diag-modal.show { display: block; }
520
+ .diag-grid {
521
+ display: grid;
522
+ grid-template-columns: 1fr 1fr;
523
+ gap: 12px;
524
+ margin-top: 16px;
525
+ font-size: 13px;
526
+ }
527
+ .diag-item {
528
+ background: var(--bg);
529
+ border: 1px solid var(--border);
530
+ padding: 10px 12px;
531
+ border-radius: 8px;
532
+ }
533
+ .diag-label { color: var(--text-dim); font-size: 11px; text-transform: uppercase; margin-bottom: 4px; }
534
+ .diag-val { font-weight: 600; color: var(--text); }
535
+ </style>
536
+ </head>
537
+ <body>
538
+
539
+ <!-- Top Bar -->
540
+ <header>
541
+ <div class="brand-section">
542
+ <div class="logo">
543
+ CapDebug
544
+ <span class="logo-badge">DESKTOP</span>
545
+ </div>
546
+ <div class="status-badge">
547
+ <span class="status-dot waiting" id="statusDot"></span>
548
+ <span id="statusText">Waiting for device...</span>
549
+ </div>
550
+ </div>
551
+
552
+ <div class="meta-metrics">
553
+ <div class="metric-pill">Device: <strong id="metricDevice">\u2014</strong></div>
554
+ <div class="metric-pill">Route: <strong id="metricRoute">\u2014</strong></div>
555
+ <div class="metric-pill">RTT: <strong id="metricRtt">\u2014</strong></div>
556
+ <div class="metric-pill">Events: <strong id="metricEvents">0</strong></div>
557
+ <div class="metric-pill" id="droppedPill" style="display:none; color:var(--amber);">Dropped: <strong id="metricDropped">0</strong></div>
558
+ </div>
559
+
560
+ <div class="header-actions">
561
+ <button class="btn" id="btnPause">\u23F8 Pause</button>
562
+ <button class="btn" id="btnClear">\u{1F9F9} Clear</button>
563
+ <button class="btn" id="btnSnapshot">\u{1F4F8} Snapshot</button>
564
+ <button class="btn" id="btnDiag">\u{1F4CA} Diag</button>
565
+ </div>
566
+ </header>
567
+
568
+ <!-- Subheader Filters -->
569
+ <div class="controls-bar">
570
+ <div class="tabs-group" id="tabsGroup">
571
+ <button class="tab-btn active" data-source="all">All</button>
572
+ <button class="tab-btn" data-source="console">Console</button>
573
+ <button class="tab-btn" data-source="errors">Errors</button>
574
+ <button class="tab-btn" data-source="network">Network</button>
575
+ <button class="tab-btn" data-source="native">Native</button>
576
+ <button class="tab-btn" data-source="custom">Custom</button>
577
+ <button class="tab-btn" data-source="bridge">Bridge</button>
578
+ </div>
579
+
580
+ <div class="filter-group">
581
+ <input type="text" class="search-input" id="searchInput" placeholder="Filter message or type...">
582
+ <select class="level-select" id="levelSelect">
583
+ <option value="all">All Levels</option>
584
+ <option value="debug">Debug</option>
585
+ <option value="info">Info</option>
586
+ <option value="warn">Warn</option>
587
+ <option value="error">Error</option>
588
+ </select>
589
+ </div>
590
+ </div>
591
+
592
+ <!-- Content -->
593
+ <div class="main-content">
594
+ <!-- Waiting Overlay -->
595
+ <div class="overlay-banner" id="waitingOverlay">
596
+ <div class="overlay-card">
597
+ <h2>Scan Terminal QR Code</h2>
598
+ <p>Point your mobile camera at the terminal to connect CapDebug via WebRTC DataChannel.</p>
599
+ <div class="session-code">${options.sessionId}</div>
600
+ <p style="font-size:12px; margin-bottom:0;">Session active for 5 minutes. Handshake will initiate automatically upon scan.</p>
601
+ </div>
602
+ </div>
603
+
604
+ <!-- Logs Container -->
605
+ <div class="logs-container" id="logsContainer"></div>
606
+
607
+ <!-- Details Pane -->
608
+ <div class="details-pane" id="detailsPane" style="display:none;">
609
+ <div class="details-header">
610
+ <span>Event Details</span>
611
+ <button class="btn" id="btnCloseDetails">\u2715</button>
612
+ </div>
613
+ <div class="details-body" id="detailsBody"></div>
614
+ </div>
615
+ </div>
616
+
617
+ <!-- Diagnostics Modal -->
618
+ <div class="diag-modal" id="diagModal">
619
+ <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px;">
620
+ <h3 style="font-size:16px;">WebRTC Diagnostics</h3>
621
+ <button class="btn" id="btnCloseDiag">\u2715</button>
622
+ </div>
623
+ <div class="diag-grid">
624
+ <div class="diag-item"><div class="diag-label">Session ID</div><div class="diag-val">${options.sessionId}</div></div>
625
+ <div class="diag-item"><div class="diag-label">Transport</div><div class="diag-val">WebRTC DataChannel</div></div>
626
+ <div class="diag-item"><div class="diag-label">ICE State</div><div class="diag-val" id="diagIceState">new</div></div>
627
+ <div class="diag-item"><div class="diag-label">Signaling State</div><div class="diag-val" id="diagSigState">stable</div></div>
628
+ <div class="diag-item"><div class="diag-label">Route Type</div><div class="diag-val" id="diagRoute">Unknown</div></div>
629
+ <div class="diag-item"><div class="diag-label">Candidate</div><div class="diag-val" id="diagCandidate">Host</div></div>
630
+ <div class="diag-item"><div class="diag-label">Protocol</div><div class="diag-val" id="diagProtocol">UDP</div></div>
631
+ <div class="diag-item"><div class="diag-label">Latency (RTT)</div><div class="diag-val" id="diagRtt">\u2014</div></div>
632
+ <div class="diag-item"><div class="diag-label">Events Received</div><div class="diag-val" id="diagReceived">0</div></div>
633
+ <div class="diag-item"><div class="diag-label">Dropped (Backpressure)</div><div class="diag-val" id="diagDropped">0</div></div>
634
+ </div>
635
+ </div>
636
+
637
+ <script>
638
+ (function() {
639
+ const sessionId = "${options.sessionId}";
640
+ const secret = "${options.secret}";
641
+ const signalingUrl = "${options.signalingUrl}";
642
+
643
+ let pc = null;
644
+ let eventsChannel = null;
645
+ let controlChannel = null;
646
+ let sse = null;
647
+
648
+ let events = [];
649
+ let selectedEvent = null;
650
+ let activeSourceFilter = 'all';
651
+ let activeLevelFilter = 'all';
652
+ let searchQuery = '';
653
+ let isPaused = false;
654
+ let totalReceived = 0;
655
+ let droppedCount = 0;
656
+ let rttMs = 0;
657
+ let pingInterval = null;
658
+
659
+ // DOM Elements
660
+ const statusDot = document.getElementById('statusDot');
661
+ const statusText = document.getElementById('statusText');
662
+ const metricDevice = document.getElementById('metricDevice');
663
+ const metricRoute = document.getElementById('metricRoute');
664
+ const metricRtt = document.getElementById('metricRtt');
665
+ const metricEvents = document.getElementById('metricEvents');
666
+ const metricDropped = document.getElementById('metricDropped');
667
+ const droppedPill = document.getElementById('droppedPill');
668
+ const waitingOverlay = document.getElementById('waitingOverlay');
669
+ const logsContainer = document.getElementById('logsContainer');
670
+ const detailsPane = document.getElementById('detailsPane');
671
+ const detailsBody = document.getElementById('detailsBody');
672
+ const btnCloseDetails = document.getElementById('btnCloseDetails');
673
+ const btnPause = document.getElementById('btnPause');
674
+ const btnClear = document.getElementById('btnClear');
675
+ const btnSnapshot = document.getElementById('btnSnapshot');
676
+ const btnDiag = document.getElementById('btnDiag');
677
+ const diagModal = document.getElementById('diagModal');
678
+ const btnCloseDiag = document.getElementById('btnCloseDiag');
679
+
680
+ // --- WebRTC Host Negotiation ---
681
+ function initSignaling() {
682
+ const sseUrl = signalingUrl + '/api/pair/events/' + encodeURIComponent(sessionId) + '?secret=' + encodeURIComponent(secret);
683
+ sse = new EventSource(sseUrl);
684
+
685
+ sse.onmessage = function(e) {
686
+ try {
687
+ const msg = JSON.parse(e.data);
688
+ handleSignaling(msg);
689
+ } catch(err) {
690
+ console.error('[Signaling] Parse error:', err);
691
+ }
692
+ };
693
+
694
+ sse.onerror = function() {
695
+ // SSE connection state
696
+ };
697
+ }
698
+
699
+ async function sendSignaling(msg) {
700
+ try {
701
+ await fetch(signalingUrl + '/api/pair/signal', {
702
+ method: 'POST',
703
+ headers: { 'Content-Type': 'application/json' },
704
+ body: JSON.stringify({
705
+ sessionId: sessionId,
706
+ secret: secret,
707
+ sender: 'host',
708
+ ...msg
709
+ })
710
+ });
711
+ } catch(err) {
712
+ console.error('[Signaling] Send error:', err);
713
+ }
714
+ }
715
+
716
+ async function handleSignaling(msg) {
717
+ if (msg.type === 'peer-joined') {
718
+ // Peer scanned QR and joined signaling session!
719
+ // NOW we create RTCPeerConnection and offer (delayed handshake invariant)
720
+ statusText.innerText = 'Peer joined! Negotiating...';
721
+ await startWebRTCHandshake();
722
+ } else if (msg.type === 'answer') {
723
+ if (pc) {
724
+ await pc.setRemoteDescription(new RTCSessionDescription(msg.payload));
725
+ }
726
+ } else if (msg.type === 'ice-candidate') {
727
+ if (pc && msg.payload) {
728
+ try {
729
+ await pc.addIceCandidate(new RTCIceCandidate(msg.payload));
730
+ } catch(e) {
731
+ console.warn('ICE Candidate add error:', e);
732
+ }
733
+ }
734
+ } else if (msg.type === 'disconnect') {
735
+ setDisconnected();
736
+ }
737
+ }
738
+
739
+ async function startWebRTCHandshake() {
740
+ pc = new RTCPeerConnection({
741
+ iceServers: [
742
+ { urls: 'stun:stun.l.google.com:19302' },
743
+ { urls: 'stun:global.stun.twilio.com:3478' }
744
+ ]
745
+ });
746
+
747
+ pc.onicecandidate = function(event) {
748
+ if (event.candidate) {
749
+ sendSignaling({
750
+ type: 'ice-candidate',
751
+ payload: event.candidate.toJSON()
752
+ });
753
+ }
754
+ };
755
+
756
+ pc.onconnectionstatechange = function() {
757
+ document.getElementById('diagIceState').innerText = pc.iceConnectionState;
758
+ document.getElementById('diagSigState').innerText = pc.signalingState;
759
+
760
+ if (pc.connectionState === 'connected') {
761
+ setConnected();
762
+ } else if (pc.connectionState === 'failed' || pc.connectionState === 'closed') {
763
+ setDisconnected();
764
+ }
765
+ };
766
+
767
+ // Create DataChannels
768
+ eventsChannel = pc.createDataChannel('events', { ordered: true });
769
+ controlChannel = pc.createDataChannel('control', { ordered: true });
770
+
771
+ setupDataChannels();
772
+
773
+ const offer = await pc.createOffer();
774
+ await pc.setLocalDescription(offer);
775
+
776
+ await sendSignaling({
777
+ type: 'offer',
778
+ payload: offer
779
+ });
780
+ }
781
+
782
+ function setupDataChannels() {
783
+ eventsChannel.onopen = checkConnected;
784
+ controlChannel.onopen = checkConnected;
785
+
786
+ eventsChannel.onmessage = function(e) {
787
+ if (typeof e.data !== 'string') return;
788
+ try {
789
+ const parsed = JSON.parse(e.data);
790
+ if (parsed.type === 'event' && parsed.event) {
791
+ appendEvent(parsed.event);
792
+ } else if (parsed.type === 'events-batch' && Array.isArray(parsed.events)) {
793
+ parsed.events.forEach(appendEvent);
794
+ }
795
+ } catch(err) {
796
+ console.error('DataChannel parse error:', err);
797
+ }
798
+ };
799
+
800
+ controlChannel.onmessage = function(e) {
801
+ try {
802
+ const parsed = JSON.parse(e.data);
803
+ if (parsed.type === 'hello') {
804
+ metricDevice.innerText = parsed.payload?.deviceName || 'Mobile Device';
805
+ if (parsed.payload?.platform) {
806
+ metricDevice.innerText += ' (' + parsed.payload.platform + ')';
807
+ }
808
+ controlChannel.send(JSON.stringify({ v: 1, type: 'hello-ack' }));
809
+ } else if (parsed.type === 'pong') {
810
+ const pingTime = parsed.payload?.pingTimestamp || parsed.timestamp;
811
+ if (pingTime) {
812
+ rttMs = Math.max(1, Date.now() - pingTime);
813
+ metricRtt.innerText = rttMs + ' ms';
814
+ document.getElementById('diagRtt').innerText = rttMs + ' ms';
815
+ }
816
+ }
817
+ } catch(err) {
818
+ console.error('Control message error:', err);
819
+ }
820
+ };
821
+ }
822
+
823
+ function checkConnected() {
824
+ if (eventsChannel && eventsChannel.readyState === 'open' &&
825
+ controlChannel && controlChannel.readyState === 'open') {
826
+ setConnected();
827
+ }
828
+ }
829
+
830
+ function setConnected() {
831
+ statusDot.className = 'status-dot connected';
832
+ statusText.innerText = 'Connected';
833
+ waitingOverlay.style.display = 'none';
834
+
835
+ // Query Stats
836
+ updateStats();
837
+ if (!pingInterval) {
838
+ pingInterval = setInterval(function() {
839
+ if (controlChannel && controlChannel.readyState === 'open') {
840
+ controlChannel.send(JSON.stringify({ v: 1, type: 'ping', timestamp: Date.now() }));
841
+ }
842
+ updateStats();
843
+ }, 2000);
844
+ }
845
+ }
846
+
847
+ function setDisconnected() {
848
+ statusDot.className = 'status-dot failed';
849
+ statusText.innerText = 'Disconnected';
850
+ if (pingInterval) {
851
+ clearInterval(pingInterval);
852
+ pingInterval = null;
853
+ }
854
+ }
855
+
856
+ async function updateStats() {
857
+ if (!pc) return;
858
+ try {
859
+ const stats = await pc.getStats();
860
+ stats.forEach(function(report) {
861
+ if (report.type === 'candidate-pair' && (report.selected || report.state === 'succeeded')) {
862
+ const local = stats.get(report.localCandidateId);
863
+ if (local) {
864
+ const cType = local.candidateType || 'host';
865
+ document.getElementById('diagCandidate').innerText = cType;
866
+ document.getElementById('diagProtocol').innerText = local.protocol || 'UDP';
867
+ const route = cType === 'host' ? 'Direct / LAN' : (cType === 'relay' ? 'Relay / TURN' : 'STUN / WAN');
868
+ metricRoute.innerText = route;
869
+ document.getElementById('diagRoute').innerText = route;
870
+ }
871
+ }
872
+ });
873
+ } catch(e) {}
874
+ }
875
+
876
+ // --- UI & Event Rendering ---
877
+ function appendEvent(ev) {
878
+ if (isPaused) return;
879
+ totalReceived++;
880
+ events.push(ev);
881
+ metricEvents.innerText = totalReceived;
882
+ document.getElementById('diagReceived').innerText = totalReceived;
883
+
884
+ if (matchesFilter(ev)) {
885
+ renderLogRow(ev);
886
+ }
887
+ }
888
+
889
+ function matchesFilter(ev) {
890
+ if (activeSourceFilter !== 'all') {
891
+ if (activeSourceFilter === 'custom' && ev.source !== 'custom') return false;
892
+ if (activeSourceFilter !== 'custom' && ev.source !== activeSourceFilter) return false;
893
+ }
894
+ if (activeLevelFilter !== 'all' && ev.level !== activeLevelFilter) {
895
+ return false;
896
+ }
897
+ if (searchQuery) {
898
+ const q = searchQuery.toLowerCase();
899
+ const matchMsg = ev.message && ev.message.toLowerCase().includes(q);
900
+ const matchType = ev.type && ev.type.toLowerCase().includes(q);
901
+ if (!matchMsg && !matchType) return false;
902
+ }
903
+ return true;
904
+ }
905
+
906
+ function renderLogRow(ev) {
907
+ const row = document.createElement('div');
908
+ row.className = 'log-row';
909
+ row.dataset.id = ev.id;
910
+
911
+ const time = new Date(ev.timestamp).toLocaleTimeString();
912
+ const durationHtml = ev.duration ? '<span class="log-duration">(' + Math.round(ev.duration) + 'ms)</span>' : '';
913
+
914
+ row.innerHTML = '<span class="log-time">' + time + '</span>' +
915
+ '<span class="pill pill-source">' + (ev.source || 'web') + '</span>' +
916
+ '<span class="pill pill-level-' + (ev.level || 'info') + '">' + (ev.level || 'info') + '</span>' +
917
+ '<span class="log-msg"><strong>' + (ev.type ? ev.type + ': ' : '') + '</strong>' + escapeHtml(ev.message || '') + durationHtml + '</span>';
918
+
919
+ row.addEventListener('click', function() {
920
+ showDetails(ev, row);
921
+ });
922
+
923
+ logsContainer.appendChild(row);
924
+ logsContainer.scrollTop = logsContainer.scrollHeight;
925
+ }
926
+
927
+ function refreshLogs() {
928
+ logsContainer.innerHTML = '';
929
+ events.filter(matchesFilter).forEach(renderLogRow);
930
+ }
931
+
932
+ function showDetails(ev, rowElem) {
933
+ document.querySelectorAll('.log-row.selected').forEach(function(el) { el.classList.remove('selected'); });
934
+ if (rowElem) rowElem.classList.add('selected');
935
+
936
+ selectedEvent = ev;
937
+ detailsPane.style.display = 'flex';
938
+
939
+ detailsBody.innerHTML = '<div style="margin-bottom:12px;">' +
940
+ '<div style="font-size:11px; color:var(--text-dim); text-transform:uppercase;">Type</div>' +
941
+ '<div style="font-weight:700; font-size:14px; margin-top:2px;">' + (ev.type || 'generic') + '</div>' +
942
+ '</div>' +
943
+ '<div style="margin-bottom:12px;">' +
944
+ '<div style="font-size:11px; color:var(--text-dim); text-transform:uppercase;">Message</div>' +
945
+ '<div style="margin-top:2px; font-size:13px; color:#cbd5e1;">' + escapeHtml(ev.message || '') + '</div>' +
946
+ '</div>' +
947
+ '<div style="margin-bottom:12px;">' +
948
+ '<div style="font-size:11px; color:var(--text-dim); text-transform:uppercase;">Payload Data</div>' +
949
+ '<pre class="json-tree">' + escapeHtml(JSON.stringify(ev.data || {}, null, 2)) + '</pre>' +
950
+ '</div>';
951
+ }
952
+
953
+ function escapeHtml(str) {
954
+ return String(str)
955
+ .replace(/&/g, '&amp;')
956
+ .replace(/</g, '&lt;')
957
+ .replace(/>/g, '&gt;')
958
+ .replace(/"/g, '&quot;');
959
+ }
960
+
961
+ // --- Button Handlers ---
962
+ btnPause.addEventListener('click', function() {
963
+ isPaused = !isPaused;
964
+ btnPause.innerText = isPaused ? '\u25B6 Resume' : '\u23F8 Pause';
965
+ if (controlChannel && controlChannel.readyState === 'open') {
966
+ controlChannel.send(JSON.stringify({
967
+ v: 1,
968
+ type: 'control',
969
+ command: { type: isPaused ? 'pause' : 'resume' }
970
+ }));
971
+ }
972
+ });
973
+
974
+ btnClear.addEventListener('click', function() {
975
+ events = [];
976
+ logsContainer.innerHTML = '';
977
+ metricEvents.innerText = '0';
978
+ totalReceived = 0;
979
+ if (controlChannel && controlChannel.readyState === 'open') {
980
+ controlChannel.send(JSON.stringify({
981
+ v: 1,
982
+ type: 'control',
983
+ command: { type: 'clear' }
984
+ }));
985
+ }
986
+ });
987
+
988
+ btnSnapshot.addEventListener('click', function() {
989
+ if (controlChannel && controlChannel.readyState === 'open') {
990
+ controlChannel.send(JSON.stringify({
991
+ v: 1,
992
+ type: 'control',
993
+ command: { type: 'snapshot.request' }
994
+ }));
995
+ }
996
+ });
997
+
998
+ btnDiag.addEventListener('click', function() { diagModal.classList.add('show'); });
999
+ btnCloseDiag.addEventListener('click', function() { diagModal.classList.remove('show'); });
1000
+ btnCloseDetails.addEventListener('click', function() { detailsPane.style.display = 'none'; });
1001
+
1002
+ // Tab filtering
1003
+ document.getElementById('tabsGroup').addEventListener('click', function(e) {
1004
+ if (e.target.classList.contains('tab-btn')) {
1005
+ document.querySelectorAll('.tab-btn').forEach(function(btn) { btn.classList.remove('active'); });
1006
+ e.target.classList.add('active');
1007
+ activeSourceFilter = e.target.dataset.source;
1008
+ refreshLogs();
1009
+ }
1010
+ });
1011
+
1012
+ // Search & Level
1013
+ document.getElementById('searchInput').addEventListener('input', function(e) {
1014
+ searchQuery = e.target.value;
1015
+ refreshLogs();
1016
+ });
1017
+
1018
+ document.getElementById('levelSelect').addEventListener('change', function(e) {
1019
+ activeLevelFilter = e.target.value;
1020
+ refreshLogs();
1021
+ });
1022
+
1023
+ // Start SSE signaling listener
1024
+ initSignaling();
1025
+ })();
1026
+ <\/script>
1027
+ </body>
1028
+ </html>`;
1029
+ }
1030
+
1031
+ function getPairingPageHtml(options) {
1032
+ return `<!DOCTYPE html>
1033
+ <html lang="en">
1034
+ <head>
1035
+ <meta charset="UTF-8">
1036
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
1037
+ <title>CapDebug Remote Pairing</title>
1038
+ <style>
1039
+ :root {
1040
+ --bg: #090d16;
1041
+ --card-bg: rgba(22, 30, 49, 0.85);
1042
+ --border: rgba(255, 255, 255, 0.1);
1043
+ --accent: #38bdf8;
1044
+ --accent-glow: rgba(56, 189, 248, 0.25);
1045
+ --text: #f1f5f9;
1046
+ --text-muted: #94a3b8;
1047
+ --green: #10b981;
1048
+ }
1049
+ * { box-sizing: border-box; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
1050
+ body {
1051
+ background: radial-gradient(circle at top, #1e293b 0%, var(--bg) 100%);
1052
+ color: var(--text);
1053
+ min-height: 100vh;
1054
+ display: flex;
1055
+ flex-direction: column;
1056
+ align-items: center;
1057
+ justify-content: center;
1058
+ padding: 24px;
1059
+ text-align: center;
1060
+ }
1061
+ .card {
1062
+ background: var(--card-bg);
1063
+ border: 1px solid var(--border);
1064
+ border-radius: 24px;
1065
+ padding: 32px 24px;
1066
+ max-width: 400px;
1067
+ width: 100%;
1068
+ box-shadow: 0 20px 40px rgba(0,0,0,0.5), 0 0 40px var(--accent-glow);
1069
+ backdrop-filter: blur(12px);
1070
+ animation: fadeIn 0.4s ease-out;
1071
+ }
1072
+ @keyframes fadeIn {
1073
+ from { opacity: 0; transform: translateY(16px); }
1074
+ to { opacity: 1; transform: translateY(0); }
1075
+ }
1076
+ .logo-badge {
1077
+ display: inline-flex;
1078
+ align-items: center;
1079
+ gap: 8px;
1080
+ padding: 6px 14px;
1081
+ background: rgba(56, 189, 248, 0.12);
1082
+ border: 1px solid rgba(56, 189, 248, 0.3);
1083
+ border-radius: 999px;
1084
+ color: var(--accent);
1085
+ font-weight: 700;
1086
+ font-size: 13px;
1087
+ letter-spacing: 0.5px;
1088
+ margin-bottom: 20px;
1089
+ }
1090
+ .pulse-dot {
1091
+ width: 8px;
1092
+ height: 8px;
1093
+ border-radius: 50%;
1094
+ background: var(--accent);
1095
+ box-shadow: 0 0 10px var(--accent);
1096
+ animation: pulse 1.5s infinite;
1097
+ }
1098
+ @keyframes pulse {
1099
+ 0%, 100% { transform: scale(1); opacity: 1; }
1100
+ 50% { transform: scale(1.6); opacity: 0.6; }
1101
+ }
1102
+ h1 {
1103
+ font-size: 24px;
1104
+ font-weight: 800;
1105
+ margin-bottom: 8px;
1106
+ letter-spacing: -0.5px;
1107
+ }
1108
+ p {
1109
+ color: var(--text-muted);
1110
+ font-size: 14px;
1111
+ line-height: 1.5;
1112
+ margin-bottom: 24px;
1113
+ }
1114
+ .session-pill {
1115
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
1116
+ background: rgba(0,0,0,0.4);
1117
+ border: 1px solid var(--border);
1118
+ padding: 10px 16px;
1119
+ border-radius: 12px;
1120
+ font-size: 18px;
1121
+ font-weight: 700;
1122
+ color: #38bdf8;
1123
+ letter-spacing: 2px;
1124
+ margin-bottom: 24px;
1125
+ display: inline-block;
1126
+ }
1127
+ .btn {
1128
+ display: flex;
1129
+ align-items: center;
1130
+ justify-content: center;
1131
+ width: 100%;
1132
+ padding: 14px 20px;
1133
+ border-radius: 14px;
1134
+ font-size: 15px;
1135
+ font-weight: 600;
1136
+ cursor: pointer;
1137
+ text-decoration: none;
1138
+ transition: all 0.2s;
1139
+ border: none;
1140
+ margin-bottom: 12px;
1141
+ }
1142
+ .btn-primary {
1143
+ background: linear-gradient(135deg, #0284c7, #38bdf8);
1144
+ color: white;
1145
+ box-shadow: 0 4px 16px rgba(56, 189, 248, 0.35);
1146
+ }
1147
+ .btn-primary:active {
1148
+ transform: scale(0.98);
1149
+ }
1150
+ .btn-secondary {
1151
+ background: rgba(255, 255, 255, 0.06);
1152
+ color: var(--text);
1153
+ border: 1px solid var(--border);
1154
+ }
1155
+ .status-text {
1156
+ font-size: 12px;
1157
+ color: var(--text-muted);
1158
+ margin-top: 16px;
1159
+ }
1160
+ </style>
1161
+ </head>
1162
+ <body>
1163
+ <div class="card">
1164
+ <div class="logo-badge">
1165
+ <span class="pulse-dot"></span>
1166
+ CAPDEBUG REMOTE
1167
+ </div>
1168
+ <h1>Pairing with Desktop</h1>
1169
+ <p>Opening CapDebug session on your device...</p>
1170
+
1171
+ <div class="session-pill" id="sessionDisplay">${options.sessionId}</div>
1172
+
1173
+ <a id="deepLinkBtn" href="#" class="btn btn-primary">Open in Capacitor App</a>
1174
+ <button id="browserBtn" class="btn btn-secondary">Connect in Browser</button>
1175
+
1176
+ <div class="status-text" id="statusMessage">Redirecting to app...</div>
1177
+ </div>
1178
+
1179
+ <script>
1180
+ (function() {
1181
+ const sessionId = "${options.sessionId}";
1182
+ const signalingUrl = "${options.signalingUrl}";
1183
+ const secret = window.location.hash ? window.location.hash.substring(1) : "";
1184
+
1185
+ const deepLinkUrl = "capdebug://pair?session=" + encodeURIComponent(sessionId) +
1186
+ "&secret=" + encodeURIComponent(secret) +
1187
+ "&signaling=" + encodeURIComponent(signalingUrl);
1188
+
1189
+ const deepLinkBtn = document.getElementById("deepLinkBtn");
1190
+ const browserBtn = document.getElementById("browserBtn");
1191
+ const statusMsg = document.getElementById("statusMessage");
1192
+
1193
+ deepLinkBtn.href = deepLinkUrl;
1194
+
1195
+ // Auto redirect to deep link after short pause
1196
+ setTimeout(function() {
1197
+ window.location.href = deepLinkUrl;
1198
+ }, 400);
1199
+
1200
+ // Handle in-browser pairing if user stays on page
1201
+ browserBtn.addEventListener("click", function() {
1202
+ statusMsg.innerText = "Connecting browser session...";
1203
+ browserBtn.disabled = true;
1204
+ // In-browser mobile web session joining
1205
+ if (window.opener || window.parent) {
1206
+ window.postMessage({ type: "capdebug:pair", sessionId: sessionId, secret: secret }, "*");
1207
+ }
1208
+ statusMsg.innerText = "If using Capacitor app, tap 'Open in Capacitor App' above.";
1209
+ });
1210
+ })();
1211
+ <\/script>
1212
+ </body>
1213
+ </html>`;
1214
+ }
1215
+
1216
+ class CapDebugCliServer {
1217
+ server = null;
1218
+ sessions = /* @__PURE__ */ new Map();
1219
+ port;
1220
+ host;
1221
+ constructor(options = {}) {
1222
+ this.port = options.port || 4783;
1223
+ this.host = options.host || "0.0.0.0";
1224
+ }
1225
+ getLanIp() {
1226
+ const interfaces = os__namespace.networkInterfaces();
1227
+ for (const name of Object.keys(interfaces)) {
1228
+ for (const iface of interfaces[name] || []) {
1229
+ if (iface.family === "IPv4" && !iface.internal) {
1230
+ return iface.address;
1231
+ }
1232
+ }
1233
+ }
1234
+ return "127.0.0.1";
1235
+ }
1236
+ getLocalViewerUrl() {
1237
+ return `http://localhost:${this.port}`;
1238
+ }
1239
+ getMobilePairingUrl(sessionId, secret) {
1240
+ const lanIp = this.getLanIp();
1241
+ return PairingHelper.formatPairingUrl(
1242
+ `http://${lanIp}:${this.port}`,
1243
+ sessionId,
1244
+ secret
1245
+ );
1246
+ }
1247
+ createSession() {
1248
+ const session = PairingHelper.createSession(5 * 60 * 1e3);
1249
+ const secret = PairingHelper.generateSecret();
1250
+ this.sessions.set(session.id, {
1251
+ session,
1252
+ secret,
1253
+ subscribers: /* @__PURE__ */ new Set(),
1254
+ peerJoined: false
1255
+ });
1256
+ return { session, secret };
1257
+ }
1258
+ getSession(id) {
1259
+ const s = this.sessions.get(id);
1260
+ if (!s) return void 0;
1261
+ if (Date.now() > s.session.expiresAt) {
1262
+ s.session.status = "expired";
1263
+ }
1264
+ return s;
1265
+ }
1266
+ async start() {
1267
+ return new Promise((resolve, reject) => {
1268
+ this.server = http__namespace.createServer((req, res) => {
1269
+ this.handleRequest(req, res);
1270
+ });
1271
+ this.server.listen(this.port, this.host, () => {
1272
+ const addr = this.server?.address();
1273
+ const actualPort = typeof addr === "object" && addr ? addr.port : this.port;
1274
+ this.port = actualPort;
1275
+ resolve({ port: this.port, lanIp: this.getLanIp() });
1276
+ });
1277
+ this.server.on("error", (err) => {
1278
+ reject(err);
1279
+ });
1280
+ });
1281
+ }
1282
+ stop() {
1283
+ return new Promise((resolve) => {
1284
+ for (const s of this.sessions.values()) {
1285
+ for (const sub of s.subscribers) {
1286
+ try {
1287
+ sub.end();
1288
+ } catch {
1289
+ }
1290
+ }
1291
+ s.subscribers.clear();
1292
+ }
1293
+ if (this.server) {
1294
+ this.server.close(() => {
1295
+ this.server = null;
1296
+ resolve();
1297
+ });
1298
+ } else {
1299
+ resolve();
1300
+ }
1301
+ });
1302
+ }
1303
+ handleRequest(req, res) {
1304
+ const parsedUrl = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
1305
+ const pathname = parsedUrl.pathname;
1306
+ res.setHeader("Access-Control-Allow-Origin", "*");
1307
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1308
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
1309
+ if (req.method === "OPTIONS") {
1310
+ res.writeHead(204);
1311
+ res.end();
1312
+ return;
1313
+ }
1314
+ if (pathname === "/" && req.method === "GET") {
1315
+ const sessionId = parsedUrl.searchParams.get("s") || Array.from(this.sessions.keys())[0] || "";
1316
+ const sessionInfo = this.getSession(sessionId) || Array.from(this.sessions.values())[0];
1317
+ const html = getDesktopViewerHtml({
1318
+ sessionId: sessionInfo ? sessionInfo.session.id : "",
1319
+ secret: sessionInfo ? sessionInfo.secret : "",
1320
+ signalingUrl: `http://localhost:${this.port}`
1321
+ });
1322
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1323
+ res.end(html);
1324
+ return;
1325
+ }
1326
+ if (pathname === "/pair" && req.method === "GET") {
1327
+ const sessionId = parsedUrl.searchParams.get("s") || "";
1328
+ const lanIp = this.getLanIp();
1329
+ const html = getPairingPageHtml({
1330
+ sessionId,
1331
+ signalingUrl: `http://${lanIp}:${this.port}`
1332
+ });
1333
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1334
+ res.end(html);
1335
+ return;
1336
+ }
1337
+ if (pathname === "/api/pair/create" && req.method === "POST") {
1338
+ const result = this.createSession();
1339
+ res.writeHead(200, { "Content-Type": "application/json" });
1340
+ res.end(JSON.stringify({ ok: true, session: result.session, secret: result.secret }));
1341
+ return;
1342
+ }
1343
+ if (pathname === "/api/pair/join" && req.method === "POST") {
1344
+ this.readJsonBody(req, (err, body) => {
1345
+ if (err || !body?.sessionId || !body?.secret) {
1346
+ res.writeHead(400, { "Content-Type": "application/json" });
1347
+ res.end(JSON.stringify({ ok: false, error: "Invalid join payload" }));
1348
+ return;
1349
+ }
1350
+ const sessionInfo = this.getSession(body.sessionId);
1351
+ if (!sessionInfo) {
1352
+ res.writeHead(404, { "Content-Type": "application/json" });
1353
+ res.end(JSON.stringify({ ok: false, error: "Session not found" }));
1354
+ return;
1355
+ }
1356
+ if (sessionInfo.session.status === "expired") {
1357
+ res.writeHead(410, { "Content-Type": "application/json" });
1358
+ res.end(JSON.stringify({ ok: false, error: "Session expired" }));
1359
+ return;
1360
+ }
1361
+ if (sessionInfo.secret !== body.secret) {
1362
+ res.writeHead(403, { "Content-Type": "application/json" });
1363
+ res.end(JSON.stringify({ ok: false, error: "Invalid secret" }));
1364
+ return;
1365
+ }
1366
+ sessionInfo.session.status = "peer-joined";
1367
+ sessionInfo.peerJoined = true;
1368
+ this.broadcast(sessionInfo, {
1369
+ sessionId: sessionInfo.session.id,
1370
+ secret: sessionInfo.secret,
1371
+ type: "peer-joined",
1372
+ sender: "peer"
1373
+ });
1374
+ res.writeHead(200, { "Content-Type": "application/json" });
1375
+ res.end(JSON.stringify({ ok: true, session: sessionInfo.session }));
1376
+ });
1377
+ return;
1378
+ }
1379
+ if (pathname === "/api/pair/signal" && req.method === "POST") {
1380
+ this.readJsonBody(req, (err, body) => {
1381
+ if (err || !body?.sessionId || !body?.secret || !body?.type) {
1382
+ res.writeHead(400, { "Content-Type": "application/json" });
1383
+ res.end(JSON.stringify({ ok: false, error: "Invalid signal payload" }));
1384
+ return;
1385
+ }
1386
+ const sessionInfo = this.getSession(body.sessionId);
1387
+ if (!sessionInfo || sessionInfo.secret !== body.secret) {
1388
+ res.writeHead(403, { "Content-Type": "application/json" });
1389
+ res.end(JSON.stringify({ ok: false, error: "Invalid session or secret" }));
1390
+ return;
1391
+ }
1392
+ this.broadcast(sessionInfo, body);
1393
+ res.writeHead(200, { "Content-Type": "application/json" });
1394
+ res.end(JSON.stringify({ ok: true }));
1395
+ });
1396
+ return;
1397
+ }
1398
+ if (pathname.startsWith("/api/pair/events/") && req.method === "GET") {
1399
+ const sessionId = pathname.replace("/api/pair/events/", "");
1400
+ const secret = parsedUrl.searchParams.get("secret") || "";
1401
+ const sessionInfo = this.getSession(sessionId);
1402
+ if (!sessionInfo || secret && sessionInfo.secret !== secret) {
1403
+ res.writeHead(403, { "Content-Type": "text/plain" });
1404
+ res.end("Unauthorized");
1405
+ return;
1406
+ }
1407
+ res.writeHead(200, {
1408
+ "Content-Type": "text/event-stream",
1409
+ "Cache-Control": "no-cache",
1410
+ "Connection": "keep-alive"
1411
+ });
1412
+ res.write(": ok\n\n");
1413
+ sessionInfo.subscribers.add(res);
1414
+ req.on("close", () => {
1415
+ sessionInfo.subscribers.delete(res);
1416
+ });
1417
+ return;
1418
+ }
1419
+ if (pathname.startsWith("/api/pair/status/") && req.method === "GET") {
1420
+ const sessionId = pathname.replace("/api/pair/status/", "");
1421
+ const sessionInfo = this.getSession(sessionId);
1422
+ if (!sessionInfo) {
1423
+ res.writeHead(404, { "Content-Type": "application/json" });
1424
+ res.end(JSON.stringify({ ok: false, error: "Session not found" }));
1425
+ return;
1426
+ }
1427
+ res.writeHead(200, { "Content-Type": "application/json" });
1428
+ res.end(JSON.stringify({ ok: true, session: sessionInfo.session }));
1429
+ return;
1430
+ }
1431
+ res.writeHead(404, { "Content-Type": "text/plain" });
1432
+ res.end("Not Found");
1433
+ }
1434
+ broadcast(sessionInfo, message) {
1435
+ const data = `data: ${JSON.stringify(message)}
1436
+
1437
+ `;
1438
+ for (const sub of sessionInfo.subscribers) {
1439
+ try {
1440
+ sub.write(data);
1441
+ } catch {
1442
+ sessionInfo.subscribers.delete(sub);
1443
+ }
1444
+ }
1445
+ }
1446
+ readJsonBody(req, cb) {
1447
+ let raw = "";
1448
+ req.on("data", (chunk) => {
1449
+ raw += chunk;
1450
+ if (raw.length > 1024 * 1024) {
1451
+ req.destroy();
1452
+ }
1453
+ });
1454
+ req.on("end", () => {
1455
+ try {
1456
+ const parsed = JSON.parse(raw || "{}");
1457
+ cb(null, parsed);
1458
+ } catch (err) {
1459
+ cb(err, null);
1460
+ }
1461
+ });
1462
+ req.on("error", (err) => cb(err, null));
1463
+ }
1464
+ }
1465
+
1466
+ const EXP_TABLE = new Uint8Array(512);
1467
+ const LOG_TABLE = new Uint8Array(256);
1468
+ (function initGF() {
1469
+ let x = 1;
1470
+ for (let i = 0; i < 255; i++) {
1471
+ EXP_TABLE[i] = x;
1472
+ EXP_TABLE[i + 255] = x;
1473
+ LOG_TABLE[x] = i;
1474
+ x = x << 1 ^ (x >= 128 ? 285 : 0);
1475
+ }
1476
+ })();
1477
+ function gfMul(x, y) {
1478
+ if (x === 0 || y === 0) return 0;
1479
+ return EXP_TABLE[LOG_TABLE[x] + LOG_TABLE[y]];
1480
+ }
1481
+ function polyMul(p, q) {
1482
+ const result = new Array(p.length + q.length - 1).fill(0);
1483
+ for (let i = 0; i < p.length; i++) {
1484
+ for (let j = 0; j < q.length; j++) {
1485
+ result[i + j] ^= gfMul(p[i], q[j]);
1486
+ }
1487
+ }
1488
+ return result;
1489
+ }
1490
+ function rsGeneratorPoly(degree) {
1491
+ let g = [1];
1492
+ for (let i = 0; i < degree; i++) {
1493
+ g = polyMul(g, [1, EXP_TABLE[i]]);
1494
+ }
1495
+ return g;
1496
+ }
1497
+ function rsCompute(data, ecCount) {
1498
+ const gen = rsGeneratorPoly(ecCount);
1499
+ const msg = new Array(data.length + ecCount).fill(0);
1500
+ for (let i = 0; i < data.length; i++) {
1501
+ msg[i] = data[i];
1502
+ }
1503
+ for (let i = 0; i < data.length; i++) {
1504
+ const lead = msg[i];
1505
+ if (lead !== 0) {
1506
+ for (let j = 0; j < gen.length; j++) {
1507
+ msg[i + j] ^= gfMul(gen[j], lead);
1508
+ }
1509
+ }
1510
+ }
1511
+ const out = new Uint8Array(ecCount);
1512
+ for (let i = 0; i < ecCount; i++) {
1513
+ out[i] = msg[data.length + i];
1514
+ }
1515
+ return out;
1516
+ }
1517
+ const QR_SPECS = [
1518
+ { version: 2, size: 25, totalBytes: 44, dataBytes: 34, ecBytes: 10, alignPos: [6, 18] },
1519
+ { version: 3, size: 29, totalBytes: 70, dataBytes: 55, ecBytes: 15, alignPos: [6, 22] },
1520
+ { version: 4, size: 33, totalBytes: 100, dataBytes: 80, ecBytes: 20, alignPos: [6, 26] },
1521
+ { version: 5, size: 37, totalBytes: 134, dataBytes: 108, ecBytes: 26, alignPos: [6, 30] },
1522
+ { version: 6, size: 41, totalBytes: 172, dataBytes: 136, ecBytes: 36, alignPos: [6, 34] }
1523
+ ];
1524
+ class TerminalQRCode {
1525
+ /**
1526
+ * Generates a 2D boolean grid (true = dark/black, false = light/white) for the given text.
1527
+ */
1528
+ static generateMatrix(text) {
1529
+ const textBytes = new TextEncoder().encode(text);
1530
+ const spec = QR_SPECS.find((s) => textBytes.length + 3 <= s.dataBytes) || QR_SPECS[QR_SPECS.length - 1];
1531
+ const dataBits = [];
1532
+ dataBits.push(0, 1, 0, 0);
1533
+ const len = textBytes.length;
1534
+ for (let i = 7; i >= 0; i--) {
1535
+ dataBits.push(len >> i & 1);
1536
+ }
1537
+ for (let i = 0; i < len; i++) {
1538
+ const b = textBytes[i];
1539
+ for (let bit = 7; bit >= 0; bit--) {
1540
+ dataBits.push(b >> bit & 1);
1541
+ }
1542
+ }
1543
+ const totalDataBits = spec.dataBytes * 8;
1544
+ for (let i = 0; i < 4 && dataBits.length < totalDataBits; i++) {
1545
+ dataBits.push(0);
1546
+ }
1547
+ while (dataBits.length % 8 !== 0) {
1548
+ dataBits.push(0);
1549
+ }
1550
+ const padBytes = [236, 17];
1551
+ let padIdx = 0;
1552
+ while (dataBits.length < totalDataBits) {
1553
+ const pad = padBytes[padIdx % 2];
1554
+ for (let bit = 7; bit >= 0; bit--) {
1555
+ dataBits.push(pad >> bit & 1);
1556
+ }
1557
+ padIdx++;
1558
+ }
1559
+ const dataBytes = new Uint8Array(spec.dataBytes);
1560
+ for (let i = 0; i < spec.dataBytes; i++) {
1561
+ let b = 0;
1562
+ for (let bit = 0; bit < 8; bit++) {
1563
+ b = b << 1 | dataBits[i * 8 + bit];
1564
+ }
1565
+ dataBytes[i] = b;
1566
+ }
1567
+ const ecBytes = rsCompute(dataBytes, spec.ecBytes);
1568
+ const finalCodewords = new Uint8Array(spec.totalBytes);
1569
+ finalCodewords.set(dataBytes, 0);
1570
+ finalCodewords.set(ecBytes, spec.dataBytes);
1571
+ const size = spec.size;
1572
+ const matrix = Array.from(
1573
+ { length: size },
1574
+ () => Array.from({ length: size }, () => null)
1575
+ );
1576
+ drawFinder(matrix, 0, 0);
1577
+ drawFinder(matrix, size - 7, 0);
1578
+ drawFinder(matrix, 0, size - 7);
1579
+ for (let i = 8; i < size - 8; i++) {
1580
+ const val = i % 2 === 0;
1581
+ if (matrix[6][i] === null) matrix[6][i] = val;
1582
+ if (matrix[i][6] === null) matrix[i][6] = val;
1583
+ }
1584
+ if (spec.alignPos.length > 0) {
1585
+ for (const r of spec.alignPos) {
1586
+ for (const c of spec.alignPos) {
1587
+ if (r === 6 && c === 6 || r === 6 && c === size - 7 || r === size - 7 && c === 6) {
1588
+ continue;
1589
+ }
1590
+ drawAlignment(matrix, r, c);
1591
+ }
1592
+ }
1593
+ }
1594
+ matrix[size - 8][8] = true;
1595
+ reserveFormatInfo(matrix, size);
1596
+ placeDataBits(matrix, size, finalCodewords);
1597
+ applyMask(matrix, size);
1598
+ writeFormatInfo(matrix, size, 30660);
1599
+ return matrix.map((row) => row.map((cell) => cell === true));
1600
+ }
1601
+ /**
1602
+ * Renders the matrix to terminal string using ANSI half-block characters (▀, ▄, █).
1603
+ * 2 vertical rows are packed into 1 line of terminal output for high compactness.
1604
+ */
1605
+ static renderToString(text) {
1606
+ const matrix = this.generateMatrix(text);
1607
+ const size = matrix.length;
1608
+ const quietZone = 2;
1609
+ const fullSize = size + quietZone * 2;
1610
+ const padded = Array.from(
1611
+ { length: fullSize },
1612
+ (_, r) => Array.from({ length: fullSize }, (_2, c) => {
1613
+ const mr = r - quietZone;
1614
+ const mc = c - quietZone;
1615
+ if (mr >= 0 && mr < size && mc >= 0 && mc < size) {
1616
+ return matrix[mr][mc];
1617
+ }
1618
+ return false;
1619
+ })
1620
+ );
1621
+ const lines = [];
1622
+ const reset = "\x1B[0m";
1623
+ const bgWhiteFgBlack = "\x1B[47m\x1B[30m";
1624
+ for (let r = 0; r < fullSize; r += 2) {
1625
+ let line = bgWhiteFgBlack;
1626
+ for (let c = 0; c < fullSize; c++) {
1627
+ const topDark = padded[r][c];
1628
+ const bottomDark = r + 1 < fullSize ? padded[r + 1][c] : false;
1629
+ if (topDark && bottomDark) {
1630
+ line += "\u2588";
1631
+ } else if (topDark && !bottomDark) {
1632
+ line += "\u2580";
1633
+ } else if (!topDark && bottomDark) {
1634
+ line += "\u2584";
1635
+ } else {
1636
+ line += " ";
1637
+ }
1638
+ }
1639
+ line += reset;
1640
+ lines.push(line);
1641
+ }
1642
+ return lines.join("\n");
1643
+ }
1644
+ }
1645
+ function drawFinder(matrix, row, col) {
1646
+ for (let r = -1; r <= 7; r++) {
1647
+ for (let c = -1; c <= 7; c++) {
1648
+ const mr = row + r;
1649
+ const mc = col + c;
1650
+ if (mr >= 0 && mr < matrix.length && mc >= 0 && mc < matrix.length) {
1651
+ if (r === -1 || r === 7 || c === -1 || c === 7) {
1652
+ matrix[mr][mc] = false;
1653
+ } else if (r === 0 || r === 6 || c === 0 || c === 6) {
1654
+ matrix[mr][mc] = true;
1655
+ } else if (r === 1 || r === 5 || c === 1 || c === 5) {
1656
+ matrix[mr][mc] = false;
1657
+ } else {
1658
+ matrix[mr][mc] = true;
1659
+ }
1660
+ }
1661
+ }
1662
+ }
1663
+ }
1664
+ function drawAlignment(matrix, centerR, centerC) {
1665
+ for (let r = -2; r <= 2; r++) {
1666
+ for (let c = -2; c <= 2; c++) {
1667
+ const mr = centerR + r;
1668
+ const mc = centerC + c;
1669
+ if (matrix[mr][mc] !== null) continue;
1670
+ if (Math.abs(r) === 2 || Math.abs(c) === 2 || r === 0 && c === 0) {
1671
+ matrix[mr][mc] = true;
1672
+ } else {
1673
+ matrix[mr][mc] = false;
1674
+ }
1675
+ }
1676
+ }
1677
+ }
1678
+ function reserveFormatInfo(matrix, size) {
1679
+ for (let i = 0; i <= 8; i++) {
1680
+ if (matrix[8][i] === null) matrix[8][i] = false;
1681
+ if (matrix[i][8] === null) matrix[i][8] = false;
1682
+ if (matrix[8][size - 1 - i] === null) matrix[8][size - 1 - i] = false;
1683
+ if (matrix[size - 1 - i][8] === null) matrix[size - 1 - i][8] = false;
1684
+ }
1685
+ }
1686
+ function placeDataBits(matrix, size, codewords) {
1687
+ let bitIdx = 0;
1688
+ const totalBits = codewords.length * 8;
1689
+ let right = size - 1;
1690
+ let upwards = true;
1691
+ while (right > 0) {
1692
+ if (right === 6) right--;
1693
+ for (let i = 0; i < size; i++) {
1694
+ const row = upwards ? size - 1 - i : i;
1695
+ for (let colOffset = 0; colOffset < 2; colOffset++) {
1696
+ const col = right - colOffset;
1697
+ if (matrix[row][col] === null) {
1698
+ let bitVal = false;
1699
+ if (bitIdx < totalBits) {
1700
+ const byte = codewords[Math.floor(bitIdx / 8)];
1701
+ const bit = 7 - bitIdx % 8;
1702
+ bitVal = (byte >> bit & 1) === 1;
1703
+ bitIdx++;
1704
+ }
1705
+ matrix[row][col] = bitVal;
1706
+ }
1707
+ }
1708
+ }
1709
+ right -= 2;
1710
+ upwards = !upwards;
1711
+ }
1712
+ }
1713
+ function applyMask(matrix, size) {
1714
+ for (let r = 0; r < size; r++) {
1715
+ for (let c = 0; c < size; c++) {
1716
+ if (isProtectedModule(r, c, size)) continue;
1717
+ if ((r + c) % 2 === 0) {
1718
+ matrix[r][c] = !matrix[r][c];
1719
+ }
1720
+ }
1721
+ }
1722
+ }
1723
+ function isProtectedModule(r, c, size) {
1724
+ if (r <= 8 && c <= 8) return true;
1725
+ if (r <= 8 && c >= size - 8) return true;
1726
+ if (r >= size - 8 && c <= 8) return true;
1727
+ if (r === 6 || c === 6) return true;
1728
+ return false;
1729
+ }
1730
+ function writeFormatInfo(matrix, size, formatBits) {
1731
+ for (let i = 0; i < 15; i++) {
1732
+ const bit = (formatBits >> 14 - i & 1) === 1;
1733
+ if (i <= 5) {
1734
+ matrix[8][i] = bit;
1735
+ } else if (i === 6) {
1736
+ matrix[8][7] = bit;
1737
+ } else if (i === 7) {
1738
+ matrix[8][8] = bit;
1739
+ } else if (i === 8) {
1740
+ matrix[7][8] = bit;
1741
+ } else {
1742
+ matrix[14 - i][8] = bit;
1743
+ }
1744
+ if (i < 8) {
1745
+ matrix[size - 1 - i][8] = bit;
1746
+ } else {
1747
+ matrix[8][size - 15 + i] = bit;
1748
+ }
1749
+ }
1750
+ }
1751
+
1752
+ async function runCli(argv = process.argv.slice(2)) {
1753
+ let port = 4783;
1754
+ let host = "0.0.0.0";
1755
+ let shouldOpen = true;
1756
+ for (let i = 0; i < argv.length; i++) {
1757
+ const arg = argv[i];
1758
+ if (arg === "--port" && argv[i + 1]) {
1759
+ port = parseInt(argv[i + 1], 10) || 4783;
1760
+ i++;
1761
+ } else if (arg.startsWith("--port=")) {
1762
+ port = parseInt(arg.split("=")[1], 10) || 4783;
1763
+ } else if (arg === "--host" && argv[i + 1]) {
1764
+ host = argv[i + 1];
1765
+ i++;
1766
+ } else if (arg === "--no-open") {
1767
+ shouldOpen = false;
1768
+ }
1769
+ }
1770
+ const server = new CapDebugCliServer({ port, host });
1771
+ await server.start();
1772
+ let sessionInfo = server.createSession();
1773
+ let pairingUrl = server.getMobilePairingUrl(sessionInfo.session.id, sessionInfo.secret);
1774
+ const viewerUrl = server.getLocalViewerUrl();
1775
+ if (shouldOpen) {
1776
+ openBrowser(viewerUrl);
1777
+ }
1778
+ let statusMessage = "Waiting for device...";
1779
+ let countdownTimer = null;
1780
+ function renderTerminalScreen() {
1781
+ process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
1782
+ const remainingSec = Math.max(0, Math.floor((sessionInfo.session.expiresAt - Date.now()) / 1e3));
1783
+ const mins = String(Math.floor(remainingSec / 60)).padStart(2, "0");
1784
+ const secs = String(remainingSec % 60).padStart(2, "0");
1785
+ const qrRendered = TerminalQRCode.renderToString(pairingUrl);
1786
+ console.log("\x1B[1m\x1B[36mCapDebug Remote\x1B[0m\n");
1787
+ console.log(`Desktop viewer:
1788
+ \x1B[4m\x1B[34m${viewerUrl}\x1B[0m
1789
+ `);
1790
+ console.log(`Scan to connect with mobile:
1791
+ `);
1792
+ console.log(qrRendered);
1793
+ console.log("");
1794
+ console.log(`Session: \x1B[1m\x1B[33m${sessionInfo.session.id}\x1B[0m`);
1795
+ console.log(`Expires in: ${mins}:${secs}`);
1796
+ console.log(`LAN URL: ${pairingUrl.split("#")[0]}`);
1797
+ console.log("");
1798
+ console.log(`Status: \x1B[32m${statusMessage}\x1B[0m
1799
+ `);
1800
+ console.log("Press:");
1801
+ console.log(" \x1B[1mO\x1B[0m Open desktop viewer in browser");
1802
+ console.log(" \x1B[1mC\x1B[0m Copy pairing URL to clipboard");
1803
+ console.log(" \x1B[1mR\x1B[0m Regenerate QR session");
1804
+ console.log(" \x1B[1mQ\x1B[0m Quit");
1805
+ }
1806
+ function setupCountdown() {
1807
+ if (countdownTimer) clearInterval(countdownTimer);
1808
+ countdownTimer = setInterval(() => {
1809
+ const remainingSec = Math.floor((sessionInfo.session.expiresAt - Date.now()) / 1e3);
1810
+ if (remainingSec <= 0) {
1811
+ statusMessage = "\x1B[31mSession expired. Press R to regenerate.\x1B[0m";
1812
+ }
1813
+ renderTerminalScreen();
1814
+ }, 1e3);
1815
+ }
1816
+ function regenerateSession() {
1817
+ sessionInfo = server.createSession();
1818
+ pairingUrl = server.getMobilePairingUrl(sessionInfo.session.id, sessionInfo.secret);
1819
+ statusMessage = "Waiting for device...";
1820
+ setupCountdown();
1821
+ renderTerminalScreen();
1822
+ }
1823
+ if (process.stdin.isTTY) {
1824
+ readline__namespace.emitKeypressEvents(process.stdin);
1825
+ process.stdin.setRawMode(true);
1826
+ process.stdin.resume();
1827
+ process.stdin.on("keypress", (str, key) => {
1828
+ if (key.ctrl && key.name === "c") {
1829
+ cleanupAndExit();
1830
+ }
1831
+ const k = (str || key.name || "").toLowerCase();
1832
+ if (k === "q") {
1833
+ cleanupAndExit();
1834
+ } else if (k === "o") {
1835
+ openBrowser(viewerUrl);
1836
+ } else if (k === "c") {
1837
+ copyToClipboard(pairingUrl);
1838
+ statusMessage = "Pairing URL copied to clipboard!";
1839
+ renderTerminalScreen();
1840
+ } else if (k === "r") {
1841
+ regenerateSession();
1842
+ }
1843
+ });
1844
+ }
1845
+ async function cleanupAndExit() {
1846
+ if (countdownTimer) clearInterval(countdownTimer);
1847
+ console.log("\nStopping CapDebug Remote server...");
1848
+ await server.stop();
1849
+ process.exit(0);
1850
+ }
1851
+ process.on("SIGINT", cleanupAndExit);
1852
+ process.on("SIGTERM", cleanupAndExit);
1853
+ setupCountdown();
1854
+ renderTerminalScreen();
1855
+ }
1856
+ function openBrowser(url) {
1857
+ const startCmd = process.platform === "darwin" ? `open "${url}"` : process.platform === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`;
1858
+ child_process.exec(startCmd, () => {
1859
+ });
1860
+ }
1861
+ function copyToClipboard(text) {
1862
+ const copyCmd = process.platform === "darwin" ? `printf "%s" "${text}" | pbcopy` : process.platform === "win32" ? `printf "%s" "${text}" | clip` : `printf "%s" "${text}" | xclip -selection clipboard 2>/dev/null || printf "%s" "${text}" | xsel -b 2>/dev/null`;
1863
+ child_process.exec(copyCmd, () => {
1864
+ });
1865
+ }
1866
+
1867
+ exports.runCli = runCli;
1868
+ //# sourceMappingURL=cli.js.map