@ait-co/devtools 0.1.58 → 0.1.59
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.
- package/dist/deeplink-BONXxWEO.cjs +44 -0
- package/dist/deeplink-BONXxWEO.cjs.map +1 -0
- package/dist/deeplink-CCGiyoHq.cjs +44 -0
- package/dist/deeplink-CCGiyoHq.cjs.map +1 -0
- package/dist/deeplink-CaO6hZVG.js +44 -0
- package/dist/deeplink-CaO6hZVG.js.map +1 -0
- package/dist/deeplink-Cqli4qzm.js +44 -0
- package/dist/deeplink-Cqli4qzm.js.map +1 -0
- package/dist/devtools-opener-BbUXBzgA.js +65 -0
- package/dist/devtools-opener-BbUXBzgA.js.map +1 -0
- package/dist/devtools-opener-Bp671YXu.cjs +62 -0
- package/dist/devtools-opener-Bp671YXu.cjs.map +1 -0
- package/dist/devtools-opener-D84kZFtR.js +65 -0
- package/dist/devtools-opener-D84kZFtR.js.map +1 -0
- package/dist/devtools-opener-h6A-UjzC.cjs +62 -0
- package/dist/devtools-opener-h6A-UjzC.cjs.map +1 -0
- package/dist/mcp/cli.js +860 -403
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/panel/index.d.ts +15 -9
- package/dist/panel/index.d.ts.map +1 -1
- package/dist/panel/index.js +27707 -1965
- package/dist/panel/index.js.map +1 -1
- package/dist/qr-http-server-Bk-9AO9Y.js +903 -0
- package/dist/qr-http-server-Bk-9AO9Y.js.map +1 -0
- package/dist/qr-http-server-C536UmTm.js +903 -0
- package/dist/qr-http-server-C536UmTm.js.map +1 -0
- package/dist/qr-http-server-CQZnEAcl.cjs +903 -0
- package/dist/qr-http-server-CQZnEAcl.cjs.map +1 -0
- package/dist/qr-http-server-GwRt-B9_.cjs +903 -0
- package/dist/qr-http-server-GwRt-B9_.cjs.map +1 -0
- package/dist/relay-secret-store-5A7_7zOp.js +111 -0
- package/dist/relay-secret-store-5A7_7zOp.js.map +1 -0
- package/dist/{relay-secret-store-DqyUoeXy.js → relay-secret-store-C4QQN5NA.js} +3 -3
- package/dist/{relay-secret-store-DqyUoeXy.js.map → relay-secret-store-C4QQN5NA.js.map} +1 -1
- package/dist/{relay-secret-store-DnTNl-9z.cjs → relay-secret-store-CLkF8Pa0.cjs} +3 -2
- package/dist/{relay-secret-store-DnTNl-9z.cjs.map → relay-secret-store-CLkF8Pa0.cjs.map} +1 -1
- package/dist/relay-url-store-COG2dSql.cjs +113 -0
- package/dist/relay-url-store-COG2dSql.cjs.map +1 -0
- package/dist/relay-url-store-WKfo0VQV.js +112 -0
- package/dist/relay-url-store-WKfo0VQV.js.map +1 -0
- package/dist/relay-url-store-qaoe0zOD.js +118 -0
- package/dist/relay-url-store-qaoe0zOD.js.map +1 -0
- package/dist/totp-86i_CNqh.js +3 -0
- package/dist/{totp-D0a8VwoR.js → totp-BIrJHsQn.js} +1 -1
- package/dist/{totp-D0a8VwoR.js.map → totp-BIrJHsQn.js.map} +1 -1
- package/dist/{totp-BkP5yU2K.js → totp-BjtKFt88.js} +2 -2
- package/dist/{totp-BkP5yU2K.js.map → totp-BjtKFt88.js.map} +1 -1
- package/dist/totp-BxtxuEt4.js +64 -0
- package/dist/totp-BxtxuEt4.js.map +1 -0
- package/dist/totp-D9rndqg_.cjs +64 -0
- package/dist/totp-D9rndqg_.cjs.map +1 -0
- package/dist/{totp-DLgGbySX.cjs → totp-DA8vjAi7.cjs} +2 -1
- package/dist/{totp-DLgGbySX.cjs.map → totp-DA8vjAi7.cjs.map} +1 -1
- package/dist/{tunnel-nKYPtc-g.cjs → tunnel-CAaBFOro.cjs} +114 -3
- package/dist/tunnel-CAaBFOro.cjs.map +1 -0
- package/dist/{tunnel-CI61NvPI.js → tunnel-COMs-wZU.js} +114 -4
- package/dist/tunnel-COMs-wZU.js.map +1 -0
- package/dist/unplugin/index.cjs +116 -4
- package/dist/unplugin/index.cjs.map +1 -1
- package/dist/unplugin/index.d.cts +20 -1
- package/dist/unplugin/index.d.cts.map +1 -1
- package/dist/unplugin/index.d.ts +20 -1
- package/dist/unplugin/index.d.ts.map +1 -1
- package/dist/unplugin/index.js +116 -5
- package/dist/unplugin/index.js.map +1 -1
- package/dist/unplugin/tunnel.cjs +114 -2
- package/dist/unplugin/tunnel.cjs.map +1 -1
- package/dist/unplugin/tunnel.d.cts +62 -1
- package/dist/unplugin/tunnel.d.cts.map +1 -1
- package/dist/unplugin/tunnel.d.ts +62 -1
- package/dist/unplugin/tunnel.d.ts.map +1 -1
- package/dist/unplugin/tunnel.js +113 -3
- package/dist/unplugin/tunnel.js.map +1 -1
- package/package.json +11 -3
- package/dist/totp-CQFmgOhM.js +0 -3
- package/dist/tunnel-CI61NvPI.js.map +0 -1
- package/dist/tunnel-nKYPtc-g.cjs.map +0 -1
|
@@ -50,12 +50,73 @@ declare function buildLauncherDeepLink(tunnelUrl: string, relayWssUrl?: string):
|
|
|
50
50
|
* other than the injected `log` sink and `qrcode-terminal` — unit-tested.
|
|
51
51
|
*/
|
|
52
52
|
declare function printTunnelBanner(url: string, opts?: PrintTunnelBannerOptions): Promise<void>;
|
|
53
|
+
/** Handle returned by {@link startTunnelDashboard}. */
|
|
54
|
+
interface TunnelDashboard {
|
|
55
|
+
/** `http://127.0.0.1:<port>` — the local dashboard URL opened in the browser. */
|
|
56
|
+
url: string;
|
|
57
|
+
/** Tear down the local HTTP server. Idempotent via the underlying server. */
|
|
58
|
+
close: () => Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
interface StartTunnelDashboardOptions {
|
|
61
|
+
/** The public `https://*.trycloudflare.com` app tunnel URL the launcher frames. */
|
|
62
|
+
tunnelUrl: string;
|
|
63
|
+
/** The `wss://` relay URL of the env-2 CDP tunnel. REQUIRED — the dashboard is a CDP-only UX. */
|
|
64
|
+
relayWssUrl: string;
|
|
65
|
+
/** Mirror of `tunnel.qr` — when `false` the dashboard is skipped (no browser open). */
|
|
66
|
+
qr?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Override the GUI/opt-out gate (testing only). When omitted the real
|
|
69
|
+
* `canOpenBrowser()` + `AIT_AUTO_DEVTOOLS` checks decide.
|
|
70
|
+
*/
|
|
71
|
+
shouldOpen?: () => boolean;
|
|
72
|
+
/** Sink for the one-line "opened in browser" note (default: `console.log`). Injected for testing. */
|
|
73
|
+
log?: (msg: string) => void;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Env-2 UX parity with env 3/4 (issue #408): when CDP wiring is on and a GUI is
|
|
77
|
+
* available, start the SAME `127.0.0.1` HTML dashboard (QR image + connect steps
|
|
78
|
+
* + FAQ) that the MCP `build_attach_url` path serves, and auto-open it in the
|
|
79
|
+
* browser. headless / opt-out falls back to the terminal ASCII QR (printed
|
|
80
|
+
* separately by {@link printTunnelBanner}).
|
|
81
|
+
*
|
|
82
|
+
* Every part the install-graph invariant depends on (`qrcode`, the MCP HTTP
|
|
83
|
+
* server, the opener) is reached only through dynamic `import()` here, inside
|
|
84
|
+
* the already-lazy `tunnel.js` chunk — nothing is added to the common build
|
|
85
|
+
* graph or the MCP-only install graph.
|
|
86
|
+
*
|
|
87
|
+
* TOTP encapsulation: the dashboard's `getDashboardState` closure mints a FRESH
|
|
88
|
+
* TOTP `at=` code on every call via `generateTotp(secret, Date.now())` and folds
|
|
89
|
+
* it into a fresh `buildLauncherAttachUrl(...)`. Because the QR is re-rendered on
|
|
90
|
+
* each SSE push / page reload from this closure, the code a phone scans is always
|
|
91
|
+
* within its 30 s window — no stale code is baked into static HTML.
|
|
92
|
+
*
|
|
93
|
+
* SECRET-HANDLING: the tunnel host, relay wssUrl, TOTP code, and `.ait_relay`
|
|
94
|
+
* value/path are NEVER written to stdout/stderr/logs here. They live only inside
|
|
95
|
+
* the attach URL (HTML body + `/qr.png` query, per qr-http-server's invariant).
|
|
96
|
+
* The only thing opened/logged is `http://127.0.0.1:<port>` (local, safe).
|
|
97
|
+
*
|
|
98
|
+
* @returns the dashboard handle when it started (caller wires `close()` into the
|
|
99
|
+
* tunnel cleanup), or `undefined` when skipped (no relay, `qr:false`, headless,
|
|
100
|
+
* opt-out, or a start failure) — in which case ASCII QR fallback stands alone.
|
|
101
|
+
*/
|
|
102
|
+
declare function startTunnelDashboard(opts: StartTunnelDashboardOptions): Promise<TunnelDashboard | undefined>;
|
|
53
103
|
interface QuickTunnel {
|
|
54
104
|
/** The public `https://*.trycloudflare.com` URL. */
|
|
55
105
|
url: string;
|
|
56
106
|
/** Stop the underlying `cloudflared` process. Idempotent. */
|
|
57
107
|
stop: () => void;
|
|
58
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Sanitize cloudflared stderr output for error diagnostics (#421).
|
|
111
|
+
*
|
|
112
|
+
* Masks `*.trycloudflare.com` hostnames and full `https://` / `wss://` URLs
|
|
113
|
+
* that carry those hostnames so tunnel host values never appear in error
|
|
114
|
+
* messages. Diagnostic content (error codes, reasons, JSON blobs) is preserved.
|
|
115
|
+
*
|
|
116
|
+
* SECRET-HANDLING: tunnel host is SECRET-class per harness policy — only
|
|
117
|
+
* placeholder text is emitted.
|
|
118
|
+
*/
|
|
119
|
+
declare function sanitizeCloudflaredOutput(line: string): string;
|
|
59
120
|
/**
|
|
60
121
|
* Start an unauthenticated Cloudflare quick tunnel to `http://localhost:<port>`
|
|
61
122
|
* and resolve once the public URL is known. Downloads the `cloudflared` binary
|
|
@@ -64,5 +125,5 @@ interface QuickTunnel {
|
|
|
64
125
|
*/
|
|
65
126
|
declare function startQuickTunnel(port: number): Promise<QuickTunnel>;
|
|
66
127
|
//#endregion
|
|
67
|
-
export { PrintTunnelBannerOptions, QuickTunnel, buildLauncherDeepLink, parseTrycloudflareUrl, printTunnelBanner, startQuickTunnel };
|
|
128
|
+
export { PrintTunnelBannerOptions, QuickTunnel, StartTunnelDashboardOptions, TunnelDashboard, buildLauncherDeepLink, parseTrycloudflareUrl, printTunnelBanner, sanitizeCloudflaredOutput, startQuickTunnel, startTunnelDashboard };
|
|
68
129
|
//# sourceMappingURL=tunnel.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tunnel.d.ts","names":[],"sources":["../../src/unplugin/tunnel.ts"],"mappings":";;AAwBA;;;;;AAKA;;;;;;;;;;iBALgB,qBAAA,CAAsB,IAAA;AAAA,UAKrB,wBAAA;;EAEf,EAAA;EA2B2E;EAzB3E,GAAA,IAAO,GAAA;EAqC8B;;;;;;EA9BrC,WAAA;AAAA;;
|
|
1
|
+
{"version":3,"file":"tunnel.d.ts","names":[],"sources":["../../src/unplugin/tunnel.ts"],"mappings":";;AAwBA;;;;;AAKA;;;;;;;;;;iBALgB,qBAAA,CAAsB,IAAA;AAAA,UAKrB,wBAAA;;EAEf,EAAA;EA2B2E;EAzB3E,GAAA,IAAO,GAAA;EAqC8B;;;;;;EA9BrC,WAAA;AAAA;;AA2FF;;;;;;;;;AAOA;;;iBAhFgB,qBAAA,CAAsB,SAAA,UAAmB,WAAA;;;;;;;iBAYnC,iBAAA,CACpB,GAAA,UACA,IAAA,GAAM,wBAAA,GACL,OAAA;;UA0Dc,eAAA;EAkDyB;EAhDxC,GAAA;EAiDM;EA/CN,KAAA,QAAa,OAAA;AAAA;AAAA,UAGE,2BAAA;EA6CP;EA3CR,SAAA;EA0CA;EAxCA,WAAA;EAyCS;EAvCT,EAAA;EAuCwB;AA8D1B;;;EAhGE,UAAA;EAoGI;EAlGJ,GAAA,IAAO,GAAA;AAAA;;;;AAiIT;;;;;;;;;;;;;;;;;;;;;;;;iBAnGsB,oBAAA,CACpB,IAAA,EAAM,2BAAA,GACL,OAAA,CAAQ,eAAA;AAAA,UA8DM,WAAA;;EAEf,GAAA;;EAEA,IAAA;AAAA;;;;;;;;;;;iBAac,yBAAA,CAA0B,IAAA;;;;;;;iBAkBpB,gBAAA,CAAiB,IAAA,WAAe,OAAA,CAAQ,WAAA"}
|
package/dist/unplugin/tunnel.js
CHANGED
|
@@ -76,6 +76,101 @@ async function printTunnelBanner(url, opts = {}) {
|
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Heuristic: can this process open a GUI browser? Mirrors `canOpenBrowser` in
|
|
81
|
+
* `src/mcp/tools.ts` but is re-declared here (not imported) so the tunnel path
|
|
82
|
+
* does not statically pull the heavy MCP `tools.ts` module graph into the lazy
|
|
83
|
+
* `import('./tunnel.js')` chunk. Kept in sync with the MCP copy.
|
|
84
|
+
*
|
|
85
|
+
* - macOS / Windows → assume yes (env-2 dev normally runs on the user's Mac).
|
|
86
|
+
* - Linux → require `DISPLAY` or `WAYLAND_DISPLAY`.
|
|
87
|
+
* - CI (`CI=true`/`CI=1`) → no.
|
|
88
|
+
*/
|
|
89
|
+
function canOpenBrowser() {
|
|
90
|
+
if (process.env.CI === "true" || process.env.CI === "1") return false;
|
|
91
|
+
const platform = process.platform;
|
|
92
|
+
if (platform === "darwin" || platform === "win32") return true;
|
|
93
|
+
if (platform === "linux") return Boolean(process.env.DISPLAY ?? process.env.WAYLAND_DISPLAY);
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Env-2 UX parity with env 3/4 (issue #408): when CDP wiring is on and a GUI is
|
|
98
|
+
* available, start the SAME `127.0.0.1` HTML dashboard (QR image + connect steps
|
|
99
|
+
* + FAQ) that the MCP `build_attach_url` path serves, and auto-open it in the
|
|
100
|
+
* browser. headless / opt-out falls back to the terminal ASCII QR (printed
|
|
101
|
+
* separately by {@link printTunnelBanner}).
|
|
102
|
+
*
|
|
103
|
+
* Every part the install-graph invariant depends on (`qrcode`, the MCP HTTP
|
|
104
|
+
* server, the opener) is reached only through dynamic `import()` here, inside
|
|
105
|
+
* the already-lazy `tunnel.js` chunk — nothing is added to the common build
|
|
106
|
+
* graph or the MCP-only install graph.
|
|
107
|
+
*
|
|
108
|
+
* TOTP encapsulation: the dashboard's `getDashboardState` closure mints a FRESH
|
|
109
|
+
* TOTP `at=` code on every call via `generateTotp(secret, Date.now())` and folds
|
|
110
|
+
* it into a fresh `buildLauncherAttachUrl(...)`. Because the QR is re-rendered on
|
|
111
|
+
* each SSE push / page reload from this closure, the code a phone scans is always
|
|
112
|
+
* within its 30 s window — no stale code is baked into static HTML.
|
|
113
|
+
*
|
|
114
|
+
* SECRET-HANDLING: the tunnel host, relay wssUrl, TOTP code, and `.ait_relay`
|
|
115
|
+
* value/path are NEVER written to stdout/stderr/logs here. They live only inside
|
|
116
|
+
* the attach URL (HTML body + `/qr.png` query, per qr-http-server's invariant).
|
|
117
|
+
* The only thing opened/logged is `http://127.0.0.1:<port>` (local, safe).
|
|
118
|
+
*
|
|
119
|
+
* @returns the dashboard handle when it started (caller wires `close()` into the
|
|
120
|
+
* tunnel cleanup), or `undefined` when skipped (no relay, `qr:false`, headless,
|
|
121
|
+
* opt-out, or a start failure) — in which case ASCII QR fallback stands alone.
|
|
122
|
+
*/
|
|
123
|
+
async function startTunnelDashboard(opts) {
|
|
124
|
+
const log = opts.log ?? ((m) => console.log(m));
|
|
125
|
+
if (!opts.relayWssUrl) return void 0;
|
|
126
|
+
if (opts.qr === false) return void 0;
|
|
127
|
+
const { isAutoDevtoolsDisabled } = await import("../devtools-opener-D84kZFtR.js");
|
|
128
|
+
if (!(opts.shouldOpen ?? (() => !isAutoDevtoolsDisabled() && canOpenBrowser()))()) return void 0;
|
|
129
|
+
const { startQrHttpServer } = await import("../qr-http-server-Bk-9AO9Y.js");
|
|
130
|
+
const { buildLauncherAttachUrl } = await import("../deeplink-Cqli4qzm.js");
|
|
131
|
+
const { generateTotp } = await import("../totp-BxtxuEt4.js");
|
|
132
|
+
const getDashboardState = () => {
|
|
133
|
+
const secret = process.env.AIT_DEBUG_TOTP_SECRET;
|
|
134
|
+
const totpCode = secret ? generateTotp(secret, Date.now()) : void 0;
|
|
135
|
+
const attachUrl = buildLauncherAttachUrl(opts.tunnelUrl, opts.relayWssUrl, totpCode);
|
|
136
|
+
return {
|
|
137
|
+
tunnel: {
|
|
138
|
+
up: true,
|
|
139
|
+
wssUrl: opts.relayWssUrl
|
|
140
|
+
},
|
|
141
|
+
pages: null,
|
|
142
|
+
attachUrl
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
let server;
|
|
146
|
+
try {
|
|
147
|
+
server = await startQrHttpServer(getDashboardState);
|
|
148
|
+
} catch {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const dashboardUrl = `http://127.0.0.1:${server.port}`;
|
|
152
|
+
const { openUrlInBrowser } = await import("../devtools-opener-D84kZFtR.js");
|
|
153
|
+
log(openUrlInBrowser(dashboardUrl) ? ` │ Opened a QR dashboard in your browser: ${dashboardUrl}` : ` │ Open this QR dashboard in your browser: ${dashboardUrl}`);
|
|
154
|
+
return {
|
|
155
|
+
url: dashboardUrl,
|
|
156
|
+
close: () => server.close()
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Sanitize cloudflared stderr output for error diagnostics (#421).
|
|
161
|
+
*
|
|
162
|
+
* Masks `*.trycloudflare.com` hostnames and full `https://` / `wss://` URLs
|
|
163
|
+
* that carry those hostnames so tunnel host values never appear in error
|
|
164
|
+
* messages. Diagnostic content (error codes, reasons, JSON blobs) is preserved.
|
|
165
|
+
*
|
|
166
|
+
* SECRET-HANDLING: tunnel host is SECRET-class per harness policy — only
|
|
167
|
+
* placeholder text is emitted.
|
|
168
|
+
*/
|
|
169
|
+
function sanitizeCloudflaredOutput(line) {
|
|
170
|
+
let s = line.replace(/(?:https?|wss?):\/\/[a-z0-9-]+\.trycloudflare\.com(?:\/[^\s]*)*/gi, (m) => m.replace(/[a-z0-9-]+\.trycloudflare\.com/i, "<HOST>.trycloudflare.com"));
|
|
171
|
+
s = s.replace(/[a-z0-9-]+\.trycloudflare\.com/gi, "<HOST>.trycloudflare.com");
|
|
172
|
+
return s;
|
|
173
|
+
}
|
|
79
174
|
const URL_TIMEOUT_MS = 2e4;
|
|
80
175
|
/**
|
|
81
176
|
* Start an unauthenticated Cloudflare quick tunnel to `http://localhost:<port>`
|
|
@@ -99,10 +194,20 @@ async function startQuickTunnel(port) {
|
|
|
99
194
|
} catch {}
|
|
100
195
|
};
|
|
101
196
|
return new Promise((resolve, reject) => {
|
|
197
|
+
const stderrLines = [];
|
|
198
|
+
/**
|
|
199
|
+
* Format the last `n` sanitized stderr lines as a diagnostic appendix.
|
|
200
|
+
* Returns an empty string when no lines have been collected.
|
|
201
|
+
*/
|
|
202
|
+
const stderrTail = (n = 15) => {
|
|
203
|
+
if (stderrLines.length === 0) return "";
|
|
204
|
+
const tail = stderrLines.slice(-n).map(sanitizeCloudflaredOutput).join("");
|
|
205
|
+
return `\ncloudflared 출력 (마지막 ${Math.min(n, stderrLines.length)}줄):\n${tail}`;
|
|
206
|
+
};
|
|
102
207
|
const timer = setTimeout(() => {
|
|
103
208
|
cleanup();
|
|
104
209
|
stop();
|
|
105
|
-
reject(/* @__PURE__ */ new Error(`[@ait-co/devtools] cloudflared did not report a tunnel URL within ${URL_TIMEOUT_MS / 1e3}s. Check your network connection, or run \`cloudflared tunnel --url http://localhost:${port}\` manually
|
|
210
|
+
reject(/* @__PURE__ */ new Error(`[@ait-co/devtools] cloudflared did not report a tunnel URL within ${URL_TIMEOUT_MS / 1e3}s. Check your network connection, or run \`cloudflared tunnel --url http://localhost:${port}\` manually.${stderrTail()}`));
|
|
106
211
|
}, URL_TIMEOUT_MS);
|
|
107
212
|
const onUrl = (line) => {
|
|
108
213
|
const found = parseTrycloudflareUrl(line);
|
|
@@ -114,13 +219,18 @@ async function startQuickTunnel(port) {
|
|
|
114
219
|
stop
|
|
115
220
|
});
|
|
116
221
|
};
|
|
222
|
+
const pushStderr = (line) => {
|
|
223
|
+
stderrLines.push(line);
|
|
224
|
+
};
|
|
117
225
|
const cleanup = () => {
|
|
118
226
|
tunnel.off("stdout", onUrl);
|
|
119
227
|
tunnel.off("stderr", onUrl);
|
|
228
|
+
tunnel.off("stderr", pushStderr);
|
|
120
229
|
};
|
|
121
230
|
tunnel.once("url", onUrl);
|
|
122
231
|
tunnel.on("stdout", onUrl);
|
|
123
232
|
tunnel.on("stderr", onUrl);
|
|
233
|
+
tunnel.on("stderr", pushStderr);
|
|
124
234
|
tunnel.once("error", (err) => {
|
|
125
235
|
clearTimeout(timer);
|
|
126
236
|
cleanup();
|
|
@@ -131,11 +241,11 @@ async function startQuickTunnel(port) {
|
|
|
131
241
|
if (stopped) return;
|
|
132
242
|
clearTimeout(timer);
|
|
133
243
|
cleanup();
|
|
134
|
-
reject(/* @__PURE__ */ new Error(`[@ait-co/devtools] cloudflared exited (code ${code ?? "null"}) before reporting a tunnel URL
|
|
244
|
+
reject(/* @__PURE__ */ new Error(`[@ait-co/devtools] cloudflared exited (code ${code ?? "null"}) before reporting a tunnel URL.${stderrTail()}`));
|
|
135
245
|
});
|
|
136
246
|
});
|
|
137
247
|
}
|
|
138
248
|
//#endregion
|
|
139
|
-
export { buildLauncherDeepLink, parseTrycloudflareUrl, printTunnelBanner, startQuickTunnel };
|
|
249
|
+
export { buildLauncherDeepLink, parseTrycloudflareUrl, printTunnelBanner, sanitizeCloudflaredOutput, startQuickTunnel, startTunnelDashboard };
|
|
140
250
|
|
|
141
251
|
//# sourceMappingURL=tunnel.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tunnel.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';\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\nconst LAUNCHER_URL = 'https://devtools.aitc.dev/launcher/';\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 `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 */\nexport function buildLauncherDeepLink(tunnelUrl: string, relayWssUrl?: string): string {\n const base = `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}`;\n if (!relayWssUrl) return base;\n return `${base}&debug=1&relay=${encodeURIComponent(relayWssUrl)}`;\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, opts.relayWssUrl);\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\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\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 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.`,\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 const cleanup = () => {\n tunnel.off('stdout', onUrl);\n tunnel.off('stderr', onUrl);\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 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.`,\n ),\n );\n });\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAiBA,MAAM,mBAAmB;;;;;;AAOzB,SAAgB,sBAAsB,MAA6B;CACjE,MAAM,IAAI,KAAK,MAAM,iBAAiB;AACtC,QAAO,IAAI,EAAE,KAAK;;AAiBpB,MAAM,eAAe;;;;;;;;;;;;;;AAerB,SAAgB,sBAAsB,WAAmB,aAA8B;CACrF,MAAM,OAAO,GAAG,aAAa,OAAO,mBAAmB,UAAU;AACjE,KAAI,CAAC,YAAa,QAAO;AACzB,QAAO,GAAG,KAAK,iBAAiB,mBAAmB,YAAY;;;;;;;;AASjE,eAAsB,kBACpB,KACA,OAAiC,EAAE,EACpB;CACf,MAAM,MAAM,KAAK,SAAS,MAAc,QAAQ,IAAI,EAAE;CACtD,MAAM,WAAW,sBAAsB,KAAK,KAAK,YAAY;AAoB7D,KAnBwB;EACtB;EACA;EACA,QAAQ;EACR;EACA,wCAAwC;EACxC;EACA;EACA,GAAI,KAAK,cACL,CACE,+DACA,uDACD,GACD,EAAE;EACN;EACA;EACA;EACA;EACD,CACS,KAAK,KAAK,CAAC;AAErB,KAAI,KAAK,OAAO,OAAO;EAGrB,MAAM,UAAU,MAAM,OAAO,oBAAoB;AACjD,QAAM,IAAI,SAAe,YAAY;AACnC,UAAO,SAAS,UAAU,EAAE,OAAO,MAAM,GAAG,QAAQ;AAClD,QAAI,IAAI;AACR,aAAS;KACT;IACF;;;AAWN,MAAM,iBAAiB;;;;;;;AAQvB,eAAsB,iBAAiB,MAAoC;CAEzE,MAAM,EAAE,KAAK,SAAS,WADF,MAAM,OAAO;AAGjC,KAAI,CAAC,WAAW,IAAI,EAAE;AACpB,QAAM,MAAM,QAAQ,IAAI,EAAE,EAAE,WAAW,MAAM,CAAC;AAC9C,QAAM,QAAQ,IAAI;;CAGpB,MAAM,SAAS,OAAO,MAAM,oBAAoB,OAAO;CACvD,IAAI,UAAU;CACd,MAAM,aAAa;AACjB,MAAI,QAAS;AACb,YAAU;AACV,MAAI;AACF,UAAO,MAAM;UACP;;AAKV,QAAO,IAAI,SAAsB,SAAS,WAAW;EACnD,MAAM,QAAQ,iBAAiB;AAC7B,YAAS;AACT,SAAM;AACN,0BACE,IAAI,MACF,qEACE,iBAAiB,IAClB,uFAAuF,KAAK,cAC9F,CACF;KACA,eAAe;EAElB,MAAM,SAAS,SAAiB;GAC9B,MAAM,QAAQ,sBAAsB,KAAK;AACzC,OAAI,CAAC,MAAO;AACZ,gBAAa,MAAM;AAEnB,YAAS;AACT,WAAQ;IAAE,KAAK;IAAO;IAAM,CAAC;;EAG/B,MAAM,gBAAgB;AACpB,UAAO,IAAI,UAAU,MAAM;AAC3B,UAAO,IAAI,UAAU,MAAM;;AAK7B,SAAO,KAAK,OAAO,MAAM;AACzB,SAAO,GAAG,UAAU,MAAM;AAC1B,SAAO,GAAG,UAAU,MAAM;AAC1B,SAAO,KAAK,UAAU,QAAe;AACnC,gBAAa,MAAM;AACnB,YAAS;AACT,SAAM;AACN,UAAO,IAAI;IACX;AACF,SAAO,KAAK,SAAS,SAAwB;AAC3C,OAAI,QAAS;AACb,gBAAa,MAAM;AACnB,YAAS;AACT,0BACE,IAAI,MACF,+CAA+C,QAAQ,OAAO,kCAC/D,CACF;IACD;GACF"}
|
|
1
|
+
{"version":3,"file":"tunnel.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';\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\nconst LAUNCHER_URL = 'https://devtools.aitc.dev/launcher/';\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 `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 */\nexport function buildLauncherDeepLink(tunnelUrl: string, relayWssUrl?: string): string {\n const base = `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}`;\n if (!relayWssUrl) return base;\n return `${base}&debug=1&relay=${encodeURIComponent(relayWssUrl)}`;\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, opts.relayWssUrl);\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/**\n * Heuristic: can this process open a GUI browser? Mirrors `canOpenBrowser` in\n * `src/mcp/tools.ts` but is re-declared here (not imported) so the tunnel path\n * does not statically pull the heavy MCP `tools.ts` module graph into the lazy\n * `import('./tunnel.js')` chunk. Kept in sync with the MCP copy.\n *\n * - macOS / Windows → assume yes (env-2 dev normally runs on the user's Mac).\n * - Linux → require `DISPLAY` or `WAYLAND_DISPLAY`.\n * - CI (`CI=true`/`CI=1`) → no.\n */\nfunction canOpenBrowser(): boolean {\n if (process.env.CI === 'true' || process.env.CI === '1') return false;\n const platform = process.platform;\n if (platform === 'darwin' || platform === 'win32') return true;\n if (platform === 'linux') {\n return Boolean(process.env.DISPLAY ?? process.env.WAYLAND_DISPLAY);\n }\n return false;\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\n/**\n * Env-2 UX parity with env 3/4 (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) that the MCP `build_attach_url` 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 * Every part the install-graph invariant depends on (`qrcode`, the MCP HTTP\n * server, the opener) is reached only through dynamic `import()` here, inside\n * the already-lazy `tunnel.js` chunk — nothing is added to the common build\n * graph or the MCP-only install graph.\n *\n * TOTP encapsulation: the dashboard's `getDashboardState` closure mints a FRESH\n * TOTP `at=` code on every call via `generateTotp(secret, Date.now())` and folds\n * it into a fresh `buildLauncherAttachUrl(...)`. Because the QR is re-rendered on\n * each SSE push / page reload from this closure, the code a phone scans is always\n * within its 30 s window — no stale code is baked into static HTML.\n *\n * SECRET-HANDLING: the tunnel host, relay wssUrl, TOTP code, and `.ait_relay`\n * value/path are NEVER written to stdout/stderr/logs here. They live only inside\n * the attach URL (HTML body + `/qr.png` query, per qr-http-server's invariant).\n * The only thing opened/logged is `http://127.0.0.1:<port>` (local, safe).\n *\n * @returns the dashboard handle when it started (caller wires `close()` into the\n * tunnel cleanup), or `undefined` when skipped (no relay, `qr:false`, headless,\n * opt-out, or a start failure) — in which case ASCII QR fallback stands alone.\n */\nexport async function startTunnelDashboard(\n opts: StartTunnelDashboardOptions,\n): Promise<TunnelDashboard | undefined> {\n const log = opts.log ?? ((m: string) => console.log(m));\n\n // Gate: dashboard is a CDP-only UX (needs a relay to attach to).\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 // GUI + AIT_AUTO_DEVTOOLS gate. Reuse the MCP opener's opt-out predicate so\n // the env-2 path honours the same `AIT_AUTO_DEVTOOLS=0` switch as env 3/4.\n const { isAutoDevtoolsDisabled } = await import('../mcp/devtools-opener.js');\n const gateOpen = opts.shouldOpen ?? (() => !isAutoDevtoolsDisabled() && canOpenBrowser());\n if (!gateOpen()) return undefined;\n\n const { startQrHttpServer } = await import('../mcp/qr-http-server.js');\n const { buildLauncherAttachUrl } = await import('../mcp/deeplink.js');\n const { generateTotp } = await import('../mcp/totp.js');\n\n // getDashboardState — mints a fresh TOTP + attach URL on every call so the QR\n // the dashboard renders (on load and on each SSE push) is never expired.\n // SECRET-HANDLING: the secret is read from env AT CALL TIME (it was injected\n // by ensureRelaySecret in the same CDP block) and is used only to compute the\n // at= code folded into attachUrl. tunnel.up is always true here — the relay\n // tunnel is already up by the time this runs.\n const getDashboardState = () => {\n const secret = process.env.AIT_DEBUG_TOTP_SECRET;\n const totpCode = secret ? generateTotp(secret, Date.now()) : undefined;\n const attachUrl = buildLauncherAttachUrl(opts.tunnelUrl, opts.relayWssUrl, totpCode);\n // pages: null — env 2(unplugin)는 데몬이 아니라 vite 플러그인 안이라\n // startChiiRelay 핸들이 connected target을 노출하지 않는다. 라이브 page 목록을\n // 알 수 없으므로 거짓 빈 목록 대신 \"연결된 Pages\" 섹션 자체를 숨긴다(#411).\n // env 3/4(debug-server.ts)는 router.active.listTargets()로 실제 목록을 채운다.\n return { tunnel: { up: true, wssUrl: opts.relayWssUrl }, pages: null, attachUrl };\n };\n\n let server: Awaited<ReturnType<typeof startQrHttpServer>>;\n try {\n server = await startQrHttpServer(getDashboardState);\n } catch {\n // SECRET-HANDLING: do not surface the error (could embed paths/hosts). The\n // ASCII QR printed by printTunnelBanner stays as the fallback.\n return undefined;\n }\n\n const dashboardUrl = `http://127.0.0.1:${server.port}`;\n\n const { openUrlInBrowser } = await import('../mcp/devtools-opener.js');\n const opened = openUrlInBrowser(dashboardUrl);\n // SECRET-HANDLING: only the local 127.0.0.1 URL is logged — never the tunnel\n // host, relay wssUrl, or TOTP code.\n log(\n opened\n ? ` │ Opened a QR dashboard in your browser: ${dashboardUrl}`\n : ` │ Open this QR dashboard in your browser: ${dashboardUrl}`,\n );\n\n return {\n url: dashboardUrl,\n close: () => server.close(),\n };\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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ait-co/devtools",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.59",
|
|
4
4
|
"description": "Development tools for Apps in Toss mini-apps — mock SDK, floating devtools panel, and universal bundler plugin",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -69,13 +69,18 @@
|
|
|
69
69
|
"@changesets/cli": "^2.31.0",
|
|
70
70
|
"@khmyznikov/pwa-install": "^0.6.3",
|
|
71
71
|
"@playwright/test": "^1.59.1",
|
|
72
|
+
"@testing-library/dom": "^10.4.1",
|
|
73
|
+
"@testing-library/react": "^16.3.0",
|
|
72
74
|
"@types/qrcode": "^1.5.6",
|
|
73
75
|
"@types/qrcode-terminal": "^0.12.2",
|
|
74
76
|
"@types/react": "^19.2.14",
|
|
77
|
+
"@types/react-dom": "^19.2.3",
|
|
75
78
|
"@types/ws": "^8.18.0",
|
|
79
|
+
"@vitejs/plugin-react": "^5.1.0",
|
|
76
80
|
"jsdom": "^29.1.1",
|
|
77
81
|
"qr-scanner": "^1.4.2",
|
|
78
|
-
"react": "^19.2.
|
|
82
|
+
"react": "^19.2.6",
|
|
83
|
+
"react-dom": "^19.2.6",
|
|
79
84
|
"satori": "^0.26.0",
|
|
80
85
|
"sharp": "^0.34.5",
|
|
81
86
|
"tsdown": "^0.21.7",
|
|
@@ -110,7 +115,10 @@
|
|
|
110
115
|
"url": "https://github.com/apps-in-toss-community/devtools/issues"
|
|
111
116
|
},
|
|
112
117
|
"scripts": {
|
|
113
|
-
"build": "
|
|
118
|
+
"build:dashboard-html": "tsx --tsconfig scripts/tsconfig.json scripts/build-dashboard-html.ts",
|
|
119
|
+
"build": "pnpm build:dashboard-html && tsdown",
|
|
120
|
+
"check:mcp-react-free": "bash scripts/check-mcp-react-free.sh",
|
|
121
|
+
"check:dashboard-html-fresh": "pnpm build:dashboard-html && git diff --exit-code src/mcp/dashboard.generated.ts",
|
|
114
122
|
"dev": "tsdown --watch",
|
|
115
123
|
"typecheck": "tsc --noEmit && tsc --noEmit -p e2e/fixture/tsconfig.json && tsc --noEmit -p scripts/tsconfig.json",
|
|
116
124
|
"test": "vitest run",
|
package/dist/totp-CQFmgOhM.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tunnel-CI61NvPI.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';\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\nconst LAUNCHER_URL = 'https://devtools.aitc.dev/launcher/';\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 `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 */\nexport function buildLauncherDeepLink(tunnelUrl: string, relayWssUrl?: string): string {\n const base = `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}`;\n if (!relayWssUrl) return base;\n return `${base}&debug=1&relay=${encodeURIComponent(relayWssUrl)}`;\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, opts.relayWssUrl);\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\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\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 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.`,\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 const cleanup = () => {\n tunnel.off('stdout', onUrl);\n tunnel.off('stderr', onUrl);\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 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.`,\n ),\n );\n });\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAiBA,MAAM,mBAAmB;;;;;;AAOzB,SAAgB,sBAAsB,MAA6B;CACjE,MAAM,IAAI,KAAK,MAAM,iBAAiB;AACtC,QAAO,IAAI,EAAE,KAAK;;AAiBpB,MAAM,eAAe;;;;;;;;;;;;;;AAerB,SAAgB,sBAAsB,WAAmB,aAA8B;CACrF,MAAM,OAAO,GAAG,aAAa,OAAO,mBAAmB,UAAU;AACjE,KAAI,CAAC,YAAa,QAAO;AACzB,QAAO,GAAG,KAAK,iBAAiB,mBAAmB,YAAY;;;;;;;;AASjE,eAAsB,kBACpB,KACA,OAAiC,EAAE,EACpB;CACf,MAAM,MAAM,KAAK,SAAS,MAAc,QAAQ,IAAI,EAAE;CACtD,MAAM,WAAW,sBAAsB,KAAK,KAAK,YAAY;AAoB7D,KAnBwB;EACtB;EACA;EACA,QAAQ;EACR;EACA,wCAAwC;EACxC;EACA;EACA,GAAI,KAAK,cACL,CACE,+DACA,uDACD,GACD,EAAE;EACN;EACA;EACA;EACA;EACD,CACS,KAAK,KAAK,CAAC;AAErB,KAAI,KAAK,OAAO,OAAO;EAGrB,MAAM,UAAU,MAAM,OAAO,oBAAoB;AACjD,QAAM,IAAI,SAAe,YAAY;AACnC,UAAO,SAAS,UAAU,EAAE,OAAO,MAAM,GAAG,QAAQ;AAClD,QAAI,IAAI;AACR,aAAS;KACT;IACF;;;AAWN,MAAM,iBAAiB;;;;;;;AAQvB,eAAsB,iBAAiB,MAAoC;CAEzE,MAAM,EAAE,KAAK,SAAS,WADF,MAAM,OAAO;AAGjC,KAAI,CAAC,WAAW,IAAI,EAAE;AACpB,QAAM,MAAM,QAAQ,IAAI,EAAE,EAAE,WAAW,MAAM,CAAC;AAC9C,QAAM,QAAQ,IAAI;;CAGpB,MAAM,SAAS,OAAO,MAAM,oBAAoB,OAAO;CACvD,IAAI,UAAU;CACd,MAAM,aAAa;AACjB,MAAI,QAAS;AACb,YAAU;AACV,MAAI;AACF,UAAO,MAAM;UACP;;AAKV,QAAO,IAAI,SAAsB,SAAS,WAAW;EACnD,MAAM,QAAQ,iBAAiB;AAC7B,YAAS;AACT,SAAM;AACN,0BACE,IAAI,MACF,qEACE,iBAAiB,IAClB,uFAAuF,KAAK,cAC9F,CACF;KACA,eAAe;EAElB,MAAM,SAAS,SAAiB;GAC9B,MAAM,QAAQ,sBAAsB,KAAK;AACzC,OAAI,CAAC,MAAO;AACZ,gBAAa,MAAM;AAEnB,YAAS;AACT,WAAQ;IAAE,KAAK;IAAO;IAAM,CAAC;;EAG/B,MAAM,gBAAgB;AACpB,UAAO,IAAI,UAAU,MAAM;AAC3B,UAAO,IAAI,UAAU,MAAM;;AAK7B,SAAO,KAAK,OAAO,MAAM;AACzB,SAAO,GAAG,UAAU,MAAM;AAC1B,SAAO,GAAG,UAAU,MAAM;AAC1B,SAAO,KAAK,UAAU,QAAe;AACnC,gBAAa,MAAM;AACnB,YAAS;AACT,SAAM;AACN,UAAO,IAAI;IACX;AACF,SAAO,KAAK,SAAS,SAAwB;AAC3C,OAAI,QAAS;AACb,gBAAa,MAAM;AACnB,YAAS;AACT,0BACE,IAAI,MACF,+CAA+C,QAAQ,OAAO,kCAC/D,CACF;IACD;GACF"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tunnel-nKYPtc-g.cjs","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';\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\nconst LAUNCHER_URL = 'https://devtools.aitc.dev/launcher/';\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 `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 */\nexport function buildLauncherDeepLink(tunnelUrl: string, relayWssUrl?: string): string {\n const base = `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}`;\n if (!relayWssUrl) return base;\n return `${base}&debug=1&relay=${encodeURIComponent(relayWssUrl)}`;\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, opts.relayWssUrl);\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\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\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 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.`,\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 const cleanup = () => {\n tunnel.off('stdout', onUrl);\n tunnel.off('stderr', onUrl);\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 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.`,\n ),\n );\n });\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAiBA,MAAM,mBAAmB;;;;;;AAOzB,SAAgB,sBAAsB,MAA6B;CACjE,MAAM,IAAI,KAAK,MAAM,iBAAiB;AACtC,QAAO,IAAI,EAAE,KAAK;;AAiBpB,MAAM,eAAe;;;;;;;;;;;;;;AAerB,SAAgB,sBAAsB,WAAmB,aAA8B;CACrF,MAAM,OAAO,GAAG,aAAa,OAAO,mBAAmB,UAAU;AACjE,KAAI,CAAC,YAAa,QAAO;AACzB,QAAO,GAAG,KAAK,iBAAiB,mBAAmB,YAAY;;;;;;;;AASjE,eAAsB,kBACpB,KACA,OAAiC,EAAE,EACpB;CACf,MAAM,MAAM,KAAK,SAAS,MAAc,QAAQ,IAAI,EAAE;CACtD,MAAM,WAAW,sBAAsB,KAAK,KAAK,YAAY;AAoB7D,KAnBwB;EACtB;EACA;EACA,QAAQ;EACR;EACA,wCAAwC;EACxC;EACA;EACA,GAAI,KAAK,cACL,CACE,+DACA,uDACD,GACD,EAAE;EACN;EACA;EACA;EACA;EACD,CACS,KAAK,KAAK,CAAC;AAErB,KAAI,KAAK,OAAO,OAAO;EAGrB,MAAM,UAAU,MAAM,OAAO,oBAAoB;AACjD,QAAM,IAAI,SAAe,YAAY;AACnC,UAAO,SAAS,UAAU,EAAE,OAAO,MAAM,GAAG,QAAQ;AAClD,QAAI,IAAI;AACR,aAAS;KACT;IACF;;;AAWN,MAAM,iBAAiB;;;;;;;AAQvB,eAAsB,iBAAiB,MAAoC;CAEzE,MAAM,EAAE,KAAK,SAAS,WADF,MAAM,OAAO;AAGjC,KAAI,EAAA,GAAA,QAAA,YAAY,IAAI,EAAE;AACpB,SAAA,GAAA,iBAAA,QAAA,GAAA,UAAA,SAAoB,IAAI,EAAE,EAAE,WAAW,MAAM,CAAC;AAC9C,QAAM,QAAQ,IAAI;;CAGpB,MAAM,SAAS,OAAO,MAAM,oBAAoB,OAAO;CACvD,IAAI,UAAU;CACd,MAAM,aAAa;AACjB,MAAI,QAAS;AACb,YAAU;AACV,MAAI;AACF,UAAO,MAAM;UACP;;AAKV,QAAO,IAAI,SAAsB,SAAS,WAAW;EACnD,MAAM,QAAQ,iBAAiB;AAC7B,YAAS;AACT,SAAM;AACN,0BACE,IAAI,MACF,qEACE,iBAAiB,IAClB,uFAAuF,KAAK,cAC9F,CACF;KACA,eAAe;EAElB,MAAM,SAAS,SAAiB;GAC9B,MAAM,QAAQ,sBAAsB,KAAK;AACzC,OAAI,CAAC,MAAO;AACZ,gBAAa,MAAM;AAEnB,YAAS;AACT,WAAQ;IAAE,KAAK;IAAO;IAAM,CAAC;;EAG/B,MAAM,gBAAgB;AACpB,UAAO,IAAI,UAAU,MAAM;AAC3B,UAAO,IAAI,UAAU,MAAM;;AAK7B,SAAO,KAAK,OAAO,MAAM;AACzB,SAAO,GAAG,UAAU,MAAM;AAC1B,SAAO,GAAG,UAAU,MAAM;AAC1B,SAAO,KAAK,UAAU,QAAe;AACnC,gBAAa,MAAM;AACnB,YAAS;AACT,SAAM;AACN,UAAO,IAAI;IACX;AACF,SAAO,KAAK,SAAS,SAAwB;AAC3C,OAAI,QAAS;AACb,gBAAa,MAAM;AACnB,YAAS;AACT,0BACE,IAAI,MACF,+CAA+C,QAAQ,OAAO,kCAC/D,CACF;IACD;GACF"}
|