@ait-co/devtools 0.1.143 → 0.2.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 (223) hide show
  1. package/README.en.md +65 -163
  2. package/README.md +65 -192
  3. package/dist/in-app/auto.d.ts +1 -138
  4. package/dist/in-app/auto.js +28 -1102
  5. package/dist/in-app/auto.js.map +1 -1
  6. package/dist/in-app/index.d.ts +38 -547
  7. package/dist/in-app/index.d.ts.map +1 -1
  8. package/dist/in-app/index.js +62 -939
  9. package/dist/in-app/index.js.map +1 -1
  10. package/dist/mcp/cli.d.ts +1 -54
  11. package/dist/mcp/cli.js +33 -9720
  12. package/dist/mcp/cli.js.map +1 -1
  13. package/dist/mcp/server.d.ts +1 -88
  14. package/dist/mcp/server.js +36 -1076
  15. package/dist/mcp/server.js.map +1 -1
  16. package/dist/mock/index.d.ts +19 -20
  17. package/dist/mock/index.d.ts.map +1 -1
  18. package/dist/mock/index.js.map +1 -1
  19. package/dist/optional-peers-CDEFhlhJ.cjs +131 -0
  20. package/dist/optional-peers-CDEFhlhJ.cjs.map +1 -0
  21. package/dist/optional-peers-FdVbUJst.js +96 -0
  22. package/dist/optional-peers-FdVbUJst.js.map +1 -0
  23. package/dist/panel/index.js +1 -103
  24. package/dist/panel/index.js.map +1 -1
  25. package/dist/relay-url-store-CPZAn-T5.js +107 -0
  26. package/dist/relay-url-store-CPZAn-T5.js.map +1 -0
  27. package/dist/relay-url-store-DLjlvMSA.cjs +108 -0
  28. package/dist/relay-url-store-DLjlvMSA.cjs.map +1 -0
  29. package/dist/stubs/bin-devtools-mcp.js +58 -0
  30. package/dist/stubs/bin-devtools-mcp.js.map +1 -0
  31. package/dist/stubs/bin-devtools-test.d.ts +2 -0
  32. package/dist/stubs/bin-devtools-test.js +55 -0
  33. package/dist/stubs/bin-devtools-test.js.map +1 -0
  34. package/dist/test-runner/config.d.ts +1 -231
  35. package/dist/test-runner/config.js +41 -45
  36. package/dist/test-runner/config.js.map +1 -1
  37. package/dist/{tunnel-BVSMXctM.cjs → tunnel-BKZkOyQp.cjs} +27 -75
  38. package/dist/tunnel-BKZkOyQp.cjs.map +1 -0
  39. package/dist/{tunnel-D7xkimBu.js → tunnel-CqSCIrdU.js} +27 -75
  40. package/dist/tunnel-CqSCIrdU.js.map +1 -0
  41. package/dist/unplugin/index.cjs +17 -30
  42. package/dist/unplugin/index.cjs.map +1 -1
  43. package/dist/unplugin/index.d.cts +34 -5
  44. package/dist/unplugin/index.d.cts.map +1 -1
  45. package/dist/unplugin/index.d.ts +35 -6
  46. package/dist/unplugin/index.d.ts.map +1 -1
  47. package/dist/unplugin/index.js +17 -30
  48. package/dist/unplugin/index.js.map +1 -1
  49. package/dist/unplugin/tunnel.cjs +61 -74
  50. package/dist/unplugin/tunnel.cjs.map +1 -1
  51. package/dist/unplugin/tunnel.d.cts +22 -16
  52. package/dist/unplugin/tunnel.d.cts.map +1 -1
  53. package/dist/unplugin/tunnel.d.ts +22 -16
  54. package/dist/unplugin/tunnel.d.ts.map +1 -1
  55. package/dist/unplugin/tunnel.js +61 -74
  56. package/dist/unplugin/tunnel.js.map +1 -1
  57. package/package.json +17 -22
  58. package/dist/attach-orchestrator-0F0m_UqQ.js +0 -1845
  59. package/dist/attach-orchestrator-0F0m_UqQ.js.map +0 -1
  60. package/dist/attach-orchestrator-D65KxFy_.js +0 -1831
  61. package/dist/attach-orchestrator-D65KxFy_.js.map +0 -1
  62. package/dist/attach-orchestrator-DL3NQ9ca.js +0 -1846
  63. package/dist/attach-orchestrator-DL3NQ9ca.js.map +0 -1
  64. package/dist/bundle-C796JIwG.d.ts +0 -159
  65. package/dist/bundle-C796JIwG.d.ts.map +0 -1
  66. package/dist/capture-DsP525OZ.d.ts +0 -58
  67. package/dist/capture-DsP525OZ.d.ts.map +0 -1
  68. package/dist/cdp-connection-rP1WdnH5.d.ts +0 -287
  69. package/dist/cdp-connection-rP1WdnH5.d.ts.map +0 -1
  70. package/dist/cell-BaLvusOl.js +0 -68
  71. package/dist/cell-BaLvusOl.js.map +0 -1
  72. package/dist/cell-CBUS3-nT.js +0 -274
  73. package/dist/cell-CBUS3-nT.js.map +0 -1
  74. package/dist/cell-EBKKpAAT.js +0 -307
  75. package/dist/cell-EBKKpAAT.js.map +0 -1
  76. package/dist/chii-relay-BZ3HqWL5.js +0 -304
  77. package/dist/chii-relay-BZ3HqWL5.js.map +0 -1
  78. package/dist/chii-relay-D7eK2acz.cjs +0 -304
  79. package/dist/chii-relay-D7eK2acz.cjs.map +0 -1
  80. package/dist/debug-server-B3ABDrRI.js +0 -456
  81. package/dist/debug-server-B3ABDrRI.js.map +0 -1
  82. package/dist/debug-server-BWhwrVXa.js +0 -1158
  83. package/dist/debug-server-BWhwrVXa.js.map +0 -1
  84. package/dist/debug-server-CfQNxxGW.js +0 -600
  85. package/dist/debug-server-CfQNxxGW.js.map +0 -1
  86. package/dist/deeplink-B5-Hxu0Q.js +0 -62
  87. package/dist/deeplink-B5-Hxu0Q.js.map +0 -1
  88. package/dist/deeplink-BpO9qc-D.js +0 -62
  89. package/dist/deeplink-BpO9qc-D.js.map +0 -1
  90. package/dist/deeplink-BzdbA1gV.cjs +0 -62
  91. package/dist/deeplink-BzdbA1gV.cjs.map +0 -1
  92. package/dist/deeplink-DCScMYcp.cjs +0 -62
  93. package/dist/deeplink-DCScMYcp.cjs.map +0 -1
  94. package/dist/devtools-opener-3Drge_RJ.js +0 -75
  95. package/dist/devtools-opener-3Drge_RJ.js.map +0 -1
  96. package/dist/devtools-opener-B8nxrxqu.js +0 -71
  97. package/dist/devtools-opener-B8nxrxqu.js.map +0 -1
  98. package/dist/devtools-opener-BDY0w3_0.cjs +0 -68
  99. package/dist/devtools-opener-BDY0w3_0.cjs.map +0 -1
  100. package/dist/devtools-opener-BTl5A6Cd.js +0 -71
  101. package/dist/devtools-opener-BTl5A6Cd.js.map +0 -1
  102. package/dist/devtools-opener-CJpEsXXQ.js +0 -76
  103. package/dist/devtools-opener-CJpEsXXQ.js.map +0 -1
  104. package/dist/devtools-opener-CxtryS8c.js +0 -75
  105. package/dist/devtools-opener-CxtryS8c.js.map +0 -1
  106. package/dist/devtools-opener-iv1OwfJN.cjs +0 -68
  107. package/dist/devtools-opener-iv1OwfJN.cjs.map +0 -1
  108. package/dist/in-app/auto.d.ts.map +0 -1
  109. package/dist/mcp/cli.d.ts.map +0 -1
  110. package/dist/mcp/server.d.ts.map +0 -1
  111. package/dist/pool-DcaaOwUq.d.ts +0 -14761
  112. package/dist/pool-DcaaOwUq.d.ts.map +0 -1
  113. package/dist/qr-http-server--gl2-WKc.cjs +0 -1644
  114. package/dist/qr-http-server--gl2-WKc.cjs.map +0 -1
  115. package/dist/qr-http-server-Bb7lMeqi.js +0 -1644
  116. package/dist/qr-http-server-Bb7lMeqi.js.map +0 -1
  117. package/dist/qr-http-server-C_lqOrgc.js +0 -1644
  118. package/dist/qr-http-server-C_lqOrgc.js.map +0 -1
  119. package/dist/qr-http-server-CopuMbub.js +0 -1644
  120. package/dist/qr-http-server-CopuMbub.js.map +0 -1
  121. package/dist/qr-http-server-D-Off6K1.cjs +0 -1644
  122. package/dist/qr-http-server-D-Off6K1.cjs.map +0 -1
  123. package/dist/qr-http-server-DrbIVDjO.js +0 -1645
  124. package/dist/qr-http-server-DrbIVDjO.js.map +0 -1
  125. package/dist/qr-http-server-n1twN18z.js +0 -1644
  126. package/dist/qr-http-server-n1twN18z.js.map +0 -1
  127. package/dist/relay-factory-N9QobQxG.js +0 -206
  128. package/dist/relay-factory-N9QobQxG.js.map +0 -1
  129. package/dist/relay-secret-store-BPhN1upr.js +0 -240
  130. package/dist/relay-secret-store-BPhN1upr.js.map +0 -1
  131. package/dist/relay-secret-store-Bmyleu0A.js +0 -154
  132. package/dist/relay-secret-store-Bmyleu0A.js.map +0 -1
  133. package/dist/relay-secret-store-CQenfcSL.js +0 -154
  134. package/dist/relay-secret-store-CQenfcSL.js.map +0 -1
  135. package/dist/relay-secret-store-DKxs7zwq.js +0 -153
  136. package/dist/relay-secret-store-DKxs7zwq.js.map +0 -1
  137. package/dist/relay-secret-store-DWKdV-eY.cjs +0 -241
  138. package/dist/relay-secret-store-DWKdV-eY.cjs.map +0 -1
  139. package/dist/relay-secret-store-WJ8EGkIl.js +0 -153
  140. package/dist/relay-secret-store-WJ8EGkIl.js.map +0 -1
  141. package/dist/relay-url-store-1FGuSYAn.cjs +0 -115
  142. package/dist/relay-url-store-1FGuSYAn.cjs.map +0 -1
  143. package/dist/relay-url-store-BR2XodiO.js +0 -123
  144. package/dist/relay-url-store-BR2XodiO.js.map +0 -1
  145. package/dist/relay-url-store-Bskcyeg8.js +0 -114
  146. package/dist/relay-url-store-Bskcyeg8.js.map +0 -1
  147. package/dist/relay-url-store-CH63fVCm.js +0 -122
  148. package/dist/relay-url-store-CH63fVCm.js.map +0 -1
  149. package/dist/relay-url-store-DaY1QPes.js +0 -123
  150. package/dist/relay-url-store-DaY1QPes.js.map +0 -1
  151. package/dist/relay-url-store-xmUuTjXA.js +0 -122
  152. package/dist/relay-url-store-xmUuTjXA.js.map +0 -1
  153. package/dist/relay-worker-B5HKkGUY.js +0 -832
  154. package/dist/relay-worker-B5HKkGUY.js.map +0 -1
  155. package/dist/relay-worker-YdlpZQl9.d.ts +0 -214
  156. package/dist/relay-worker-YdlpZQl9.d.ts.map +0 -1
  157. package/dist/rolldown-runtime-DGkTqVfb.js +0 -15
  158. package/dist/rolldown-runtime-DUslC3ob.js +0 -14
  159. package/dist/runtime-kn9DxOeg.d.ts +0 -249
  160. package/dist/runtime-kn9DxOeg.d.ts.map +0 -1
  161. package/dist/test-runner/bin.js +0 -2584
  162. package/dist/test-runner/bin.js.map +0 -1
  163. package/dist/test-runner/bridge-stub.d.ts +0 -125
  164. package/dist/test-runner/bridge-stub.d.ts.map +0 -1
  165. package/dist/test-runner/bridge-stub.js +0 -92
  166. package/dist/test-runner/bridge-stub.js.map +0 -1
  167. package/dist/test-runner/bundle.d.ts +0 -2
  168. package/dist/test-runner/bundle.js +0 -439
  169. package/dist/test-runner/bundle.js.map +0 -1
  170. package/dist/test-runner/capture.d.ts +0 -2
  171. package/dist/test-runner/capture.js +0 -44
  172. package/dist/test-runner/capture.js.map +0 -1
  173. package/dist/test-runner/config.d.ts.map +0 -1
  174. package/dist/test-runner/method-pace.d.ts +0 -82
  175. package/dist/test-runner/method-pace.d.ts.map +0 -1
  176. package/dist/test-runner/method-pace.js +0 -120
  177. package/dist/test-runner/method-pace.js.map +0 -1
  178. package/dist/test-runner/pool.d.ts +0 -2
  179. package/dist/test-runner/pool.js +0 -136
  180. package/dist/test-runner/pool.js.map +0 -1
  181. package/dist/test-runner/relay-factory.d.ts +0 -11245
  182. package/dist/test-runner/relay-factory.d.ts.map +0 -1
  183. package/dist/test-runner/relay-factory.js +0 -206
  184. package/dist/test-runner/relay-factory.js.map +0 -1
  185. package/dist/test-runner/relay-worker.d.ts +0 -2
  186. package/dist/test-runner/relay-worker.js +0 -2
  187. package/dist/test-runner/report.d.ts +0 -163
  188. package/dist/test-runner/report.d.ts.map +0 -1
  189. package/dist/test-runner/report.js +0 -198
  190. package/dist/test-runner/report.js.map +0 -1
  191. package/dist/test-runner/rpc.d.ts +0 -56
  192. package/dist/test-runner/rpc.d.ts.map +0 -1
  193. package/dist/test-runner/rpc.js +0 -98
  194. package/dist/test-runner/rpc.js.map +0 -1
  195. package/dist/test-runner/runtime.d.ts +0 -2
  196. package/dist/test-runner/runtime.js +0 -659
  197. package/dist/test-runner/runtime.js.map +0 -1
  198. package/dist/test-runner/task-graph.d.ts +0 -38
  199. package/dist/test-runner/task-graph.d.ts.map +0 -1
  200. package/dist/test-runner/task-graph.js +0 -182
  201. package/dist/test-runner/task-graph.js.map +0 -1
  202. package/dist/throttle-DKKzX1qC.js +0 -59
  203. package/dist/throttle-DKKzX1qC.js.map +0 -1
  204. package/dist/totp-95OAa20j.js +0 -64
  205. package/dist/totp-95OAa20j.js.map +0 -1
  206. package/dist/totp-BjtoQNfu.cjs +0 -64
  207. package/dist/totp-BjtoQNfu.cjs.map +0 -1
  208. package/dist/totp-CZLLKfOC.js +0 -200
  209. package/dist/totp-CZLLKfOC.js.map +0 -1
  210. package/dist/totp-DAxys-r0.js +0 -199
  211. package/dist/totp-DAxys-r0.js.map +0 -1
  212. package/dist/totp-DIbrZtI7.js +0 -189
  213. package/dist/totp-DIbrZtI7.js.map +0 -1
  214. package/dist/totp-Df252ZdA.cjs +0 -192
  215. package/dist/totp-Df252ZdA.cjs.map +0 -1
  216. package/dist/totp-DfekTBk3.js +0 -211
  217. package/dist/totp-DfekTBk3.js.map +0 -1
  218. package/dist/totp-Dwft0Kz7.js +0 -3
  219. package/dist/totp-WY6l0ysP.js +0 -190
  220. package/dist/totp-WY6l0ysP.js.map +0 -1
  221. package/dist/tunnel-BVSMXctM.cjs.map +0 -1
  222. package/dist/tunnel-D7xkimBu.js.map +0 -1
  223. /package/dist/{test-runner/bin.d.ts → stubs/bin-devtools-mcp.d.ts} +0 -0
@@ -1,56 +1,52 @@
1
- import { RELAY_POOL_NAME, createRelayPool } from "./pool.js";
2
- import { t as createRelayConnectionFactory } from "../relay-factory-N9QobQxG.js";
3
- //#region src/test-runner/config.ts
4
- const DEFAULT_CONFIG = {
5
- include: ["**/*.ait.test.ts"],
6
- timeoutMs: 3e4,
7
- extraExternals: []
8
- };
1
+ //#region src/stubs/moved.ts
9
2
  /**
10
- * Define a phone-relay test configuration.
3
+ * Shared copy for the transition stubs left behind by the debug-surface split
4
+ * (#818).
11
5
  *
12
- * Merges user overrides with sensible defaults and returns a resolved
13
- * `PhoneTestConfig`. This object can be passed to `runTestFilesOverRelay`
14
- * (via the CLI or custom scripts) to run tests on a real device WebView.
6
+ * TRANSITION REMOVE IN 1.0.0. Everything in `src/stubs/` exists only so a
7
+ * consumer still on a pre-0.2.0 wiring gets a sentence telling them where the
8
+ * code went, instead of a bare `ERR_PACKAGE_PATH_NOT_EXPORTED`. Once 1.0.0
9
+ * drops them, the same import fails with the resolver's own error — which is
10
+ * the correct end state, just a worse first encounter.
11
+ *
12
+ * Two stub shapes, and the difference between them is a safety property, not a
13
+ * style choice:
14
+ *
15
+ * - Node-side tooling subpaths (`/mcp/server`, `/mcp/cli`, `/test-runner`)
16
+ * THROW on import. They only ever run in a developer's terminal, so failing
17
+ * loudly at the earliest possible moment is right.
18
+ * - The in-app subpaths (`/in-app`, `/in-app/auto`) MUST NOT throw. That code
19
+ * could be sitting in a shipped production bundle; a throw there takes the
20
+ * user's mini-app down. They degrade to a no-op plus one `console.error`.
15
21
  */
16
- function definePhoneTestConfig(userConfig) {
17
- return {
18
- include: userConfig?.include ?? DEFAULT_CONFIG.include,
19
- timeoutMs: userConfig?.timeoutMs ?? DEFAULT_CONFIG.timeoutMs,
20
- extraExternals: userConfig?.extraExternals ?? DEFAULT_CONFIG.extraExternals
21
- };
22
+ /** The package that now owns the MCP daemon, test runner, and dev bridge. */
23
+ const DEBUGGER_PACKAGE = "@ait-co/debugger";
24
+ /**
25
+ * Builds the migration sentence for a moved subpath.
26
+ *
27
+ * SECRET-HANDLING: fixed text plus the two specifiers only — no paths, hosts,
28
+ * URLs, or environment values.
29
+ */
30
+ function movedMessage(oldSubpath, newPackage, install) {
31
+ return [
32
+ `[@ait-co/devtools] '${oldSubpath}' 는 0.2.0에서 제거되었습니다.`,
33
+ `이 기능은 '${newPackage}' 로 이동했습니다.`,
34
+ `설치: ${install}`
35
+ ].join(" ");
22
36
  }
37
+ //#endregion
38
+ //#region src/stubs/test-runner.ts
23
39
  /**
24
- * Build the Vitest `test` config slice for running tests on a device over the
25
- * relay. Spread the result into your Vitest config:
40
+ * TRANSITION STUB REMOVE IN 1.0.0.
26
41
  *
27
- * @example
28
- * // vitest.config.ts
29
- * import { defineConfig } from 'vitest/config';
30
- * import { definePhoneVitestConfig } from '@ait-co/devtools/test-runner';
31
- * export default defineConfig({
32
- * test: definePhoneVitestConfig({ connection: myRelayFactory }),
33
- * });
42
+ * `@ait-co/devtools/test-runner` moved to `@ait-co/debugger/test-runner` (#818).
34
43
  *
35
- * Files matching `include` are dispatched to the relay pool (named
36
- * {@link RELAY_POOL_NAME}); everything else runs in Vitest's default pool.
44
+ * Throws at module evaluation. This entry is imported from a Vitest config,
45
+ * which Node evaluates in the developer's terminal — never in a shipped app
46
+ * bundle — so a loud failure is the helpful behaviour.
37
47
  */
38
- function definePhoneVitestConfig(userConfig) {
39
- const resolved = definePhoneTestConfig(userConfig);
40
- return {
41
- pool: createRelayPool({
42
- connection: userConfig.connection,
43
- run: {
44
- timeoutMs: resolved.timeoutMs,
45
- bundleOptions: { extraExternals: resolved.extraExternals },
46
- collectCaptures: userConfig.collectCaptures === true
47
- }
48
- }),
49
- include: resolved.include,
50
- testTimeout: resolved.timeoutMs
51
- };
52
- }
48
+ throw new Error(movedMessage("@ait-co/devtools/test-runner", `${DEBUGGER_PACKAGE}/test-runner`, `pnpm add -D ${DEBUGGER_PACKAGE}`));
53
49
  //#endregion
54
- export { RELAY_POOL_NAME, createRelayConnectionFactory, createRelayPool, definePhoneTestConfig, definePhoneVitestConfig };
50
+ export {};
55
51
 
56
52
  //# sourceMappingURL=config.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","names":[],"sources":["../../src/test-runner/config.ts"],"sourcesContent":["/**\n * Configuration helper for phone-relay test runs.\n *\n * `definePhoneTestConfig` is the user-facing entry point for configuring the\n * relay test runner. It mirrors the pattern of Vitest's `defineConfig` so\n * users can write:\n *\n * // ait-test.config.ts\n * import { definePhoneTestConfig } from '@ait-co/devtools/test-runner';\n * export default definePhoneTestConfig({ ... });\n *\n * The helper resolves user config and, when given a relay connection factory,\n * builds the Vitest `pool` (`createRelayPool`) so Vitest's own config\n * resolution routes matching files to the device over the relay (#645). Without\n * a connection factory it still returns the resolved config for use with the\n * lower-level `runTestFilesOverRelay` transport (#644).\n */\n\nimport type { RelayConnectionFactory } from './pool.js';\nimport { createRelayPool, RELAY_POOL_NAME } from './pool.js';\nimport {\n createRelayConnectionFactory,\n type RelayConnectionFactoryOptions,\n} from './relay-factory.js';\n\n/**\n * Resolved phone-test configuration returned by `definePhoneTestConfig`.\n */\nexport interface PhoneTestConfig {\n /**\n * Glob patterns (relative to cwd) for test files to run on the device.\n * Defaults to `['**\\/*.ait.test.ts']`.\n */\n include: string[];\n /**\n * Per-file evaluate timeout in milliseconds. Defaults to 30 000.\n */\n timeoutMs: number;\n /**\n * Additional esbuild `external` patterns for bundling.\n * The SDK package is always external; add more here if needed.\n */\n extraExternals: string[];\n}\n\n/** User-facing config shape accepted by `definePhoneTestConfig`. */\nexport interface PhoneTestUserConfig {\n include?: string[];\n timeoutMs?: number;\n extraExternals?: string[];\n}\n\nconst DEFAULT_CONFIG: PhoneTestConfig = {\n include: ['**/*.ait.test.ts'],\n timeoutMs: 30_000,\n extraExternals: [],\n};\n\n/**\n * Define a phone-relay test configuration.\n *\n * Merges user overrides with sensible defaults and returns a resolved\n * `PhoneTestConfig`. This object can be passed to `runTestFilesOverRelay`\n * (via the CLI or custom scripts) to run tests on a real device WebView.\n */\nexport function definePhoneTestConfig(userConfig?: PhoneTestUserConfig): PhoneTestConfig {\n return {\n include: userConfig?.include ?? DEFAULT_CONFIG.include,\n timeoutMs: userConfig?.timeoutMs ?? DEFAULT_CONFIG.timeoutMs,\n extraExternals: userConfig?.extraExternals ?? DEFAULT_CONFIG.extraExternals,\n };\n}\n\n/**\n * The slice of a Vitest `test` config this helper produces `pool`,\n * `include`, and `testTimeout`. Typed structurally (not against Vitest's\n * `InlineConfig`) so consumers can spread it into their own `defineConfig`\n * without this module taking a value dependency on `vitest`.\n */\nexport interface PhoneVitestTestConfig {\n /** The relay `PoolRunnerInitializer`, matched by `getFilePoolName`. */\n pool: ReturnType<typeof createRelayPool>;\n /** Glob patterns for device test files. */\n include: string[];\n /** Per-test timeout in ms (mirrors the relay per-file evaluate timeout). */\n testTimeout: number;\n}\n\n/** User config accepted by {@link definePhoneVitestConfig}. */\nexport interface PhoneVitestUserConfig extends PhoneTestUserConfig {\n /**\n * Opens/closes the CDP relay connection that tests run over. Required —\n * without it there is no device to dispatch files to.\n *\n * SECRET-HANDLING: the factory owns the relay wss/TOTP; this config object\n * never stores or logs those values.\n */\n connection: RelayConnectionFactory;\n /**\n * When `true`, the relay pool harvests `__AIT_CAPTURE__` console lines during\n * the run (forwarded as `collectCaptures` to `runTestFilesOverRelay`), so\n * `RelayRunReport.captures` is populated; a downstream consumer persists the\n * artifacts (pool-side artifact writing + cell-meta wiring lands in PR-S1).\n * Omitted/`false` = no capture harvest (build-only, zero listener overhead).\n */\n collectCaptures?: boolean;\n}\n\n/**\n * Build the Vitest `test` config slice for running tests on a device over the\n * relay. Spread the result into your Vitest config:\n *\n * @example\n * // vitest.config.ts\n * import { defineConfig } from 'vitest/config';\n * import { definePhoneVitestConfig } from '@ait-co/devtools/test-runner';\n * export default defineConfig({\n * test: definePhoneVitestConfig({ connection: myRelayFactory }),\n * });\n *\n * Files matching `include` are dispatched to the relay pool (named\n * {@link RELAY_POOL_NAME}); everything else runs in Vitest's default pool.\n */\nexport function definePhoneVitestConfig(userConfig: PhoneVitestUserConfig): PhoneVitestTestConfig {\n const resolved = definePhoneTestConfig(userConfig);\n return {\n pool: createRelayPool({\n connection: userConfig.connection,\n run: {\n timeoutMs: resolved.timeoutMs,\n bundleOptions: { extraExternals: resolved.extraExternals },\n // Harvest captures only when explicitly opted in keeps the build-only\n // path free of the live console listener (#696).\n collectCaptures: userConfig.collectCaptures === true,\n },\n }),\n include: resolved.include,\n testTimeout: resolved.timeoutMs,\n };\n}\n\nexport type { RelayConnectionFactory, RelayConnectionFactoryOptions };\n// Re-exported so a downstream `vitest.config.ts` can build the connection\n// factory straight from the `@ait-co/devtools/test-runner` barrel. relay-factory\n// keeps its heavy MCP graph behind dynamic imports, so this static re-export does\n// NOT drag chii/cloudflared/tools onto this Node-config entry (verified by\n// scripts/check-test-runner-dist.sh).\nexport { createRelayConnectionFactory, createRelayPool, RELAY_POOL_NAME };\n"],"mappings":";;;AAoDA,MAAM,iBAAkC;CACtC,SAAS,CAAC,mBAAmB;CAC7B,WAAW;CACX,gBAAgB,EAAE;CACnB;;;;;;;;AASD,SAAgB,sBAAsB,YAAmD;AACvF,QAAO;EACL,SAAS,YAAY,WAAW,eAAe;EAC/C,WAAW,YAAY,aAAa,eAAe;EACnD,gBAAgB,YAAY,kBAAkB,eAAe;EAC9D;;;;;;;;;;;;;;;;;AAqDH,SAAgB,wBAAwB,YAA0D;CAChG,MAAM,WAAW,sBAAsB,WAAW;AAClD,QAAO;EACL,MAAM,gBAAgB;GACpB,YAAY,WAAW;GACvB,KAAK;IACH,WAAW,SAAS;IACpB,eAAe,EAAE,gBAAgB,SAAS,gBAAgB;IAG1D,iBAAiB,WAAW,oBAAoB;IACjD;GACF,CAAC;EACF,SAAS,SAAS;EAClB,aAAa,SAAS;EACvB"}
1
+ {"version":3,"file":"config.js","names":[],"sources":["../../src/stubs/moved.ts","../../src/stubs/test-runner.ts"],"sourcesContent":["/**\n * Shared copy for the transition stubs left behind by the debug-surface split\n * (#818).\n *\n * TRANSITION REMOVE IN 1.0.0. Everything in `src/stubs/` exists only so a\n * consumer still on a pre-0.2.0 wiring gets a sentence telling them where the\n * code went, instead of a bare `ERR_PACKAGE_PATH_NOT_EXPORTED`. Once 1.0.0\n * drops them, the same import fails with the resolver's own error which is\n * the correct end state, just a worse first encounter.\n *\n * Two stub shapes, and the difference between them is a safety property, not a\n * style choice:\n *\n * - Node-side tooling subpaths (`/mcp/server`, `/mcp/cli`, `/test-runner`)\n * THROW on import. They only ever run in a developer's terminal, so failing\n * loudly at the earliest possible moment is right.\n * - The in-app subpaths (`/in-app`, `/in-app/auto`) MUST NOT throw. That code\n * could be sitting in a shipped production bundle; a throw there takes the\n * user's mini-app down. They degrade to a no-op plus one `console.error`.\n */\n\n/** The package that now owns the MCP daemon, test runner, and dev bridge. */\nexport const DEBUGGER_PACKAGE = '@ait-co/debugger';\n\n/** The package that now owns the on-device attach + eruda console. */\nexport const DEBUG_CONSOLE_PACKAGE = '@ait-co/debug-console';\n\n/**\n * Builds the migration sentence for a moved subpath.\n *\n * SECRET-HANDLING: fixed text plus the two specifiers only no paths, hosts,\n * URLs, or environment values.\n */\nexport function movedMessage(oldSubpath: string, newPackage: string, install: string): string {\n return [\n `[@ait-co/devtools] '${oldSubpath}' 0.2.0에서 제거되었습니다.`,\n `이 기능은 '${newPackage}' 이동했습니다.`,\n `설치: ${install}`,\n ].join(' ');\n}\n","/**\n * TRANSITION STUB REMOVE IN 1.0.0.\n *\n * `@ait-co/devtools/test-runner` moved to `@ait-co/debugger/test-runner` (#818).\n *\n * Throws at module evaluation. This entry is imported from a Vitest config,\n * which Node evaluates in the developer's terminal never in a shipped app\n * bundleso a loud failure is the helpful behaviour.\n */\n\nimport { DEBUGGER_PACKAGE, movedMessage } from './moved.js';\n\nthrow new Error(\n movedMessage(\n '@ait-co/devtools/test-runner',\n `${DEBUGGER_PACKAGE}/test-runner`,\n `pnpm add -D ${DEBUGGER_PACKAGE}`,\n ),\n);\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAsBA,MAAa,mBAAmB;;;;;;;AAWhC,SAAgB,aAAa,YAAoB,YAAoB,SAAyB;AAC5F,QAAO;EACL,uBAAuB,WAAW;EAClC,UAAU,WAAW;EACrB,OAAO;EACR,CAAC,KAAK,IAAI;;;;;;;;;;;;;AC1Bb,MAAM,IAAI,MACR,aACE,gCACA,GAAG,iBAAiB,eACpB,eAAe,mBAChB,CACF"}
@@ -1,3 +1,4 @@
1
+ const require_optional_peers = require("./optional-peers-CDEFhlhJ.cjs");
1
2
  let node_path = require("node:path");
2
3
  let node_fs = require("node:fs");
3
4
  let node_fs_promises = require("node:fs/promises");
@@ -105,97 +106,48 @@ async function printTunnelBanner(url, opts = {}) {
105
106
  }
106
107
  }
107
108
  /**
108
- * Heuristic: can this process open a GUI browser? Mirrors `canOpenBrowser` in
109
- * `src/mcp/tools.ts` but is re-declared here (not imported) so the tunnel path
110
- * does not statically pull the heavy MCP `tools.ts` module graph into the lazy
111
- * `import('./tunnel.js')` chunk. Kept in sync with the MCP copy.
112
- *
113
- * - macOS / Windows → assume yes (env-2 dev normally runs on the user's Mac).
114
- * - Linux → require `DISPLAY` or `WAYLAND_DISPLAY`.
115
- * - CI (`CI=true`/`CI=1`) → no.
116
- */
117
- function canOpenBrowser() {
118
- if (process.env.CI === "true" || process.env.CI === "1") return false;
119
- const platform = process.platform;
120
- if (platform === "darwin" || platform === "win32") return true;
121
- if (platform === "linux") return Boolean(process.env.DISPLAY ?? process.env.WAYLAND_DISPLAY);
122
- return false;
123
- }
124
- /**
125
- * Env-2 UX parity with env 3/4 (issue #408): when CDP wiring is on and a GUI is
109
+ * Env-2 UX parity with env 3 (issue #408): when CDP wiring is on and a GUI is
126
110
  * available, start the SAME `127.0.0.1` HTML dashboard (QR image + connect steps
127
- * + FAQ) that the MCP `start_attach` path serves, and auto-open it in the
111
+ * + FAQ) the debug daemon's `start_attach` path serves, and auto-open it in the
128
112
  * browser. headless / opt-out falls back to the terminal ASCII QR (printed
129
113
  * separately by {@link printTunnelBanner}).
130
114
  *
131
- * Every part the install-graph invariant depends on (`qrcode`, the MCP HTTP
132
- * server, the opener) is reached only through dynamic `import()` here, inside
133
- * the already-lazy `tunnel.js` chunk nothing is added to the common build
134
- * graph or the MCP-only install graph.
115
+ * DELEGATED (issue #817): the implementation now lives in `@ait-co/debugger`'s
116
+ * `/dev-bridge` subpath the one cross-repo code delegation of the package
117
+ * split. The dashboard needs the daemon's QR HTTP server, deep-link builder and
118
+ * TOTP minting, all of which moved into that package; keeping a second copy here
119
+ * would fork them. `@ait-co/debugger` is an OPTIONAL peer, so a consumer who
120
+ * never asks for env-2 CDP installs nothing extra.
121
+ *
122
+ * Why this is not a separate plugin: the relay `wss://` URL must exist BEFORE
123
+ * the QR banner is printed (the launcher QR carries `&relay=`), so the relay and
124
+ * the dashboard are order-dependent and stay one composed call in the dev loop.
135
125
  *
136
- * TOTP encapsulation: the dashboard's `getDashboardState` closure mints a FRESH
137
- * TOTP `at=` code on every call via `generateTotp(secret, Date.now())` and folds
138
- * it into a fresh `buildLauncherAttachUrl(...)`. Because the QR is re-rendered on
139
- * each SSE push / page reload from this closure, the code a phone scans is always
140
- * within its 30 s window no stale code is baked into static HTML.
126
+ * Degradation: when `@ait-co/debugger` is not installed the import fails and we
127
+ * return `undefined` exactly the same "no dashboard" outcome as a closed GUI
128
+ * gate, with the terminal ASCII QR standing alone. We do NOT fall back to a
129
+ * local copy of the daemon code. The caller (`./index.ts`) prints the install
130
+ * hint once when CDP was requested without the package.
141
131
  *
142
132
  * SECRET-HANDLING: the tunnel host, relay wssUrl, TOTP code, and `.ait_relay`
143
- * value/path are NEVER written to stdout/stderr/logs here. They live only inside
144
- * the attach URL (HTML body + `/qr.png` query, per qr-http-server's invariant).
145
- * The only thing opened/logged is `http://127.0.0.1:<port>` (local, safe).
133
+ * value/path are NEVER written to stdout/stderr/logs — neither here nor in the
134
+ * delegate. The only thing opened/logged is `http://127.0.0.1:<port>` (local).
146
135
  *
147
136
  * @returns the dashboard handle when it started (caller wires `close()` into the
148
- * tunnel cleanup), or `undefined` when skipped (no relay, `qr:false`, headless,
149
- * opt-out, or a start failure) — in which case ASCII QR fallback stands alone.
137
+ * tunnel cleanup), or `undefined` when skipped (package absent, no relay,
138
+ * `qr:false`, headless, opt-out, or a start failure) — in which case the ASCII
139
+ * QR fallback stands alone.
150
140
  */
151
141
  async function startTunnelDashboard(opts) {
152
- const log = opts.log ?? ((m) => console.log(m));
153
142
  if (!opts.relayWssUrl) return void 0;
154
143
  if (opts.qr === false) return void 0;
155
- const { isAutoDevtoolsDisabled } = await Promise.resolve().then(() => require("./devtools-opener-iv1OwfJN.cjs"));
156
- if (!(opts.shouldOpen ?? (() => !isAutoDevtoolsDisabled() && canOpenBrowser()))()) return void 0;
157
- const { startQrHttpServer } = await Promise.resolve().then(() => require("./qr-http-server--gl2-WKc.cjs"));
158
- const { buildLauncherAttachUrl } = await Promise.resolve().then(() => require("./deeplink-BzdbA1gV.cjs"));
159
- const { generateTotp } = await Promise.resolve().then(() => require("./totp-Df252ZdA.cjs"));
160
- const getDashboardState = () => {
161
- const secret = process.env.AIT_DEBUG_TOTP_SECRET;
162
- const totpCode = secret ? generateTotp(secret, Date.now()) : void 0;
163
- const attachUrl = buildLauncherAttachUrl(opts.tunnelUrl, opts.relayWssUrl, totpCode, { name: opts.name });
164
- return {
165
- tunnel: {
166
- up: true,
167
- wssUrl: opts.relayWssUrl
168
- },
169
- pages: null,
170
- attachUrl,
171
- inspectorUrl: null,
172
- mode: "relay-mobile",
173
- phase: "active"
174
- };
175
- };
176
- let server;
144
+ let devBridge;
177
145
  try {
178
- server = await startQrHttpServer(getDashboardState);
146
+ devBridge = await import(require_optional_peers.DEBUGGER_DEV_BRIDGE_ID);
179
147
  } catch {
180
148
  return;
181
149
  }
182
- let totpRefreshHandle = setInterval(() => {
183
- server.notifyStateChange();
184
- }, 2e4);
185
- totpRefreshHandle.unref();
186
- const dashboardUrl = `http://127.0.0.1:${server.port}`;
187
- const { openUrlInBrowser } = await Promise.resolve().then(() => require("./devtools-opener-iv1OwfJN.cjs"));
188
- log(openUrlInBrowser(dashboardUrl) ? ` │ Opened a QR dashboard in your browser: ${dashboardUrl}` : ` │ Open this QR dashboard in your browser: ${dashboardUrl}`);
189
- return {
190
- url: dashboardUrl,
191
- close: () => {
192
- if (totpRefreshHandle) {
193
- clearInterval(totpRefreshHandle);
194
- totpRefreshHandle = null;
195
- }
196
- return server.close();
197
- }
198
- };
150
+ return devBridge.startTunnelDashboard(opts);
199
151
  }
200
152
  /**
201
153
  * Sanitize cloudflared stderr output for error diagnostics (#421).
@@ -291,4 +243,4 @@ exports.printTunnelBanner = printTunnelBanner;
291
243
  exports.startQuickTunnel = startQuickTunnel;
292
244
  exports.startTunnelDashboard = startTunnelDashboard;
293
245
 
294
- //# sourceMappingURL=tunnel-BVSMXctM.cjs.map
246
+ //# sourceMappingURL=tunnel-BKZkOyQp.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tunnel-BKZkOyQp.cjs","names":["DEBUGGER_DEV_BRIDGE_ID"],"sources":["../src/unplugin/tunnel.ts"],"sourcesContent":["/**\n * Cloudflare quick-tunnel helper for the devtools unplugin.\n *\n * Loaded lazily (`await import('./tunnel.js')`) only when the `tunnel` option is\n * on, so `cloudflared` / `qrcode-terminal` are never pulled in for the common\n * case. This is the one place in `@ait-co/devtools` that depends on Node-only\n * APIs (`child_process` via the `cloudflared` wrapper) — keep it thin and out of\n * jsdom unit tests; the spawn path is verified by hand / e2e (same spirit as the\n * \"web 모드는 e2e\" rule in CLAUDE.md). The pure helpers below\n * (`parseTrycloudflareUrl`, `printTunnelBanner`) are unit-tested.\n */\n\nimport { existsSync } from 'node:fs';\nimport { mkdir } from 'node:fs/promises';\nimport { dirname } from 'node:path';\nimport { DEBUGGER_DEV_BRIDGE_ID } from './optional-peers.js';\n\n/** Matches the public URL cloudflared prints for an unauthenticated quick tunnel. */\nconst TRYCLOUDFLARE_RE = /https:\\/\\/[a-z0-9-]+\\.trycloudflare\\.com/i;\n\n/**\n * Extract the `https://<sub>.trycloudflare.com` URL from a line of cloudflared\n * output, or `null` if the line doesn't contain one. Pulled out as a pure\n * function so it can be unit-tested without spawning anything.\n */\nexport function parseTrycloudflareUrl(line: string): string | null {\n const m = line.match(TRYCLOUDFLARE_RE);\n return m ? m[0] : null;\n}\n\nexport interface PrintTunnelBannerOptions {\n /** Print an ASCII QR encoding the tunnel URL (default: true). */\n qr?: boolean;\n /** Sink for the banner text (default: `console.log`). Injected for testing. */\n log?: (msg: string) => void;\n /**\n * The `wss://` relay URL of the env-2 CDP tunnel, if `tunnel.cdp` is on. When\n * present the QR deep-link additionally carries `&debug=1&relay=<wss>` so the\n * framed PWA passes the in-app debug gate and attaches a Chii target — the\n * same single scan opens screen preview *and* CDP debugging.\n */\n relayWssUrl?: string;\n /**\n * Human-readable app name to embed as `name=` in the launcher deep-link (#498).\n * When provided (non-blank), the launcher partner bar shows this name instead of\n * the generic default.\n */\n name?: string;\n /**\n * The miniapp's webViewType. When `'game'`, the deep-link carries `&navBarType=game`\n * so the launcher enters game nav chrome automatically on scan (#584).\n * `'partner'` (the default) is the launcher's implicit default — not added to\n * keep the URL clean.\n */\n webViewType?: 'partner' | 'game';\n /**\n * Whether the miniapp's navigationBar has `transparentBackground: true`\n * (granite.config `navigationBar.transparentBackground`, SDK 2.8.0, #587).\n * When `true`, the deep-link carries `&navBarTransparent=1` so the launcher\n * partner bar renders with a transparent background (content shows through).\n * `false` / omitted → not added (URL clean, back-compat).\n */\n navBarTransparent?: boolean;\n /**\n * The miniapp's navigationBar theme (`granite.config `navigationBar.theme`,\n * SDK 2.8.0, #587). When `'light'` or `'dark'`, the deep-link carries\n * `&navBarTheme=<v>` so the launcher partner bar uses the matching foreground\n * colour. Omitted / other values → not added (URL clean, back-compat).\n */\n navBarTheme?: 'light' | 'dark';\n}\n\nconst LAUNCHER_URL = 'https://devtools.aitc.dev/launcher/';\n\n/**\n * Options for {@link buildLauncherDeepLink}.\n */\nexport interface BuildLauncherDeepLinkOptions {\n /**\n * `wss://` relay URL for env-2 CDP wiring. When present the deep-link carries\n * `&debug=1&relay=<wss>`.\n */\n relayWssUrl?: string;\n /**\n * Human-readable app name shown in the partner nav bar (`name=` param, #498).\n * Blank / whitespace-only values are not added.\n */\n name?: string;\n /**\n * The miniapp's webViewType. When `'game'`, adds `&navBarType=game` to the\n * deep-link so the launcher enters game nav chrome automatically on scan (#584).\n * `'partner'` (the launcher's implicit default) is not added to keep the URL\n * clean.\n */\n webViewType?: 'partner' | 'game';\n /**\n * Whether the miniapp's navigationBar has `transparentBackground: true`\n * (granite.config `navigationBar.transparentBackground`, SDK 2.8.0, #587).\n * When `true`, adds `&navBarTransparent=1` to the deep-link so the launcher\n * partner bar renders with a transparent background. Omitted when `false` /\n * undefined to keep the URL clean (back-compat).\n */\n navBarTransparent?: boolean;\n /**\n * The miniapp's navigationBar theme (granite.config `navigationBar.theme`,\n * SDK 2.8.0, #587). When `'light'` or `'dark'`, adds `&navBarTheme=<v>` to\n * the deep-link so the launcher partner bar uses the matching foreground colour.\n * Omitted when undefined / other values to keep the URL clean (back-compat).\n */\n navBarTheme?: 'light' | 'dark';\n}\n\n/**\n * Build the deep-link URL that QR codes encode: when the launcher PWA is\n * already on the phone's home screen, scanning this opens it directly into the\n * live view for `tunnelUrl` (the launcher consumes `?url=` and clears it).\n * Plain-text raw URL is no longer enough — the launcher gates its setup UI to\n * the installed PWA, so a raw tunnel URL opened in a normal browser tab would\n * land on a \"please install\" screen.\n *\n * When `opts.relayWssUrl` is given (env-2 CDP wiring), the deep-link also carries\n * `&debug=1&relay=<wss>`; the launcher folds those onto the framed tunnel URL so\n * the in-app debug gate's Layer C (`debug=1` opt-in + `relay=<wss>`) is met and\n * a Chii target.js is injected into the live view.\n *\n * When `opts.name` is given (non-blank), it is added as `&name=` so the launcher\n * partner bar shows the app name instead of the generic default (#498).\n *\n * When `opts.webViewType` is `'game'`, `&navBarType=game` is appended so the\n * launcher enters game nav chrome (floating capsule, no full bar) automatically\n * on scan. `'partner'` is the launcher's implicit default and is not added to\n * keep the URL clean (#584).\n *\n * When `opts.navBarTransparent` is `true`, `&navBarTransparent=1` is appended\n * so the launcher partner bar renders with a transparent background (#587).\n *\n * When `opts.navBarTheme` is `'light'` or `'dark'`, `&navBarTheme=<v>` is\n * appended so the launcher partner bar uses the matching foreground colour (#587).\n *\n * Back-compat: the second argument may also be a plain string (`relayWssUrl`)\n * for callers that haven't migrated to the options object yet.\n */\nexport function buildLauncherDeepLink(\n tunnelUrl: string,\n optsOrRelay?: string | BuildLauncherDeepLinkOptions,\n): string {\n // Normalise the overloaded second argument.\n const opts: BuildLauncherDeepLinkOptions =\n typeof optsOrRelay === 'string' ? { relayWssUrl: optsOrRelay } : (optsOrRelay ?? {});\n\n const base = `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}`;\n let url = base;\n if (opts.relayWssUrl) {\n url += `&debug=1&relay=${encodeURIComponent(opts.relayWssUrl)}`;\n }\n if (opts.name !== undefined && opts.name.trim() !== '') {\n url += `&name=${encodeURIComponent(opts.name.trim())}`;\n }\n if (opts.webViewType === 'game') {\n url += '&navBarType=game';\n }\n if (opts.navBarTransparent === true) {\n url += '&navBarTransparent=1';\n }\n if (opts.navBarTheme === 'light' || opts.navBarTheme === 'dark') {\n url += `&navBarTheme=${opts.navBarTheme}`;\n }\n return url;\n}\n\n/**\n * Print the terminal banner announcing the live tunnel: the public URL, an ASCII\n * QR encoding a launcher deep-link, and a one-line note that quick tunnels are\n * ephemeral, unauthenticated and not for production. Pure w.r.t. side effects\n * other than the injected `log` sink and `qrcode-terminal` — unit-tested.\n */\nexport async function printTunnelBanner(\n url: string,\n opts: PrintTunnelBannerOptions = {},\n): Promise<void> {\n const log = opts.log ?? ((m: string) => console.log(m));\n const deepLink = buildLauncherDeepLink(url, {\n relayWssUrl: opts.relayWssUrl,\n name: opts.name,\n webViewType: opts.webViewType,\n navBarTransparent: opts.navBarTransparent,\n navBarTheme: opts.navBarTheme,\n });\n const lines: string[] = [\n '',\n ' ┌─ @ait-co/devtools · live tunnel ────────────────────────────',\n ` │ ${url}`,\n ' │',\n ` │ Install the launcher PWA once: ${LAUNCHER_URL}`,\n ' │ Then scan the QR below — it opens the launcher directly',\n ' │ into this tunnel URL (no manual paste needed).',\n ...(opts.relayWssUrl\n ? [\n ' │ The same scan also attaches CDP — connect your AI host',\n ' │ to the relay and debug the live view on-device.',\n ]\n : []),\n ' │ Quick tunnels are unauthenticated, change every run, and are',\n ' │ not for production use.',\n ' └──────────────────────────────────────────────────────────────',\n '',\n ];\n log(lines.join('\\n'));\n\n if (opts.qr !== false) {\n // qrcode-terminal is only pulled in on this code path (ambient types live\n // in src/qrcode-terminal.d.ts).\n const qrcode = (await import('qrcode-terminal')).default;\n await new Promise<void>((resolve) => {\n qrcode.generate(deepLink, { small: true }, (out) => {\n log(out);\n resolve();\n });\n });\n }\n}\n\n/** Handle returned by {@link startTunnelDashboard}. */\nexport interface TunnelDashboard {\n /** `http://127.0.0.1:<port>` — the local dashboard URL opened in the browser. */\n url: string;\n /** Tear down the local HTTP server. Idempotent via the underlying server. */\n close: () => Promise<void>;\n}\n\nexport interface StartTunnelDashboardOptions {\n /** The public `https://*.trycloudflare.com` app tunnel URL the launcher frames. */\n tunnelUrl: string;\n /** The `wss://` relay URL of the env-2 CDP tunnel. REQUIRED — the dashboard is a CDP-only UX. */\n relayWssUrl: string;\n /** Mirror of `tunnel.qr` — when `false` the dashboard is skipped (no browser open). */\n qr?: boolean;\n /**\n * Override the GUI/opt-out gate (testing only). When omitted the real\n * `canOpenBrowser()` + `AIT_AUTO_DEVTOOLS` checks decide.\n */\n shouldOpen?: () => boolean;\n /** Sink for the one-line \"opened in browser\" note (default: `console.log`). Injected for testing. */\n log?: (msg: string) => void;\n /**\n * Human-readable app name to embed as `name=` in the launcher deep-link (#498).\n * When provided (non-blank), the launcher partner bar shows this name instead of\n * the generic default.\n */\n name?: string;\n}\n\n/**\n * Env-2 UX parity with env 3 (issue #408): when CDP wiring is on and a GUI is\n * available, start the SAME `127.0.0.1` HTML dashboard (QR image + connect steps\n * + FAQ) the debug daemon's `start_attach` path serves, and auto-open it in the\n * browser. headless / opt-out falls back to the terminal ASCII QR (printed\n * separately by {@link printTunnelBanner}).\n *\n * DELEGATED (issue #817): the implementation now lives in `@ait-co/debugger`'s\n * `/dev-bridge` subpath — the one cross-repo code delegation of the package\n * split. The dashboard needs the daemon's QR HTTP server, deep-link builder and\n * TOTP minting, all of which moved into that package; keeping a second copy here\n * would fork them. `@ait-co/debugger` is an OPTIONAL peer, so a consumer who\n * never asks for env-2 CDP installs nothing extra.\n *\n * Why this is not a separate plugin: the relay `wss://` URL must exist BEFORE\n * the QR banner is printed (the launcher QR carries `&relay=`), so the relay and\n * the dashboard are order-dependent and stay one composed call in the dev loop.\n *\n * Degradation: when `@ait-co/debugger` is not installed the import fails and we\n * return `undefined` — exactly the same \"no dashboard\" outcome as a closed GUI\n * gate, with the terminal ASCII QR standing alone. We do NOT fall back to a\n * local copy of the daemon code. The caller (`./index.ts`) prints the install\n * hint once when CDP was requested without the package.\n *\n * SECRET-HANDLING: the tunnel host, relay wssUrl, TOTP code, and `.ait_relay`\n * value/path are NEVER written to stdout/stderr/logs — neither here nor in the\n * delegate. The only thing opened/logged is `http://127.0.0.1:<port>` (local).\n *\n * @returns the dashboard handle when it started (caller wires `close()` into the\n * tunnel cleanup), or `undefined` when skipped (package absent, no relay,\n * `qr:false`, headless, opt-out, or a start failure) — in which case the ASCII\n * QR fallback stands alone.\n */\nexport async function startTunnelDashboard(\n opts: StartTunnelDashboardOptions,\n): Promise<TunnelDashboard | undefined> {\n // Gate: dashboard is a CDP-only UX (needs a relay to attach to). Checked here\n // as well as in the delegate so an absent relay never even probes the package.\n if (!opts.relayWssUrl) return undefined;\n // Opt-out via `tunnel.qr:false` (same toggle that suppresses the ASCII QR).\n if (opts.qr === false) return undefined;\n\n let devBridge: { startTunnelDashboard: typeof startTunnelDashboard };\n try {\n devBridge = (await import(DEBUGGER_DEV_BRIDGE_ID)) as {\n startTunnelDashboard: typeof startTunnelDashboard;\n };\n } catch {\n // Not installed (or an unexpected load failure) — degrade to ASCII QR only.\n // SECRET-HANDLING: the error is not surfaced (it can embed local paths).\n return undefined;\n }\n return devBridge.startTunnelDashboard(opts);\n}\n\nexport interface QuickTunnel {\n /** The public `https://*.trycloudflare.com` URL. */\n url: string;\n /** Stop the underlying `cloudflared` process. Idempotent. */\n stop: () => void;\n}\n\n/**\n * Sanitize cloudflared stderr output for error diagnostics (#421).\n *\n * Masks `*.trycloudflare.com` hostnames and full `https://` / `wss://` URLs\n * that carry those hostnames so tunnel host values never appear in error\n * messages. Diagnostic content (error codes, reasons, JSON blobs) is preserved.\n *\n * SECRET-HANDLING: tunnel host is SECRET-class per harness policy — only\n * placeholder text is emitted.\n */\nexport function sanitizeCloudflaredOutput(line: string): string {\n // Full URL forms: https://xxx.trycloudflare.com/… and wss://xxx.trycloudflare.com/…\n let s = line.replace(/(?:https?|wss?):\\/\\/[a-z0-9-]+\\.trycloudflare\\.com(?:\\/[^\\s]*)*/gi, (m) =>\n m.replace(/[a-z0-9-]+\\.trycloudflare\\.com/i, '<HOST>.trycloudflare.com'),\n );\n // Bare hostname without scheme (e.g. printed in cloudflared JSON logs)\n s = s.replace(/[a-z0-9-]+\\.trycloudflare\\.com/gi, '<HOST>.trycloudflare.com');\n return s;\n}\n\nconst URL_TIMEOUT_MS = 20_000;\n\n/**\n * Start an unauthenticated Cloudflare quick tunnel to `http://localhost:<port>`\n * and resolve once the public URL is known. Downloads the `cloudflared` binary\n * on first use if it is not already installed. Rejects with a friendly error if\n * no URL appears within {@link URL_TIMEOUT_MS}.\n */\nexport async function startQuickTunnel(port: number): Promise<QuickTunnel> {\n const cloudflared = await import('cloudflared');\n const { bin, install, Tunnel } = cloudflared;\n\n if (!existsSync(bin)) {\n await mkdir(dirname(bin), { recursive: true });\n await install(bin);\n }\n\n const tunnel = Tunnel.quick(`http://localhost:${port}`);\n let stopped = false;\n const stop = () => {\n if (stopped) return;\n stopped = true;\n try {\n tunnel.stop();\n } catch {\n // process may already be gone\n }\n };\n\n return new Promise<QuickTunnel>((resolve, reject) => {\n // #421: accumulate stderr to attach as diagnostics on failure.\n // SECRET-HANDLING: lines are sanitized before inclusion in error messages.\n const stderrLines: string[] = [];\n\n /**\n * Format the last `n` sanitized stderr lines as a diagnostic appendix.\n * Returns an empty string when no lines have been collected.\n */\n const stderrTail = (n = 15): string => {\n if (stderrLines.length === 0) return '';\n const tail = stderrLines.slice(-n).map(sanitizeCloudflaredOutput).join('');\n return `\\ncloudflared 출력 (마지막 ${Math.min(n, stderrLines.length)}줄):\\n${tail}`;\n };\n\n const timer = setTimeout(() => {\n cleanup();\n stop();\n reject(\n new Error(\n `[@ait-co/devtools] cloudflared did not report a tunnel URL within ${\n URL_TIMEOUT_MS / 1000\n }s. Check your network connection, or run \\`cloudflared tunnel --url http://localhost:${port}\\` manually.${stderrTail()}`,\n ),\n );\n }, URL_TIMEOUT_MS);\n\n const onUrl = (line: string) => {\n const found = parseTrycloudflareUrl(line);\n if (!found) return;\n clearTimeout(timer);\n // Stop scanning further output once we have the URL.\n cleanup();\n resolve({ url: found, stop });\n };\n\n // Accumulate stderr lines for diagnostics (#421). Named so it can be\n // removed from the listener list when cleanup() runs.\n const pushStderr = (line: string) => {\n stderrLines.push(line);\n };\n\n const cleanup = () => {\n tunnel.off('stdout', onUrl);\n tunnel.off('stderr', onUrl);\n tunnel.off('stderr', pushStderr);\n };\n\n // The library emits a parsed `url` event; we also scan raw stdout/stderr in\n // case the output format shifts.\n tunnel.once('url', onUrl);\n tunnel.on('stdout', onUrl);\n tunnel.on('stderr', onUrl);\n // Second stderr listener: accumulate all lines for error diagnostics.\n tunnel.on('stderr', pushStderr);\n tunnel.once('error', (err: Error) => {\n clearTimeout(timer);\n cleanup();\n stop();\n reject(err);\n });\n tunnel.once('exit', (code: number | null) => {\n if (stopped) return;\n clearTimeout(timer);\n cleanup();\n reject(\n new Error(\n `[@ait-co/devtools] cloudflared exited (code ${code ?? 'null'}) before reporting a tunnel URL.${stderrTail()}`,\n ),\n );\n });\n });\n}\n"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { t as DEBUGGER_DEV_BRIDGE_ID } from "./optional-peers-FdVbUJst.js";
1
2
  import { dirname } from "node:path";
2
3
  import { existsSync } from "node:fs";
3
4
  import { mkdir } from "node:fs/promises";
@@ -105,97 +106,48 @@ async function printTunnelBanner(url, opts = {}) {
105
106
  }
106
107
  }
107
108
  /**
108
- * Heuristic: can this process open a GUI browser? Mirrors `canOpenBrowser` in
109
- * `src/mcp/tools.ts` but is re-declared here (not imported) so the tunnel path
110
- * does not statically pull the heavy MCP `tools.ts` module graph into the lazy
111
- * `import('./tunnel.js')` chunk. Kept in sync with the MCP copy.
112
- *
113
- * - macOS / Windows → assume yes (env-2 dev normally runs on the user's Mac).
114
- * - Linux → require `DISPLAY` or `WAYLAND_DISPLAY`.
115
- * - CI (`CI=true`/`CI=1`) → no.
116
- */
117
- function canOpenBrowser() {
118
- if (process.env.CI === "true" || process.env.CI === "1") return false;
119
- const platform = process.platform;
120
- if (platform === "darwin" || platform === "win32") return true;
121
- if (platform === "linux") return Boolean(process.env.DISPLAY ?? process.env.WAYLAND_DISPLAY);
122
- return false;
123
- }
124
- /**
125
- * Env-2 UX parity with env 3/4 (issue #408): when CDP wiring is on and a GUI is
109
+ * Env-2 UX parity with env 3 (issue #408): when CDP wiring is on and a GUI is
126
110
  * available, start the SAME `127.0.0.1` HTML dashboard (QR image + connect steps
127
- * + FAQ) that the MCP `start_attach` path serves, and auto-open it in the
111
+ * + FAQ) the debug daemon's `start_attach` path serves, and auto-open it in the
128
112
  * browser. headless / opt-out falls back to the terminal ASCII QR (printed
129
113
  * separately by {@link printTunnelBanner}).
130
114
  *
131
- * Every part the install-graph invariant depends on (`qrcode`, the MCP HTTP
132
- * server, the opener) is reached only through dynamic `import()` here, inside
133
- * the already-lazy `tunnel.js` chunk nothing is added to the common build
134
- * graph or the MCP-only install graph.
115
+ * DELEGATED (issue #817): the implementation now lives in `@ait-co/debugger`'s
116
+ * `/dev-bridge` subpath the one cross-repo code delegation of the package
117
+ * split. The dashboard needs the daemon's QR HTTP server, deep-link builder and
118
+ * TOTP minting, all of which moved into that package; keeping a second copy here
119
+ * would fork them. `@ait-co/debugger` is an OPTIONAL peer, so a consumer who
120
+ * never asks for env-2 CDP installs nothing extra.
121
+ *
122
+ * Why this is not a separate plugin: the relay `wss://` URL must exist BEFORE
123
+ * the QR banner is printed (the launcher QR carries `&relay=`), so the relay and
124
+ * the dashboard are order-dependent and stay one composed call in the dev loop.
135
125
  *
136
- * TOTP encapsulation: the dashboard's `getDashboardState` closure mints a FRESH
137
- * TOTP `at=` code on every call via `generateTotp(secret, Date.now())` and folds
138
- * it into a fresh `buildLauncherAttachUrl(...)`. Because the QR is re-rendered on
139
- * each SSE push / page reload from this closure, the code a phone scans is always
140
- * within its 30 s window no stale code is baked into static HTML.
126
+ * Degradation: when `@ait-co/debugger` is not installed the import fails and we
127
+ * return `undefined` exactly the same "no dashboard" outcome as a closed GUI
128
+ * gate, with the terminal ASCII QR standing alone. We do NOT fall back to a
129
+ * local copy of the daemon code. The caller (`./index.ts`) prints the install
130
+ * hint once when CDP was requested without the package.
141
131
  *
142
132
  * SECRET-HANDLING: the tunnel host, relay wssUrl, TOTP code, and `.ait_relay`
143
- * value/path are NEVER written to stdout/stderr/logs here. They live only inside
144
- * the attach URL (HTML body + `/qr.png` query, per qr-http-server's invariant).
145
- * The only thing opened/logged is `http://127.0.0.1:<port>` (local, safe).
133
+ * value/path are NEVER written to stdout/stderr/logs — neither here nor in the
134
+ * delegate. The only thing opened/logged is `http://127.0.0.1:<port>` (local).
146
135
  *
147
136
  * @returns the dashboard handle when it started (caller wires `close()` into the
148
- * tunnel cleanup), or `undefined` when skipped (no relay, `qr:false`, headless,
149
- * opt-out, or a start failure) — in which case ASCII QR fallback stands alone.
137
+ * tunnel cleanup), or `undefined` when skipped (package absent, no relay,
138
+ * `qr:false`, headless, opt-out, or a start failure) — in which case the ASCII
139
+ * QR fallback stands alone.
150
140
  */
151
141
  async function startTunnelDashboard(opts) {
152
- const log = opts.log ?? ((m) => console.log(m));
153
142
  if (!opts.relayWssUrl) return void 0;
154
143
  if (opts.qr === false) return void 0;
155
- const { isAutoDevtoolsDisabled } = await import("./devtools-opener-B8nxrxqu.js");
156
- if (!(opts.shouldOpen ?? (() => !isAutoDevtoolsDisabled() && canOpenBrowser()))()) return void 0;
157
- const { startQrHttpServer } = await import("./qr-http-server-n1twN18z.js");
158
- const { buildLauncherAttachUrl } = await import("./deeplink-B5-Hxu0Q.js");
159
- const { generateTotp } = await import("./totp-DIbrZtI7.js");
160
- const getDashboardState = () => {
161
- const secret = process.env.AIT_DEBUG_TOTP_SECRET;
162
- const totpCode = secret ? generateTotp(secret, Date.now()) : void 0;
163
- const attachUrl = buildLauncherAttachUrl(opts.tunnelUrl, opts.relayWssUrl, totpCode, { name: opts.name });
164
- return {
165
- tunnel: {
166
- up: true,
167
- wssUrl: opts.relayWssUrl
168
- },
169
- pages: null,
170
- attachUrl,
171
- inspectorUrl: null,
172
- mode: "relay-mobile",
173
- phase: "active"
174
- };
175
- };
176
- let server;
144
+ let devBridge;
177
145
  try {
178
- server = await startQrHttpServer(getDashboardState);
146
+ devBridge = await import(DEBUGGER_DEV_BRIDGE_ID);
179
147
  } catch {
180
148
  return;
181
149
  }
182
- let totpRefreshHandle = setInterval(() => {
183
- server.notifyStateChange();
184
- }, 2e4);
185
- totpRefreshHandle.unref();
186
- const dashboardUrl = `http://127.0.0.1:${server.port}`;
187
- const { openUrlInBrowser } = await import("./devtools-opener-B8nxrxqu.js");
188
- log(openUrlInBrowser(dashboardUrl) ? ` │ Opened a QR dashboard in your browser: ${dashboardUrl}` : ` │ Open this QR dashboard in your browser: ${dashboardUrl}`);
189
- return {
190
- url: dashboardUrl,
191
- close: () => {
192
- if (totpRefreshHandle) {
193
- clearInterval(totpRefreshHandle);
194
- totpRefreshHandle = null;
195
- }
196
- return server.close();
197
- }
198
- };
150
+ return devBridge.startTunnelDashboard(opts);
199
151
  }
200
152
  /**
201
153
  * Sanitize cloudflared stderr output for error diagnostics (#421).
@@ -289,4 +241,4 @@ async function startQuickTunnel(port) {
289
241
  //#endregion
290
242
  export { printTunnelBanner, startQuickTunnel, startTunnelDashboard };
291
243
 
292
- //# sourceMappingURL=tunnel-D7xkimBu.js.map
244
+ //# sourceMappingURL=tunnel-CqSCIrdU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tunnel-CqSCIrdU.js","names":[],"sources":["../src/unplugin/tunnel.ts"],"sourcesContent":["/**\n * Cloudflare quick-tunnel helper for the devtools unplugin.\n *\n * Loaded lazily (`await import('./tunnel.js')`) only when the `tunnel` option is\n * on, so `cloudflared` / `qrcode-terminal` are never pulled in for the common\n * case. This is the one place in `@ait-co/devtools` that depends on Node-only\n * APIs (`child_process` via the `cloudflared` wrapper) — keep it thin and out of\n * jsdom unit tests; the spawn path is verified by hand / e2e (same spirit as the\n * \"web 모드는 e2e\" rule in CLAUDE.md). The pure helpers below\n * (`parseTrycloudflareUrl`, `printTunnelBanner`) are unit-tested.\n */\n\nimport { existsSync } from 'node:fs';\nimport { mkdir } from 'node:fs/promises';\nimport { dirname } from 'node:path';\nimport { DEBUGGER_DEV_BRIDGE_ID } from './optional-peers.js';\n\n/** Matches the public URL cloudflared prints for an unauthenticated quick tunnel. */\nconst TRYCLOUDFLARE_RE = /https:\\/\\/[a-z0-9-]+\\.trycloudflare\\.com/i;\n\n/**\n * Extract the `https://<sub>.trycloudflare.com` URL from a line of cloudflared\n * output, or `null` if the line doesn't contain one. Pulled out as a pure\n * function so it can be unit-tested without spawning anything.\n */\nexport function parseTrycloudflareUrl(line: string): string | null {\n const m = line.match(TRYCLOUDFLARE_RE);\n return m ? m[0] : null;\n}\n\nexport interface PrintTunnelBannerOptions {\n /** Print an ASCII QR encoding the tunnel URL (default: true). */\n qr?: boolean;\n /** Sink for the banner text (default: `console.log`). Injected for testing. */\n log?: (msg: string) => void;\n /**\n * The `wss://` relay URL of the env-2 CDP tunnel, if `tunnel.cdp` is on. When\n * present the QR deep-link additionally carries `&debug=1&relay=<wss>` so the\n * framed PWA passes the in-app debug gate and attaches a Chii target — the\n * same single scan opens screen preview *and* CDP debugging.\n */\n relayWssUrl?: string;\n /**\n * Human-readable app name to embed as `name=` in the launcher deep-link (#498).\n * When provided (non-blank), the launcher partner bar shows this name instead of\n * the generic default.\n */\n name?: string;\n /**\n * The miniapp's webViewType. When `'game'`, the deep-link carries `&navBarType=game`\n * so the launcher enters game nav chrome automatically on scan (#584).\n * `'partner'` (the default) is the launcher's implicit default — not added to\n * keep the URL clean.\n */\n webViewType?: 'partner' | 'game';\n /**\n * Whether the miniapp's navigationBar has `transparentBackground: true`\n * (granite.config `navigationBar.transparentBackground`, SDK 2.8.0, #587).\n * When `true`, the deep-link carries `&navBarTransparent=1` so the launcher\n * partner bar renders with a transparent background (content shows through).\n * `false` / omitted → not added (URL clean, back-compat).\n */\n navBarTransparent?: boolean;\n /**\n * The miniapp's navigationBar theme (`granite.config `navigationBar.theme`,\n * SDK 2.8.0, #587). When `'light'` or `'dark'`, the deep-link carries\n * `&navBarTheme=<v>` so the launcher partner bar uses the matching foreground\n * colour. Omitted / other values → not added (URL clean, back-compat).\n */\n navBarTheme?: 'light' | 'dark';\n}\n\nconst LAUNCHER_URL = 'https://devtools.aitc.dev/launcher/';\n\n/**\n * Options for {@link buildLauncherDeepLink}.\n */\nexport interface BuildLauncherDeepLinkOptions {\n /**\n * `wss://` relay URL for env-2 CDP wiring. When present the deep-link carries\n * `&debug=1&relay=<wss>`.\n */\n relayWssUrl?: string;\n /**\n * Human-readable app name shown in the partner nav bar (`name=` param, #498).\n * Blank / whitespace-only values are not added.\n */\n name?: string;\n /**\n * The miniapp's webViewType. When `'game'`, adds `&navBarType=game` to the\n * deep-link so the launcher enters game nav chrome automatically on scan (#584).\n * `'partner'` (the launcher's implicit default) is not added to keep the URL\n * clean.\n */\n webViewType?: 'partner' | 'game';\n /**\n * Whether the miniapp's navigationBar has `transparentBackground: true`\n * (granite.config `navigationBar.transparentBackground`, SDK 2.8.0, #587).\n * When `true`, adds `&navBarTransparent=1` to the deep-link so the launcher\n * partner bar renders with a transparent background. Omitted when `false` /\n * undefined to keep the URL clean (back-compat).\n */\n navBarTransparent?: boolean;\n /**\n * The miniapp's navigationBar theme (granite.config `navigationBar.theme`,\n * SDK 2.8.0, #587). When `'light'` or `'dark'`, adds `&navBarTheme=<v>` to\n * the deep-link so the launcher partner bar uses the matching foreground colour.\n * Omitted when undefined / other values to keep the URL clean (back-compat).\n */\n navBarTheme?: 'light' | 'dark';\n}\n\n/**\n * Build the deep-link URL that QR codes encode: when the launcher PWA is\n * already on the phone's home screen, scanning this opens it directly into the\n * live view for `tunnelUrl` (the launcher consumes `?url=` and clears it).\n * Plain-text raw URL is no longer enough — the launcher gates its setup UI to\n * the installed PWA, so a raw tunnel URL opened in a normal browser tab would\n * land on a \"please install\" screen.\n *\n * When `opts.relayWssUrl` is given (env-2 CDP wiring), the deep-link also carries\n * `&debug=1&relay=<wss>`; the launcher folds those onto the framed tunnel URL so\n * the in-app debug gate's Layer C (`debug=1` opt-in + `relay=<wss>`) is met and\n * a Chii target.js is injected into the live view.\n *\n * When `opts.name` is given (non-blank), it is added as `&name=` so the launcher\n * partner bar shows the app name instead of the generic default (#498).\n *\n * When `opts.webViewType` is `'game'`, `&navBarType=game` is appended so the\n * launcher enters game nav chrome (floating capsule, no full bar) automatically\n * on scan. `'partner'` is the launcher's implicit default and is not added to\n * keep the URL clean (#584).\n *\n * When `opts.navBarTransparent` is `true`, `&navBarTransparent=1` is appended\n * so the launcher partner bar renders with a transparent background (#587).\n *\n * When `opts.navBarTheme` is `'light'` or `'dark'`, `&navBarTheme=<v>` is\n * appended so the launcher partner bar uses the matching foreground colour (#587).\n *\n * Back-compat: the second argument may also be a plain string (`relayWssUrl`)\n * for callers that haven't migrated to the options object yet.\n */\nexport function buildLauncherDeepLink(\n tunnelUrl: string,\n optsOrRelay?: string | BuildLauncherDeepLinkOptions,\n): string {\n // Normalise the overloaded second argument.\n const opts: BuildLauncherDeepLinkOptions =\n typeof optsOrRelay === 'string' ? { relayWssUrl: optsOrRelay } : (optsOrRelay ?? {});\n\n const base = `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}`;\n let url = base;\n if (opts.relayWssUrl) {\n url += `&debug=1&relay=${encodeURIComponent(opts.relayWssUrl)}`;\n }\n if (opts.name !== undefined && opts.name.trim() !== '') {\n url += `&name=${encodeURIComponent(opts.name.trim())}`;\n }\n if (opts.webViewType === 'game') {\n url += '&navBarType=game';\n }\n if (opts.navBarTransparent === true) {\n url += '&navBarTransparent=1';\n }\n if (opts.navBarTheme === 'light' || opts.navBarTheme === 'dark') {\n url += `&navBarTheme=${opts.navBarTheme}`;\n }\n return url;\n}\n\n/**\n * Print the terminal banner announcing the live tunnel: the public URL, an ASCII\n * QR encoding a launcher deep-link, and a one-line note that quick tunnels are\n * ephemeral, unauthenticated and not for production. Pure w.r.t. side effects\n * other than the injected `log` sink and `qrcode-terminal` — unit-tested.\n */\nexport async function printTunnelBanner(\n url: string,\n opts: PrintTunnelBannerOptions = {},\n): Promise<void> {\n const log = opts.log ?? ((m: string) => console.log(m));\n const deepLink = buildLauncherDeepLink(url, {\n relayWssUrl: opts.relayWssUrl,\n name: opts.name,\n webViewType: opts.webViewType,\n navBarTransparent: opts.navBarTransparent,\n navBarTheme: opts.navBarTheme,\n });\n const lines: string[] = [\n '',\n ' ┌─ @ait-co/devtools · live tunnel ────────────────────────────',\n ` │ ${url}`,\n ' │',\n ` │ Install the launcher PWA once: ${LAUNCHER_URL}`,\n ' │ Then scan the QR below — it opens the launcher directly',\n ' │ into this tunnel URL (no manual paste needed).',\n ...(opts.relayWssUrl\n ? [\n ' │ The same scan also attaches CDP — connect your AI host',\n ' │ to the relay and debug the live view on-device.',\n ]\n : []),\n ' │ Quick tunnels are unauthenticated, change every run, and are',\n ' │ not for production use.',\n ' └──────────────────────────────────────────────────────────────',\n '',\n ];\n log(lines.join('\\n'));\n\n if (opts.qr !== false) {\n // qrcode-terminal is only pulled in on this code path (ambient types live\n // in src/qrcode-terminal.d.ts).\n const qrcode = (await import('qrcode-terminal')).default;\n await new Promise<void>((resolve) => {\n qrcode.generate(deepLink, { small: true }, (out) => {\n log(out);\n resolve();\n });\n });\n }\n}\n\n/** Handle returned by {@link startTunnelDashboard}. */\nexport interface TunnelDashboard {\n /** `http://127.0.0.1:<port>` — the local dashboard URL opened in the browser. */\n url: string;\n /** Tear down the local HTTP server. Idempotent via the underlying server. */\n close: () => Promise<void>;\n}\n\nexport interface StartTunnelDashboardOptions {\n /** The public `https://*.trycloudflare.com` app tunnel URL the launcher frames. */\n tunnelUrl: string;\n /** The `wss://` relay URL of the env-2 CDP tunnel. REQUIRED — the dashboard is a CDP-only UX. */\n relayWssUrl: string;\n /** Mirror of `tunnel.qr` — when `false` the dashboard is skipped (no browser open). */\n qr?: boolean;\n /**\n * Override the GUI/opt-out gate (testing only). When omitted the real\n * `canOpenBrowser()` + `AIT_AUTO_DEVTOOLS` checks decide.\n */\n shouldOpen?: () => boolean;\n /** Sink for the one-line \"opened in browser\" note (default: `console.log`). Injected for testing. */\n log?: (msg: string) => void;\n /**\n * Human-readable app name to embed as `name=` in the launcher deep-link (#498).\n * When provided (non-blank), the launcher partner bar shows this name instead of\n * the generic default.\n */\n name?: string;\n}\n\n/**\n * Env-2 UX parity with env 3 (issue #408): when CDP wiring is on and a GUI is\n * available, start the SAME `127.0.0.1` HTML dashboard (QR image + connect steps\n * + FAQ) the debug daemon's `start_attach` path serves, and auto-open it in the\n * browser. headless / opt-out falls back to the terminal ASCII QR (printed\n * separately by {@link printTunnelBanner}).\n *\n * DELEGATED (issue #817): the implementation now lives in `@ait-co/debugger`'s\n * `/dev-bridge` subpath — the one cross-repo code delegation of the package\n * split. The dashboard needs the daemon's QR HTTP server, deep-link builder and\n * TOTP minting, all of which moved into that package; keeping a second copy here\n * would fork them. `@ait-co/debugger` is an OPTIONAL peer, so a consumer who\n * never asks for env-2 CDP installs nothing extra.\n *\n * Why this is not a separate plugin: the relay `wss://` URL must exist BEFORE\n * the QR banner is printed (the launcher QR carries `&relay=`), so the relay and\n * the dashboard are order-dependent and stay one composed call in the dev loop.\n *\n * Degradation: when `@ait-co/debugger` is not installed the import fails and we\n * return `undefined` — exactly the same \"no dashboard\" outcome as a closed GUI\n * gate, with the terminal ASCII QR standing alone. We do NOT fall back to a\n * local copy of the daemon code. The caller (`./index.ts`) prints the install\n * hint once when CDP was requested without the package.\n *\n * SECRET-HANDLING: the tunnel host, relay wssUrl, TOTP code, and `.ait_relay`\n * value/path are NEVER written to stdout/stderr/logs — neither here nor in the\n * delegate. The only thing opened/logged is `http://127.0.0.1:<port>` (local).\n *\n * @returns the dashboard handle when it started (caller wires `close()` into the\n * tunnel cleanup), or `undefined` when skipped (package absent, no relay,\n * `qr:false`, headless, opt-out, or a start failure) — in which case the ASCII\n * QR fallback stands alone.\n */\nexport async function startTunnelDashboard(\n opts: StartTunnelDashboardOptions,\n): Promise<TunnelDashboard | undefined> {\n // Gate: dashboard is a CDP-only UX (needs a relay to attach to). Checked here\n // as well as in the delegate so an absent relay never even probes the package.\n if (!opts.relayWssUrl) return undefined;\n // Opt-out via `tunnel.qr:false` (same toggle that suppresses the ASCII QR).\n if (opts.qr === false) return undefined;\n\n let devBridge: { startTunnelDashboard: typeof startTunnelDashboard };\n try {\n devBridge = (await import(DEBUGGER_DEV_BRIDGE_ID)) as {\n startTunnelDashboard: typeof startTunnelDashboard;\n };\n } catch {\n // Not installed (or an unexpected load failure) — degrade to ASCII QR only.\n // SECRET-HANDLING: the error is not surfaced (it can embed local paths).\n return undefined;\n }\n return devBridge.startTunnelDashboard(opts);\n}\n\nexport interface QuickTunnel {\n /** The public `https://*.trycloudflare.com` URL. */\n url: string;\n /** Stop the underlying `cloudflared` process. Idempotent. */\n stop: () => void;\n}\n\n/**\n * Sanitize cloudflared stderr output for error diagnostics (#421).\n *\n * Masks `*.trycloudflare.com` hostnames and full `https://` / `wss://` URLs\n * that carry those hostnames so tunnel host values never appear in error\n * messages. Diagnostic content (error codes, reasons, JSON blobs) is preserved.\n *\n * SECRET-HANDLING: tunnel host is SECRET-class per harness policy — only\n * placeholder text is emitted.\n */\nexport function sanitizeCloudflaredOutput(line: string): string {\n // Full URL forms: https://xxx.trycloudflare.com/… and wss://xxx.trycloudflare.com/…\n let s = line.replace(/(?:https?|wss?):\\/\\/[a-z0-9-]+\\.trycloudflare\\.com(?:\\/[^\\s]*)*/gi, (m) =>\n m.replace(/[a-z0-9-]+\\.trycloudflare\\.com/i, '<HOST>.trycloudflare.com'),\n );\n // Bare hostname without scheme (e.g. printed in cloudflared JSON logs)\n s = s.replace(/[a-z0-9-]+\\.trycloudflare\\.com/gi, '<HOST>.trycloudflare.com');\n return s;\n}\n\nconst URL_TIMEOUT_MS = 20_000;\n\n/**\n * Start an unauthenticated Cloudflare quick tunnel to `http://localhost:<port>`\n * and resolve once the public URL is known. Downloads the `cloudflared` binary\n * on first use if it is not already installed. Rejects with a friendly error if\n * no URL appears within {@link URL_TIMEOUT_MS}.\n */\nexport async function startQuickTunnel(port: number): Promise<QuickTunnel> {\n const cloudflared = await import('cloudflared');\n const { bin, install, Tunnel } = cloudflared;\n\n if (!existsSync(bin)) {\n await mkdir(dirname(bin), { recursive: true });\n await install(bin);\n }\n\n const tunnel = Tunnel.quick(`http://localhost:${port}`);\n let stopped = false;\n const stop = () => {\n if (stopped) return;\n stopped = true;\n try {\n tunnel.stop();\n } catch {\n // process may already be gone\n }\n };\n\n return new Promise<QuickTunnel>((resolve, reject) => {\n // #421: accumulate stderr to attach as diagnostics on failure.\n // SECRET-HANDLING: lines are sanitized before inclusion in error messages.\n const stderrLines: string[] = [];\n\n /**\n * Format the last `n` sanitized stderr lines as a diagnostic appendix.\n * Returns an empty string when no lines have been collected.\n */\n const stderrTail = (n = 15): string => {\n if (stderrLines.length === 0) return '';\n const tail = stderrLines.slice(-n).map(sanitizeCloudflaredOutput).join('');\n return `\\ncloudflared 출력 (마지막 ${Math.min(n, stderrLines.length)}줄):\\n${tail}`;\n };\n\n const timer = setTimeout(() => {\n cleanup();\n stop();\n reject(\n new Error(\n `[@ait-co/devtools] cloudflared did not report a tunnel URL within ${\n URL_TIMEOUT_MS / 1000\n }s. Check your network connection, or run \\`cloudflared tunnel --url http://localhost:${port}\\` manually.${stderrTail()}`,\n ),\n );\n }, URL_TIMEOUT_MS);\n\n const onUrl = (line: string) => {\n const found = parseTrycloudflareUrl(line);\n if (!found) return;\n clearTimeout(timer);\n // Stop scanning further output once we have the URL.\n cleanup();\n resolve({ url: found, stop });\n };\n\n // Accumulate stderr lines for diagnostics (#421). Named so it can be\n // removed from the listener list when cleanup() runs.\n const pushStderr = (line: string) => {\n stderrLines.push(line);\n };\n\n const cleanup = () => {\n tunnel.off('stdout', onUrl);\n tunnel.off('stderr', onUrl);\n tunnel.off('stderr', pushStderr);\n };\n\n // The library emits a parsed `url` event; we also scan raw stdout/stderr in\n // case the output format shifts.\n tunnel.once('url', onUrl);\n tunnel.on('stdout', onUrl);\n tunnel.on('stderr', onUrl);\n // Second stderr listener: accumulate all lines for error diagnostics.\n tunnel.on('stderr', pushStderr);\n tunnel.once('error', (err: Error) => {\n clearTimeout(timer);\n cleanup();\n stop();\n reject(err);\n });\n tunnel.once('exit', (code: number | null) => {\n if (stopped) return;\n clearTimeout(timer);\n cleanup();\n reject(\n new Error(\n `[@ait-co/devtools] cloudflared exited (code ${code ?? 'null'}) before reporting a tunnel URL.${stderrTail()}`,\n ),\n );\n });\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAkBA,MAAM,mBAAmB;;;;;;AAOzB,SAAgB,sBAAsB,MAA6B;CACjE,MAAM,IAAI,KAAK,MAAM,iBAAiB;AACtC,QAAO,IAAI,EAAE,KAAK;;AA6CpB,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsErB,SAAgB,sBACd,WACA,aACQ;CAER,MAAM,OACJ,OAAO,gBAAgB,WAAW,EAAE,aAAa,aAAa,GAAI,eAAe,EAAE;CAGrF,IAAI,MADS,GAAG,aAAa,OAAO,mBAAmB,UAAU;AAEjE,KAAI,KAAK,YACP,QAAO,kBAAkB,mBAAmB,KAAK,YAAY;AAE/D,KAAI,KAAK,SAAS,KAAA,KAAa,KAAK,KAAK,MAAM,KAAK,GAClD,QAAO,SAAS,mBAAmB,KAAK,KAAK,MAAM,CAAC;AAEtD,KAAI,KAAK,gBAAgB,OACvB,QAAO;AAET,KAAI,KAAK,sBAAsB,KAC7B,QAAO;AAET,KAAI,KAAK,gBAAgB,WAAW,KAAK,gBAAgB,OACvD,QAAO,gBAAgB,KAAK;AAE9B,QAAO;;;;;;;;AAST,eAAsB,kBACpB,KACA,OAAiC,EAAE,EACpB;CACf,MAAM,MAAM,KAAK,SAAS,MAAc,QAAQ,IAAI,EAAE;CACtD,MAAM,WAAW,sBAAsB,KAAK;EAC1C,aAAa,KAAK;EAClB,MAAM,KAAK;EACX,aAAa,KAAK;EAClB,mBAAmB,KAAK;EACxB,aAAa,KAAK;EACnB,CAAC;AAoBF,KAnBwB;EACtB;EACA;EACA,QAAQ;EACR;EACA,wCAAwC;EACxC;EACA;EACA,GAAI,KAAK,cACL,CACE,+DACA,uDACD,GACD,EAAE;EACN;EACA;EACA;EACA;EACD,CACS,KAAK,KAAK,CAAC;AAErB,KAAI,KAAK,OAAO,OAAO;EAGrB,MAAM,UAAU,MAAM,OAAO,oBAAoB;AACjD,QAAM,IAAI,SAAe,YAAY;AACnC,UAAO,SAAS,UAAU,EAAE,OAAO,MAAM,GAAG,QAAQ;AAClD,QAAI,IAAI;AACR,aAAS;KACT;IACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmEN,eAAsB,qBACpB,MACsC;AAGtC,KAAI,CAAC,KAAK,YAAa,QAAO,KAAA;AAE9B,KAAI,KAAK,OAAO,MAAO,QAAO,KAAA;CAE9B,IAAI;AACJ,KAAI;AACF,cAAa,MAAM,OAAO;SAGpB;AAGN;;AAEF,QAAO,UAAU,qBAAqB,KAAK;;;;;;;;;;;;AAoB7C,SAAgB,0BAA0B,MAAsB;CAE9D,IAAI,IAAI,KAAK,QAAQ,sEAAsE,MACzF,EAAE,QAAQ,mCAAmC,2BAA2B,CACzE;AAED,KAAI,EAAE,QAAQ,oCAAoC,2BAA2B;AAC7E,QAAO;;AAGT,MAAM,iBAAiB;;;;;;;AAQvB,eAAsB,iBAAiB,MAAoC;CAEzE,MAAM,EAAE,KAAK,SAAS,WADF,MAAM,OAAO;AAGjC,KAAI,CAAC,WAAW,IAAI,EAAE;AACpB,QAAM,MAAM,QAAQ,IAAI,EAAE,EAAE,WAAW,MAAM,CAAC;AAC9C,QAAM,QAAQ,IAAI;;CAGpB,MAAM,SAAS,OAAO,MAAM,oBAAoB,OAAO;CACvD,IAAI,UAAU;CACd,MAAM,aAAa;AACjB,MAAI,QAAS;AACb,YAAU;AACV,MAAI;AACF,UAAO,MAAM;UACP;;AAKV,QAAO,IAAI,SAAsB,SAAS,WAAW;EAGnD,MAAM,cAAwB,EAAE;;;;;EAMhC,MAAM,cAAc,IAAI,OAAe;AACrC,OAAI,YAAY,WAAW,EAAG,QAAO;GACrC,MAAM,OAAO,YAAY,MAAM,CAAC,EAAE,CAAC,IAAI,0BAA0B,CAAC,KAAK,GAAG;AAC1E,UAAO,yBAAyB,KAAK,IAAI,GAAG,YAAY,OAAO,CAAC,OAAO;;EAGzE,MAAM,QAAQ,iBAAiB;AAC7B,YAAS;AACT,SAAM;AACN,0BACE,IAAI,MACF,qEACE,iBAAiB,IAClB,uFAAuF,KAAK,cAAc,YAAY,GACxH,CACF;KACA,eAAe;EAElB,MAAM,SAAS,SAAiB;GAC9B,MAAM,QAAQ,sBAAsB,KAAK;AACzC,OAAI,CAAC,MAAO;AACZ,gBAAa,MAAM;AAEnB,YAAS;AACT,WAAQ;IAAE,KAAK;IAAO;IAAM,CAAC;;EAK/B,MAAM,cAAc,SAAiB;AACnC,eAAY,KAAK,KAAK;;EAGxB,MAAM,gBAAgB;AACpB,UAAO,IAAI,UAAU,MAAM;AAC3B,UAAO,IAAI,UAAU,MAAM;AAC3B,UAAO,IAAI,UAAU,WAAW;;AAKlC,SAAO,KAAK,OAAO,MAAM;AACzB,SAAO,GAAG,UAAU,MAAM;AAC1B,SAAO,GAAG,UAAU,MAAM;AAE1B,SAAO,GAAG,UAAU,WAAW;AAC/B,SAAO,KAAK,UAAU,QAAe;AACnC,gBAAa,MAAM;AACnB,YAAS;AACT,SAAM;AACN,UAAO,IAAI;IACX;AACF,SAAO,KAAK,SAAS,SAAwB;AAC3C,OAAI,QAAS;AACb,gBAAa,MAAM;AACnB,YAAS;AACT,0BACE,IAAI,MACF,+CAA+C,QAAQ,OAAO,kCAAkC,YAAY,GAC7G,CACF;IACD;GACF"}