@ait-co/devtools 0.1.144 → 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 (181) hide show
  1. package/README.en.md +33 -217
  2. package/README.md +33 -246
  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/panel/index.js +1 -103
  20. package/dist/panel/index.js.map +1 -1
  21. package/dist/relay-url-store-CPZAn-T5.js +107 -0
  22. package/dist/relay-url-store-CPZAn-T5.js.map +1 -0
  23. package/dist/relay-url-store-DLjlvMSA.cjs +108 -0
  24. package/dist/relay-url-store-DLjlvMSA.cjs.map +1 -0
  25. package/dist/stubs/bin-devtools-mcp.js +58 -0
  26. package/dist/stubs/bin-devtools-mcp.js.map +1 -0
  27. package/dist/stubs/bin-devtools-test.d.ts +2 -0
  28. package/dist/stubs/bin-devtools-test.js +55 -0
  29. package/dist/stubs/bin-devtools-test.js.map +1 -0
  30. package/dist/test-runner/config.d.ts +1 -231
  31. package/dist/test-runner/config.js +41 -45
  32. package/dist/test-runner/config.js.map +1 -1
  33. package/dist/{tunnel-BGT9Curk.cjs → tunnel-BKZkOyQp.cjs} +1 -1
  34. package/dist/{tunnel-BGT9Curk.cjs.map → tunnel-BKZkOyQp.cjs.map} +1 -1
  35. package/dist/{tunnel-BOKmLzBO.js → tunnel-CqSCIrdU.js} +1 -1
  36. package/dist/{tunnel-BOKmLzBO.js.map → tunnel-CqSCIrdU.js.map} +1 -1
  37. package/dist/unplugin/index.cjs +9 -18
  38. package/dist/unplugin/index.cjs.map +1 -1
  39. package/dist/unplugin/index.d.cts +22 -3
  40. package/dist/unplugin/index.d.cts.map +1 -1
  41. package/dist/unplugin/index.d.ts +23 -4
  42. package/dist/unplugin/index.d.ts.map +1 -1
  43. package/dist/unplugin/index.js +10 -19
  44. package/dist/unplugin/index.js.map +1 -1
  45. package/package.json +10 -25
  46. package/dist/attach-orchestrator-0F0m_UqQ.js +0 -1845
  47. package/dist/attach-orchestrator-0F0m_UqQ.js.map +0 -1
  48. package/dist/attach-orchestrator-D65KxFy_.js +0 -1831
  49. package/dist/attach-orchestrator-D65KxFy_.js.map +0 -1
  50. package/dist/attach-orchestrator-DL3NQ9ca.js +0 -1846
  51. package/dist/attach-orchestrator-DL3NQ9ca.js.map +0 -1
  52. package/dist/bundle-C796JIwG.d.ts +0 -159
  53. package/dist/bundle-C796JIwG.d.ts.map +0 -1
  54. package/dist/capture-DsP525OZ.d.ts +0 -58
  55. package/dist/capture-DsP525OZ.d.ts.map +0 -1
  56. package/dist/cdp-connection-rP1WdnH5.d.ts +0 -287
  57. package/dist/cdp-connection-rP1WdnH5.d.ts.map +0 -1
  58. package/dist/cell-BaLvusOl.js +0 -68
  59. package/dist/cell-BaLvusOl.js.map +0 -1
  60. package/dist/cell-CBUS3-nT.js +0 -274
  61. package/dist/cell-CBUS3-nT.js.map +0 -1
  62. package/dist/cell-EBKKpAAT.js +0 -307
  63. package/dist/cell-EBKKpAAT.js.map +0 -1
  64. package/dist/chii-relay-B3ZhjGMi.js +0 -304
  65. package/dist/chii-relay-B3ZhjGMi.js.map +0 -1
  66. package/dist/chii-relay-CGMlePMd.cjs +0 -304
  67. package/dist/chii-relay-CGMlePMd.cjs.map +0 -1
  68. package/dist/debug-server-B3ABDrRI.js +0 -456
  69. package/dist/debug-server-B3ABDrRI.js.map +0 -1
  70. package/dist/debug-server-BWhwrVXa.js +0 -1158
  71. package/dist/debug-server-BWhwrVXa.js.map +0 -1
  72. package/dist/debug-server-CfQNxxGW.js +0 -600
  73. package/dist/debug-server-CfQNxxGW.js.map +0 -1
  74. package/dist/devtools-opener-3Drge_RJ.js +0 -75
  75. package/dist/devtools-opener-3Drge_RJ.js.map +0 -1
  76. package/dist/devtools-opener-CJpEsXXQ.js +0 -76
  77. package/dist/devtools-opener-CJpEsXXQ.js.map +0 -1
  78. package/dist/devtools-opener-CxtryS8c.js +0 -75
  79. package/dist/devtools-opener-CxtryS8c.js.map +0 -1
  80. package/dist/in-app/auto.d.ts.map +0 -1
  81. package/dist/mcp/cli.d.ts.map +0 -1
  82. package/dist/mcp/server.d.ts.map +0 -1
  83. package/dist/pool-DcaaOwUq.d.ts +0 -14761
  84. package/dist/pool-DcaaOwUq.d.ts.map +0 -1
  85. package/dist/qr-http-server-C_lqOrgc.js +0 -1644
  86. package/dist/qr-http-server-C_lqOrgc.js.map +0 -1
  87. package/dist/qr-http-server-CopuMbub.js +0 -1644
  88. package/dist/qr-http-server-CopuMbub.js.map +0 -1
  89. package/dist/qr-http-server-DrbIVDjO.js +0 -1645
  90. package/dist/qr-http-server-DrbIVDjO.js.map +0 -1
  91. package/dist/relay-factory-N9QobQxG.js +0 -206
  92. package/dist/relay-factory-N9QobQxG.js.map +0 -1
  93. package/dist/relay-secret-store-BR0YIkNv.cjs +0 -241
  94. package/dist/relay-secret-store-BR0YIkNv.cjs.map +0 -1
  95. package/dist/relay-secret-store-Bmyleu0A.js +0 -154
  96. package/dist/relay-secret-store-Bmyleu0A.js.map +0 -1
  97. package/dist/relay-secret-store-CQenfcSL.js +0 -154
  98. package/dist/relay-secret-store-CQenfcSL.js.map +0 -1
  99. package/dist/relay-secret-store-CYM8CBIF.js +0 -240
  100. package/dist/relay-secret-store-CYM8CBIF.js.map +0 -1
  101. package/dist/relay-secret-store-DKxs7zwq.js +0 -153
  102. package/dist/relay-secret-store-DKxs7zwq.js.map +0 -1
  103. package/dist/relay-secret-store-WJ8EGkIl.js +0 -153
  104. package/dist/relay-secret-store-WJ8EGkIl.js.map +0 -1
  105. package/dist/relay-url-store-BR2XodiO.js +0 -123
  106. package/dist/relay-url-store-BR2XodiO.js.map +0 -1
  107. package/dist/relay-url-store-C1as_m5G.cjs +0 -115
  108. package/dist/relay-url-store-C1as_m5G.cjs.map +0 -1
  109. package/dist/relay-url-store-CH63fVCm.js +0 -122
  110. package/dist/relay-url-store-CH63fVCm.js.map +0 -1
  111. package/dist/relay-url-store-CzFo_84F.js +0 -114
  112. package/dist/relay-url-store-CzFo_84F.js.map +0 -1
  113. package/dist/relay-url-store-DaY1QPes.js +0 -123
  114. package/dist/relay-url-store-DaY1QPes.js.map +0 -1
  115. package/dist/relay-url-store-xmUuTjXA.js +0 -122
  116. package/dist/relay-url-store-xmUuTjXA.js.map +0 -1
  117. package/dist/relay-worker-B5HKkGUY.js +0 -832
  118. package/dist/relay-worker-B5HKkGUY.js.map +0 -1
  119. package/dist/relay-worker-YdlpZQl9.d.ts +0 -214
  120. package/dist/relay-worker-YdlpZQl9.d.ts.map +0 -1
  121. package/dist/rolldown-runtime-DGkTqVfb.js +0 -15
  122. package/dist/rolldown-runtime-DUslC3ob.js +0 -14
  123. package/dist/runtime-kn9DxOeg.d.ts +0 -249
  124. package/dist/runtime-kn9DxOeg.d.ts.map +0 -1
  125. package/dist/test-runner/bin.js +0 -2584
  126. package/dist/test-runner/bin.js.map +0 -1
  127. package/dist/test-runner/bridge-stub.d.ts +0 -125
  128. package/dist/test-runner/bridge-stub.d.ts.map +0 -1
  129. package/dist/test-runner/bridge-stub.js +0 -92
  130. package/dist/test-runner/bridge-stub.js.map +0 -1
  131. package/dist/test-runner/bundle.d.ts +0 -2
  132. package/dist/test-runner/bundle.js +0 -439
  133. package/dist/test-runner/bundle.js.map +0 -1
  134. package/dist/test-runner/capture.d.ts +0 -2
  135. package/dist/test-runner/capture.js +0 -44
  136. package/dist/test-runner/capture.js.map +0 -1
  137. package/dist/test-runner/config.d.ts.map +0 -1
  138. package/dist/test-runner/method-pace.d.ts +0 -82
  139. package/dist/test-runner/method-pace.d.ts.map +0 -1
  140. package/dist/test-runner/method-pace.js +0 -120
  141. package/dist/test-runner/method-pace.js.map +0 -1
  142. package/dist/test-runner/pool.d.ts +0 -2
  143. package/dist/test-runner/pool.js +0 -136
  144. package/dist/test-runner/pool.js.map +0 -1
  145. package/dist/test-runner/relay-factory.d.ts +0 -11245
  146. package/dist/test-runner/relay-factory.d.ts.map +0 -1
  147. package/dist/test-runner/relay-factory.js +0 -206
  148. package/dist/test-runner/relay-factory.js.map +0 -1
  149. package/dist/test-runner/relay-worker.d.ts +0 -2
  150. package/dist/test-runner/relay-worker.js +0 -2
  151. package/dist/test-runner/report.d.ts +0 -163
  152. package/dist/test-runner/report.d.ts.map +0 -1
  153. package/dist/test-runner/report.js +0 -198
  154. package/dist/test-runner/report.js.map +0 -1
  155. package/dist/test-runner/rpc.d.ts +0 -56
  156. package/dist/test-runner/rpc.d.ts.map +0 -1
  157. package/dist/test-runner/rpc.js +0 -98
  158. package/dist/test-runner/rpc.js.map +0 -1
  159. package/dist/test-runner/runtime.d.ts +0 -2
  160. package/dist/test-runner/runtime.js +0 -659
  161. package/dist/test-runner/runtime.js.map +0 -1
  162. package/dist/test-runner/task-graph.d.ts +0 -38
  163. package/dist/test-runner/task-graph.d.ts.map +0 -1
  164. package/dist/test-runner/task-graph.js +0 -182
  165. package/dist/test-runner/task-graph.js.map +0 -1
  166. package/dist/throttle-DKKzX1qC.js +0 -59
  167. package/dist/throttle-DKKzX1qC.js.map +0 -1
  168. package/dist/totp-BqmCLSNA.js +0 -189
  169. package/dist/totp-BqmCLSNA.js.map +0 -1
  170. package/dist/totp-CMHR5lsW.cjs +0 -191
  171. package/dist/totp-CMHR5lsW.cjs.map +0 -1
  172. package/dist/totp-CZLLKfOC.js +0 -200
  173. package/dist/totp-CZLLKfOC.js.map +0 -1
  174. package/dist/totp-DAxys-r0.js +0 -199
  175. package/dist/totp-DAxys-r0.js.map +0 -1
  176. package/dist/totp-DfekTBk3.js +0 -211
  177. package/dist/totp-DfekTBk3.js.map +0 -1
  178. package/dist/totp-Dwft0Kz7.js +0 -3
  179. package/dist/totp-WY6l0ysP.js +0 -190
  180. package/dist/totp-WY6l0ysP.js.map +0 -1
  181. /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"}
@@ -243,4 +243,4 @@ exports.printTunnelBanner = printTunnelBanner;
243
243
  exports.startQuickTunnel = startQuickTunnel;
244
244
  exports.startTunnelDashboard = startTunnelDashboard;
245
245
 
246
- //# sourceMappingURL=tunnel-BGT9Curk.cjs.map
246
+ //# sourceMappingURL=tunnel-BKZkOyQp.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"tunnel-BGT9Curk.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
+ {"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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"}
@@ -241,4 +241,4 @@ async function startQuickTunnel(port) {
241
241
  //#endregion
242
242
  export { printTunnelBanner, startQuickTunnel, startTunnelDashboard };
243
243
 
244
- //# sourceMappingURL=tunnel-BOKmLzBO.js.map
244
+ //# sourceMappingURL=tunnel-CqSCIrdU.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tunnel-BOKmLzBO.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":"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"}
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":"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"}
@@ -230,7 +230,6 @@ const aitDevtoolsPlugin = (0, unplugin.createUnplugin)((options) => {
230
230
  });
231
231
  if (shouldTunnel) {
232
232
  let tunnel = null;
233
- let relayTunnel = null;
234
233
  let relay = null;
235
234
  let qrDashboard = null;
236
235
  let relayUrlDeleteFn = null;
@@ -243,7 +242,7 @@ const aitDevtoolsPlugin = (0, unplugin.createUnplugin)((options) => {
243
242
  console.warn("[@ait-co/devtools] tunnel: could not determine the dev server port; skipping.");
244
243
  return;
245
244
  }
246
- Promise.resolve().then(() => require("../tunnel-BGT9Curk.cjs")).then(async ({ startQuickTunnel, printTunnelBanner, startTunnelDashboard }) => {
245
+ Promise.resolve().then(() => require("../tunnel-BKZkOyQp.cjs")).then(async ({ startQuickTunnel, printTunnelBanner, startTunnelDashboard }) => {
247
246
  const t = await startQuickTunnel(port);
248
247
  tunnel = t;
249
248
  let relayWssUrl;
@@ -251,16 +250,11 @@ const aitDevtoolsPlugin = (0, unplugin.createUnplugin)((options) => {
251
250
  let relayLocalHttpUrl;
252
251
  if (tunnelConfig.cdp && !require_optional_peers.hasDebugger()) console.warn(`[@ait-co/devtools] tunnel: @ait-co/debugger가 없어 CDP relay를 건너뜁니다 — 화면 미리보기는 그대로 동작합니다. 설치: ${require_optional_peers.INSTALL_HINT}`);
253
252
  else if (tunnelConfig.cdp) try {
254
- const { ensureRelaySecret } = await Promise.resolve().then(() => require("../relay-secret-store-BR0YIkNv.cjs"));
255
- await ensureRelaySecret({ projectRoot: server.config.root });
256
- const { assertRelayAuthConfigured, buildRelayVerifyAuth } = await Promise.resolve().then(() => require("../totp-CMHR5lsW.cjs"));
257
- assertRelayAuthConfigured();
258
- const verifyAuth = buildRelayVerifyAuth();
259
- const { startChiiRelay } = await Promise.resolve().then(() => require("../chii-relay-CGMlePMd.cjs"));
253
+ const { startDevServerCdpRelay } = await import(require_optional_peers.DEBUGGER_DEV_BRIDGE_ID);
260
254
  let lastAuthRejectWarnAt = 0;
261
- const r = await startChiiRelay({
262
- port: 0,
263
- verifyAuth,
255
+ const r = await startDevServerCdpRelay({
256
+ projectRoot: server.config.root,
257
+ openTunnel: (localPort) => startQuickTunnel(localPort),
264
258
  onAuthReject: () => {
265
259
  const nowMs = Date.now();
266
260
  if (nowMs - lastAuthRejectWarnAt < 1e4) return;
@@ -269,11 +263,9 @@ const aitDevtoolsPlugin = (0, unplugin.createUnplugin)((options) => {
269
263
  }
270
264
  });
271
265
  relay = r;
272
- const rt = await startQuickTunnel(r.port);
273
- relayTunnel = rt;
274
- relayHttpUrl = rt.url;
275
- relayWssUrl = rt.url.replace(/^https:/, "wss:");
276
- relayLocalHttpUrl = `http://127.0.0.1:${r.port}`;
266
+ relayHttpUrl = r.httpUrl;
267
+ relayWssUrl = r.wssUrl;
268
+ relayLocalHttpUrl = r.localHttpUrl;
277
269
  } catch (err) {
278
270
  console.warn(`[@ait-co/devtools] tunnel: CDP relay not started — screen preview works without on-device debugging: ${err instanceof Error ? err.message : String(err)}`);
279
271
  }
@@ -293,7 +285,7 @@ const aitDevtoolsPlugin = (0, unplugin.createUnplugin)((options) => {
293
285
  navBarTransparent,
294
286
  navBarTheme
295
287
  });
296
- const { writeRelayUrls, deleteRelayUrls } = await Promise.resolve().then(() => require("../relay-url-store-C1as_m5G.cjs"));
288
+ const { writeRelayUrls, deleteRelayUrls } = await Promise.resolve().then(() => require("../relay-url-store-DLjlvMSA.cjs"));
297
289
  await writeRelayUrls({
298
290
  projectRoot: server.config.root,
299
291
  tunnelBaseUrl: t.url,
@@ -318,7 +310,6 @@ const aitDevtoolsPlugin = (0, unplugin.createUnplugin)((options) => {
318
310
  const cleanup = () => {
319
311
  parentWatcher?.stop();
320
312
  tunnel?.stop();
321
- relayTunnel?.stop();
322
313
  relay?.close();
323
314
  qrDashboard?.close();
324
315
  relayUrlDeleteFn?.(server.config.root);