@ait-co/devtools 0.1.20 → 0.1.21
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/README.en.md +7 -3
- package/README.md +7 -3
- package/dist/panel/index.js +2 -2
- package/dist/{tunnel-BbcgVy4L.js → tunnel-CY1velpk.js} +20 -7
- package/dist/tunnel-CY1velpk.js.map +1 -0
- package/dist/{tunnel-DeXfLGRl.cjs → tunnel-DgiECOnW.cjs} +20 -7
- package/dist/tunnel-DgiECOnW.cjs.map +1 -0
- package/dist/unplugin/index.cjs +1 -1
- package/dist/unplugin/index.js +1 -1
- package/dist/unplugin/tunnel.cjs +20 -6
- package/dist/unplugin/tunnel.cjs.map +1 -1
- package/dist/unplugin/tunnel.d.cts +13 -4
- package/dist/unplugin/tunnel.d.cts.map +1 -1
- package/dist/unplugin/tunnel.d.ts +13 -4
- package/dist/unplugin/tunnel.d.ts.map +1 -1
- package/dist/unplugin/tunnel.js +20 -7
- package/dist/unplugin/tunnel.js.map +1 -1
- package/package.json +1 -1
- package/dist/tunnel-BbcgVy4L.js.map +0 -1
- package/dist/tunnel-DeXfLGRl.cjs.map +0 -1
package/README.en.md
CHANGED
|
@@ -268,15 +268,19 @@ export default defineConfig({
|
|
|
268
268
|
}
|
|
269
269
|
```
|
|
270
270
|
|
|
271
|
-
### 2. Per-phone setup
|
|
271
|
+
### 2. Per-phone setup (required)
|
|
272
272
|
|
|
273
273
|
Open `https://devtools.aitc.dev/launcher/` on your phone and **add it to your home screen** (iOS Safari: Share → Add to Home Screen; Android Chrome: Install app). The launcher URL never changes, so this is a one-time step.
|
|
274
274
|
|
|
275
|
+
The launcher **only works when launched as an installed PWA from the home screen**. Opening it in a regular browser tab shows only the install hint — the URL input and scanner are hidden. The chrome-less standalone display is the whole point of the launcher shell, and a regular tab can't provide that.
|
|
276
|
+
|
|
275
277
|
### 3. Each session
|
|
276
278
|
|
|
277
279
|
1. Run `pnpm dev:phone` on your desktop (or `AIT_TUNNEL=1 pnpm dev` if you skipped step 1-(c)). The terminal will print a `https://*.trycloudflare.com` URL along with an ASCII QR code.
|
|
278
|
-
2.
|
|
279
|
-
3. Next session, just scan the new
|
|
280
|
+
2. Scan the QR code with your phone's camera (or with the "Scan QR" button inside the launcher). The QR encodes a `https://devtools.aitc.dev/launcher/?url=<tunnel>` deep-link, so the launcher PWA opens and auto-enters the day's dev app full-screen — no paste step required.
|
|
281
|
+
3. Next session, just scan the new QR. The launcher remembers the last URL and you can swap it any time with the "Rescan" button.
|
|
282
|
+
|
|
283
|
+
> Whether the OS camera routes the QR straight into the installed launcher PWA (instead of a regular browser tab) is most reliable on Android Chrome; iOS Safari versions may fall back to a normal tab. In that case, open the launcher from its home-screen icon and use its in-page "Scan QR" button.
|
|
280
284
|
|
|
281
285
|
### Background
|
|
282
286
|
|
package/README.md
CHANGED
|
@@ -268,15 +268,19 @@ export default defineConfig({
|
|
|
268
268
|
}
|
|
269
269
|
```
|
|
270
270
|
|
|
271
|
-
### 2. 폰당 1회 셋업
|
|
271
|
+
### 2. 폰당 1회 셋업 (필수)
|
|
272
272
|
|
|
273
273
|
폰에서 `https://devtools.aitc.dev/launcher/`를 열고 **홈 화면에 추가**합니다 (iOS Safari "공유 → 홈 화면에 추가", Android Chrome "앱 설치"). launcher 자체는 URL이 바뀌지 않으니 한 번만 하면 됩니다.
|
|
274
274
|
|
|
275
|
+
launcher는 **PWA(홈 화면 앱)로 실행할 때만 동작**합니다. 일반 브라우저 탭에서 열면 설치 안내만 노출되고 입력/스캐너 UI는 숨겨집니다 — 크롬리스 standalone 디스플레이가 PWA 셸의 본질이라, 일반 탭에서의 동작은 의도적으로 막아둡니다.
|
|
276
|
+
|
|
275
277
|
### 3. 매 세션
|
|
276
278
|
|
|
277
279
|
1. 데스크톱에서 `pnpm dev:phone`을 실행합니다 (1-(c) 스크립트를 추가하지 않았다면 `AIT_TUNNEL=1 pnpm dev`). 터미널에 `https://*.trycloudflare.com` URL + ASCII QR이 출력됩니다.
|
|
278
|
-
2. 폰의 launcher 아이콘
|
|
279
|
-
3. 다음 세션엔 새
|
|
280
|
+
2. 폰의 카메라(또는 launcher 아이콘 안의 "Scan QR")로 QR을 스캔합니다. QR은 `https://devtools.aitc.dev/launcher/?url=<tunnel>` 딥링크라 launcher PWA가 자동으로 열리고 그날의 dev 앱이 풀스크린으로 뜹니다 — URL 붙여넣기 단계가 필요 없습니다.
|
|
281
|
+
3. 다음 세션엔 새 QR을 스캔만 하면 됩니다. launcher는 마지막 URL을 기억하고, "Rescan" 버튼으로 언제든 교체할 수 있습니다.
|
|
282
|
+
|
|
283
|
+
> QR을 일반 카메라 앱으로 찍었을 때 Safari/Chrome이 일반 탭이 아닌 설치된 launcher PWA로 곧장 라우팅하는 동작은 Android Chrome에서 가장 안정적이고, iOS Safari는 버전에 따라 일반 탭으로 폴백할 수 있습니다. 그 경우 launcher 홈 화면 아이콘에서 한 번 열어주면 그 안의 QR 스캐너로 다시 시도할 수 있습니다.
|
|
280
284
|
|
|
281
285
|
### 배경
|
|
282
286
|
|
package/dist/panel/index.js
CHANGED
|
@@ -1050,7 +1050,7 @@ function readGlobalString(key) {
|
|
|
1050
1050
|
}
|
|
1051
1051
|
const TELEMETRY_ENDPOINT = readGlobalString("__TELEMETRY_ENDPOINT__") ?? "https://t.aitc.dev";
|
|
1052
1052
|
function getVersion() {
|
|
1053
|
-
return "0.1.
|
|
1053
|
+
return "0.1.21";
|
|
1054
1054
|
}
|
|
1055
1055
|
let panelVisibleSince = null;
|
|
1056
1056
|
let accumulatedMs = 0;
|
|
@@ -4167,7 +4167,7 @@ function mount() {
|
|
|
4167
4167
|
mockBadge.textContent = aitState.state.panelEditable ? t("panel.editMode.on") : t("panel.editMode.off");
|
|
4168
4168
|
refreshPanel();
|
|
4169
4169
|
});
|
|
4170
|
-
const headerRight = h("span", { style: "display:flex;align-items:center;gap:6px" }, mockBadge, h("span", { style: "font-size:11px;color:#666;font-weight:400" }, `v0.1.
|
|
4170
|
+
const headerRight = h("span", { style: "display:flex;align-items:center;gap:6px" }, mockBadge, h("span", { style: "font-size:11px;color:#666;font-weight:400" }, `v0.1.21`), closeBtn);
|
|
4171
4171
|
const header = h("div", { className: "ait-panel-header" }, h("span", {}, t("panel.title")), headerRight);
|
|
4172
4172
|
tabsEl = h("div", { className: "ait-panel-tabs" });
|
|
4173
4173
|
for (const tab of getTabs()) {
|
|
@@ -26,20 +26,33 @@ function parseTrycloudflareUrl(line) {
|
|
|
26
26
|
}
|
|
27
27
|
const LAUNCHER_URL = "https://devtools.aitc.dev/launcher/";
|
|
28
28
|
/**
|
|
29
|
+
* Build the deep-link URL that QR codes encode: when the launcher PWA is
|
|
30
|
+
* already on the phone's home screen, scanning this opens it directly into the
|
|
31
|
+
* live view for `tunnelUrl` (the launcher consumes `?url=` and clears it).
|
|
32
|
+
* Plain-text raw URL is no longer enough — the launcher gates its setup UI to
|
|
33
|
+
* the installed PWA, so a raw tunnel URL opened in a normal browser tab would
|
|
34
|
+
* land on a "please install" screen.
|
|
35
|
+
*/
|
|
36
|
+
function buildLauncherDeepLink(tunnelUrl) {
|
|
37
|
+
return `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
29
40
|
* Print the terminal banner announcing the live tunnel: the public URL, an ASCII
|
|
30
|
-
* QR encoding
|
|
31
|
-
* unauthenticated and not for production. Pure w.r.t. side effects
|
|
32
|
-
* the injected `log` sink and `qrcode-terminal` — unit-tested.
|
|
41
|
+
* QR encoding a launcher deep-link, and a one-line note that quick tunnels are
|
|
42
|
+
* ephemeral, unauthenticated and not for production. Pure w.r.t. side effects
|
|
43
|
+
* other than the injected `log` sink and `qrcode-terminal` — unit-tested.
|
|
33
44
|
*/
|
|
34
45
|
async function printTunnelBanner(url, opts = {}) {
|
|
35
46
|
const log = opts.log ?? ((m) => console.log(m));
|
|
47
|
+
const deepLink = buildLauncherDeepLink(url);
|
|
36
48
|
log([
|
|
37
49
|
"",
|
|
38
50
|
" ┌─ @ait-co/devtools · live tunnel ────────────────────────────",
|
|
39
51
|
` │ ${url}`,
|
|
40
52
|
" │",
|
|
41
|
-
` │
|
|
42
|
-
" │
|
|
53
|
+
` │ Install the launcher PWA once: ${LAUNCHER_URL}`,
|
|
54
|
+
" │ Then scan the QR below — it opens the launcher directly",
|
|
55
|
+
" │ into this tunnel URL (no manual paste needed).",
|
|
43
56
|
" │ Quick tunnels are unauthenticated, change every run, and are",
|
|
44
57
|
" │ not for production use.",
|
|
45
58
|
" └──────────────────────────────────────────────────────────────",
|
|
@@ -48,7 +61,7 @@ async function printTunnelBanner(url, opts = {}) {
|
|
|
48
61
|
if (opts.qr !== false) {
|
|
49
62
|
const qrcode = (await import("qrcode-terminal")).default;
|
|
50
63
|
await new Promise((resolve) => {
|
|
51
|
-
qrcode.generate(
|
|
64
|
+
qrcode.generate(deepLink, { small: true }, (out) => {
|
|
52
65
|
log(out);
|
|
53
66
|
resolve();
|
|
54
67
|
});
|
|
@@ -111,4 +124,4 @@ async function startQuickTunnel(port) {
|
|
|
111
124
|
//#endregion
|
|
112
125
|
export { printTunnelBanner, startQuickTunnel };
|
|
113
126
|
|
|
114
|
-
//# sourceMappingURL=tunnel-
|
|
127
|
+
//# sourceMappingURL=tunnel-CY1velpk.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tunnel-CY1velpk.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\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 */\nexport function buildLauncherDeepLink(tunnelUrl: string): string {\n return `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}`;\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 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 ' │ 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 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 tunnel.off('stdout', onUrl);\n tunnel.off('stderr', onUrl);\n resolve({ url: found, stop });\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 stop();\n reject(err);\n });\n tunnel.once('exit', (code: number | null) => {\n if (stopped) return;\n clearTimeout(timer);\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;;AAUpB,MAAM,eAAe;;;;;;;;;AAUrB,SAAgB,sBAAsB,WAA2B;AAC/D,QAAO,GAAG,aAAa,OAAO,mBAAmB,UAAU;;;;;;;;AAS7D,eAAsB,kBACpB,KACA,OAAiC,EAAE,EACpB;CACf,MAAM,MAAM,KAAK,SAAS,MAAc,QAAQ,IAAI,EAAE;CACtD,MAAM,WAAW,sBAAsB,IAAI;AAc3C,KAbwB;EACtB;EACA;EACA,QAAQ;EACR;EACA,wCAAwC;EACxC;EACA;EACA;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,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,UAAO,IAAI,UAAU,MAAM;AAC3B,UAAO,IAAI,UAAU,MAAM;AAC3B,WAAQ;IAAE,KAAK;IAAO;IAAM,CAAC;;AAK/B,SAAO,KAAK,OAAO,MAAM;AACzB,SAAO,GAAG,UAAU,MAAM;AAC1B,SAAO,GAAG,UAAU,MAAM;AAC1B,SAAO,KAAK,UAAU,QAAe;AACnC,gBAAa,MAAM;AACnB,SAAM;AACN,UAAO,IAAI;IACX;AACF,SAAO,KAAK,SAAS,SAAwB;AAC3C,OAAI,QAAS;AACb,gBAAa,MAAM;AACnB,0BACE,IAAI,MACF,+CAA+C,QAAQ,OAAO,kCAC/D,CACF;IACD;GACF"}
|
|
@@ -26,20 +26,33 @@ function parseTrycloudflareUrl(line) {
|
|
|
26
26
|
}
|
|
27
27
|
const LAUNCHER_URL = "https://devtools.aitc.dev/launcher/";
|
|
28
28
|
/**
|
|
29
|
+
* Build the deep-link URL that QR codes encode: when the launcher PWA is
|
|
30
|
+
* already on the phone's home screen, scanning this opens it directly into the
|
|
31
|
+
* live view for `tunnelUrl` (the launcher consumes `?url=` and clears it).
|
|
32
|
+
* Plain-text raw URL is no longer enough — the launcher gates its setup UI to
|
|
33
|
+
* the installed PWA, so a raw tunnel URL opened in a normal browser tab would
|
|
34
|
+
* land on a "please install" screen.
|
|
35
|
+
*/
|
|
36
|
+
function buildLauncherDeepLink(tunnelUrl) {
|
|
37
|
+
return `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
29
40
|
* Print the terminal banner announcing the live tunnel: the public URL, an ASCII
|
|
30
|
-
* QR encoding
|
|
31
|
-
* unauthenticated and not for production. Pure w.r.t. side effects
|
|
32
|
-
* the injected `log` sink and `qrcode-terminal` — unit-tested.
|
|
41
|
+
* QR encoding a launcher deep-link, and a one-line note that quick tunnels are
|
|
42
|
+
* ephemeral, unauthenticated and not for production. Pure w.r.t. side effects
|
|
43
|
+
* other than the injected `log` sink and `qrcode-terminal` — unit-tested.
|
|
33
44
|
*/
|
|
34
45
|
async function printTunnelBanner(url, opts = {}) {
|
|
35
46
|
const log = opts.log ?? ((m) => console.log(m));
|
|
47
|
+
const deepLink = buildLauncherDeepLink(url);
|
|
36
48
|
log([
|
|
37
49
|
"",
|
|
38
50
|
" ┌─ @ait-co/devtools · live tunnel ────────────────────────────",
|
|
39
51
|
` │ ${url}`,
|
|
40
52
|
" │",
|
|
41
|
-
` │
|
|
42
|
-
" │
|
|
53
|
+
` │ Install the launcher PWA once: ${LAUNCHER_URL}`,
|
|
54
|
+
" │ Then scan the QR below — it opens the launcher directly",
|
|
55
|
+
" │ into this tunnel URL (no manual paste needed).",
|
|
43
56
|
" │ Quick tunnels are unauthenticated, change every run, and are",
|
|
44
57
|
" │ not for production use.",
|
|
45
58
|
" └──────────────────────────────────────────────────────────────",
|
|
@@ -48,7 +61,7 @@ async function printTunnelBanner(url, opts = {}) {
|
|
|
48
61
|
if (opts.qr !== false) {
|
|
49
62
|
const qrcode = (await import("qrcode-terminal")).default;
|
|
50
63
|
await new Promise((resolve) => {
|
|
51
|
-
qrcode.generate(
|
|
64
|
+
qrcode.generate(deepLink, { small: true }, (out) => {
|
|
52
65
|
log(out);
|
|
53
66
|
resolve();
|
|
54
67
|
});
|
|
@@ -112,4 +125,4 @@ async function startQuickTunnel(port) {
|
|
|
112
125
|
exports.printTunnelBanner = printTunnelBanner;
|
|
113
126
|
exports.startQuickTunnel = startQuickTunnel;
|
|
114
127
|
|
|
115
|
-
//# sourceMappingURL=tunnel-
|
|
128
|
+
//# sourceMappingURL=tunnel-DgiECOnW.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tunnel-DgiECOnW.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\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 */\nexport function buildLauncherDeepLink(tunnelUrl: string): string {\n return `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}`;\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 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 ' │ 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 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 tunnel.off('stdout', onUrl);\n tunnel.off('stderr', onUrl);\n resolve({ url: found, stop });\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 stop();\n reject(err);\n });\n tunnel.once('exit', (code: number | null) => {\n if (stopped) return;\n clearTimeout(timer);\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;;AAUpB,MAAM,eAAe;;;;;;;;;AAUrB,SAAgB,sBAAsB,WAA2B;AAC/D,QAAO,GAAG,aAAa,OAAO,mBAAmB,UAAU;;;;;;;;AAS7D,eAAsB,kBACpB,KACA,OAAiC,EAAE,EACpB;CACf,MAAM,MAAM,KAAK,SAAS,MAAc,QAAQ,IAAI,EAAE;CACtD,MAAM,WAAW,sBAAsB,IAAI;AAc3C,KAbwB;EACtB;EACA;EACA,QAAQ;EACR;EACA,wCAAwC;EACxC;EACA;EACA;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,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,UAAO,IAAI,UAAU,MAAM;AAC3B,UAAO,IAAI,UAAU,MAAM;AAC3B,WAAQ;IAAE,KAAK;IAAO;IAAM,CAAC;;AAK/B,SAAO,KAAK,OAAO,MAAM;AACzB,SAAO,GAAG,UAAU,MAAM;AAC1B,SAAO,GAAG,UAAU,MAAM;AAC1B,SAAO,KAAK,UAAU,QAAe;AACnC,gBAAa,MAAM;AACnB,SAAM;AACN,UAAO,IAAI;IACX;AACF,SAAO,KAAK,SAAS,SAAwB;AAC3C,OAAI,QAAS;AACb,gBAAa,MAAM;AACnB,0BACE,IAAI,MACF,+CAA+C,QAAQ,OAAO,kCAC/D,CACF;IACD;GACF"}
|
package/dist/unplugin/index.cjs
CHANGED
|
@@ -89,7 +89,7 @@ const aitDevtoolsPlugin = (0, unplugin.createUnplugin)((options) => {
|
|
|
89
89
|
console.warn("[@ait-co/devtools] tunnel: could not determine the dev server port; skipping.");
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
92
|
-
Promise.resolve().then(() => require("../tunnel-
|
|
92
|
+
Promise.resolve().then(() => require("../tunnel-DgiECOnW.cjs")).then(async ({ startQuickTunnel, printTunnelBanner }) => {
|
|
93
93
|
const t = await startQuickTunnel(port);
|
|
94
94
|
tunnel = t;
|
|
95
95
|
await printTunnelBanner(t.url, { qr: tunnelConfig.qr });
|
package/dist/unplugin/index.js
CHANGED
|
@@ -85,7 +85,7 @@ const aitDevtoolsPlugin = createUnplugin((options) => {
|
|
|
85
85
|
console.warn("[@ait-co/devtools] tunnel: could not determine the dev server port; skipping.");
|
|
86
86
|
return;
|
|
87
87
|
}
|
|
88
|
-
import("../tunnel-
|
|
88
|
+
import("../tunnel-CY1velpk.js").then(async ({ startQuickTunnel, printTunnelBanner }) => {
|
|
89
89
|
const t = await startQuickTunnel(port);
|
|
90
90
|
tunnel = t;
|
|
91
91
|
await printTunnelBanner(t.url, { qr: tunnelConfig.qr });
|
package/dist/unplugin/tunnel.cjs
CHANGED
|
@@ -27,20 +27,33 @@ function parseTrycloudflareUrl(line) {
|
|
|
27
27
|
}
|
|
28
28
|
const LAUNCHER_URL = "https://devtools.aitc.dev/launcher/";
|
|
29
29
|
/**
|
|
30
|
+
* Build the deep-link URL that QR codes encode: when the launcher PWA is
|
|
31
|
+
* already on the phone's home screen, scanning this opens it directly into the
|
|
32
|
+
* live view for `tunnelUrl` (the launcher consumes `?url=` and clears it).
|
|
33
|
+
* Plain-text raw URL is no longer enough — the launcher gates its setup UI to
|
|
34
|
+
* the installed PWA, so a raw tunnel URL opened in a normal browser tab would
|
|
35
|
+
* land on a "please install" screen.
|
|
36
|
+
*/
|
|
37
|
+
function buildLauncherDeepLink(tunnelUrl) {
|
|
38
|
+
return `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}`;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
30
41
|
* Print the terminal banner announcing the live tunnel: the public URL, an ASCII
|
|
31
|
-
* QR encoding
|
|
32
|
-
* unauthenticated and not for production. Pure w.r.t. side effects
|
|
33
|
-
* the injected `log` sink and `qrcode-terminal` — unit-tested.
|
|
42
|
+
* QR encoding a launcher deep-link, and a one-line note that quick tunnels are
|
|
43
|
+
* ephemeral, unauthenticated and not for production. Pure w.r.t. side effects
|
|
44
|
+
* other than the injected `log` sink and `qrcode-terminal` — unit-tested.
|
|
34
45
|
*/
|
|
35
46
|
async function printTunnelBanner(url, opts = {}) {
|
|
36
47
|
const log = opts.log ?? ((m) => console.log(m));
|
|
48
|
+
const deepLink = buildLauncherDeepLink(url);
|
|
37
49
|
log([
|
|
38
50
|
"",
|
|
39
51
|
" ┌─ @ait-co/devtools · live tunnel ────────────────────────────",
|
|
40
52
|
` │ ${url}`,
|
|
41
53
|
" │",
|
|
42
|
-
` │
|
|
43
|
-
" │
|
|
54
|
+
` │ Install the launcher PWA once: ${LAUNCHER_URL}`,
|
|
55
|
+
" │ Then scan the QR below — it opens the launcher directly",
|
|
56
|
+
" │ into this tunnel URL (no manual paste needed).",
|
|
44
57
|
" │ Quick tunnels are unauthenticated, change every run, and are",
|
|
45
58
|
" │ not for production use.",
|
|
46
59
|
" └──────────────────────────────────────────────────────────────",
|
|
@@ -49,7 +62,7 @@ async function printTunnelBanner(url, opts = {}) {
|
|
|
49
62
|
if (opts.qr !== false) {
|
|
50
63
|
const qrcode = (await import("qrcode-terminal")).default;
|
|
51
64
|
await new Promise((resolve) => {
|
|
52
|
-
qrcode.generate(
|
|
65
|
+
qrcode.generate(deepLink, { small: true }, (out) => {
|
|
53
66
|
log(out);
|
|
54
67
|
resolve();
|
|
55
68
|
});
|
|
@@ -110,6 +123,7 @@ async function startQuickTunnel(port) {
|
|
|
110
123
|
});
|
|
111
124
|
}
|
|
112
125
|
//#endregion
|
|
126
|
+
exports.buildLauncherDeepLink = buildLauncherDeepLink;
|
|
113
127
|
exports.parseTrycloudflareUrl = parseTrycloudflareUrl;
|
|
114
128
|
exports.printTunnelBanner = printTunnelBanner;
|
|
115
129
|
exports.startQuickTunnel = startQuickTunnel;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tunnel.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\nconst LAUNCHER_URL = 'https://devtools.aitc.dev/launcher/';\n\n/**\n * Print the terminal banner announcing the live tunnel: the public URL, an ASCII\n * QR encoding
|
|
1
|
+
{"version":3,"file":"tunnel.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\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 */\nexport function buildLauncherDeepLink(tunnelUrl: string): string {\n return `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}`;\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 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 ' │ 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 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 tunnel.off('stdout', onUrl);\n tunnel.off('stderr', onUrl);\n resolve({ url: found, stop });\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 stop();\n reject(err);\n });\n tunnel.once('exit', (code: number | null) => {\n if (stopped) return;\n clearTimeout(timer);\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;;AAUpB,MAAM,eAAe;;;;;;;;;AAUrB,SAAgB,sBAAsB,WAA2B;AAC/D,QAAO,GAAG,aAAa,OAAO,mBAAmB,UAAU;;;;;;;;AAS7D,eAAsB,kBACpB,KACA,OAAiC,EAAE,EACpB;CACf,MAAM,MAAM,KAAK,SAAS,MAAc,QAAQ,IAAI,EAAE;CACtD,MAAM,WAAW,sBAAsB,IAAI;AAc3C,KAbwB;EACtB;EACA;EACA,QAAQ;EACR;EACA,wCAAwC;EACxC;EACA;EACA;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,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,UAAO,IAAI,UAAU,MAAM;AAC3B,UAAO,IAAI,UAAU,MAAM;AAC3B,WAAQ;IAAE,KAAK;IAAO;IAAM,CAAC;;AAK/B,SAAO,KAAK,OAAO,MAAM;AACzB,SAAO,GAAG,UAAU,MAAM;AAC1B,SAAO,GAAG,UAAU,MAAM;AAC1B,SAAO,KAAK,UAAU,QAAe;AACnC,gBAAa,MAAM;AACnB,SAAM;AACN,UAAO,IAAI;IACX;AACF,SAAO,KAAK,SAAS,SAAwB;AAC3C,OAAI,QAAS;AACb,gBAAa,MAAM;AACnB,0BACE,IAAI,MACF,+CAA+C,QAAQ,OAAO,kCAC/D,CACF;IACD;GACF"}
|
|
@@ -22,11 +22,20 @@ interface PrintTunnelBannerOptions {
|
|
|
22
22
|
/** Sink for the banner text (default: `console.log`). Injected for testing. */
|
|
23
23
|
log?: (msg: string) => void;
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Build the deep-link URL that QR codes encode: when the launcher PWA is
|
|
27
|
+
* already on the phone's home screen, scanning this opens it directly into the
|
|
28
|
+
* live view for `tunnelUrl` (the launcher consumes `?url=` and clears it).
|
|
29
|
+
* Plain-text raw URL is no longer enough — the launcher gates its setup UI to
|
|
30
|
+
* the installed PWA, so a raw tunnel URL opened in a normal browser tab would
|
|
31
|
+
* land on a "please install" screen.
|
|
32
|
+
*/
|
|
33
|
+
declare function buildLauncherDeepLink(tunnelUrl: string): string;
|
|
25
34
|
/**
|
|
26
35
|
* Print the terminal banner announcing the live tunnel: the public URL, an ASCII
|
|
27
|
-
* QR encoding
|
|
28
|
-
* unauthenticated and not for production. Pure w.r.t. side effects
|
|
29
|
-
* the injected `log` sink and `qrcode-terminal` — unit-tested.
|
|
36
|
+
* QR encoding a launcher deep-link, and a one-line note that quick tunnels are
|
|
37
|
+
* ephemeral, unauthenticated and not for production. Pure w.r.t. side effects
|
|
38
|
+
* other than the injected `log` sink and `qrcode-terminal` — unit-tested.
|
|
30
39
|
*/
|
|
31
40
|
declare function printTunnelBanner(url: string, opts?: PrintTunnelBannerOptions): Promise<void>;
|
|
32
41
|
interface QuickTunnel {
|
|
@@ -43,5 +52,5 @@ interface QuickTunnel {
|
|
|
43
52
|
*/
|
|
44
53
|
declare function startQuickTunnel(port: number): Promise<QuickTunnel>;
|
|
45
54
|
//#endregion
|
|
46
|
-
export { PrintTunnelBannerOptions, QuickTunnel, parseTrycloudflareUrl, printTunnelBanner, startQuickTunnel };
|
|
55
|
+
export { PrintTunnelBannerOptions, QuickTunnel, buildLauncherDeepLink, parseTrycloudflareUrl, printTunnelBanner, startQuickTunnel };
|
|
47
56
|
//# sourceMappingURL=tunnel.d.cts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tunnel.d.cts","names":[],"sources":["../../src/unplugin/tunnel.ts"],"mappings":";;AAwBA;;;;;AAKA;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"tunnel.d.cts","names":[],"sources":["../../src/unplugin/tunnel.ts"],"mappings":";;AAwBA;;;;;AAKA;;;;;;;;;AAiBA;iBAtBgB,qBAAA,CAAsB,IAAA;AAAA,UAKrB,wBAAA;EAiBqB;EAfpC,EAAA;EAyBoB;EAvBpB,GAAA,IAAO,GAAA;AAAA;;;;;;;;AAyDT;iBA5CgB,qBAAA,CAAsB,SAAA;;;;AA2DtC;;;iBAjDsB,iBAAA,CACpB,GAAA,UACA,IAAA,GAAM,wBAAA,GACL,OAAA;AAAA,UA+Bc,WAAA;EAeqC;EAbpD,GAAA;EAauE;EAXvE,IAAA;AAAA;;;;;;;iBAWoB,gBAAA,CAAiB,IAAA,WAAe,OAAA,CAAQ,WAAA"}
|
|
@@ -22,11 +22,20 @@ interface PrintTunnelBannerOptions {
|
|
|
22
22
|
/** Sink for the banner text (default: `console.log`). Injected for testing. */
|
|
23
23
|
log?: (msg: string) => void;
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Build the deep-link URL that QR codes encode: when the launcher PWA is
|
|
27
|
+
* already on the phone's home screen, scanning this opens it directly into the
|
|
28
|
+
* live view for `tunnelUrl` (the launcher consumes `?url=` and clears it).
|
|
29
|
+
* Plain-text raw URL is no longer enough — the launcher gates its setup UI to
|
|
30
|
+
* the installed PWA, so a raw tunnel URL opened in a normal browser tab would
|
|
31
|
+
* land on a "please install" screen.
|
|
32
|
+
*/
|
|
33
|
+
declare function buildLauncherDeepLink(tunnelUrl: string): string;
|
|
25
34
|
/**
|
|
26
35
|
* Print the terminal banner announcing the live tunnel: the public URL, an ASCII
|
|
27
|
-
* QR encoding
|
|
28
|
-
* unauthenticated and not for production. Pure w.r.t. side effects
|
|
29
|
-
* the injected `log` sink and `qrcode-terminal` — unit-tested.
|
|
36
|
+
* QR encoding a launcher deep-link, and a one-line note that quick tunnels are
|
|
37
|
+
* ephemeral, unauthenticated and not for production. Pure w.r.t. side effects
|
|
38
|
+
* other than the injected `log` sink and `qrcode-terminal` — unit-tested.
|
|
30
39
|
*/
|
|
31
40
|
declare function printTunnelBanner(url: string, opts?: PrintTunnelBannerOptions): Promise<void>;
|
|
32
41
|
interface QuickTunnel {
|
|
@@ -43,5 +52,5 @@ interface QuickTunnel {
|
|
|
43
52
|
*/
|
|
44
53
|
declare function startQuickTunnel(port: number): Promise<QuickTunnel>;
|
|
45
54
|
//#endregion
|
|
46
|
-
export { PrintTunnelBannerOptions, QuickTunnel, parseTrycloudflareUrl, printTunnelBanner, startQuickTunnel };
|
|
55
|
+
export { PrintTunnelBannerOptions, QuickTunnel, buildLauncherDeepLink, parseTrycloudflareUrl, printTunnelBanner, startQuickTunnel };
|
|
47
56
|
//# sourceMappingURL=tunnel.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tunnel.d.ts","names":[],"sources":["../../src/unplugin/tunnel.ts"],"mappings":";;AAwBA;;;;;AAKA;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"tunnel.d.ts","names":[],"sources":["../../src/unplugin/tunnel.ts"],"mappings":";;AAwBA;;;;;AAKA;;;;;;;;;AAiBA;iBAtBgB,qBAAA,CAAsB,IAAA;AAAA,UAKrB,wBAAA;EAiBqB;EAfpC,EAAA;EAyBoB;EAvBpB,GAAA,IAAO,GAAA;AAAA;;;;;;;;AAyDT;iBA5CgB,qBAAA,CAAsB,SAAA;;;;AA2DtC;;;iBAjDsB,iBAAA,CACpB,GAAA,UACA,IAAA,GAAM,wBAAA,GACL,OAAA;AAAA,UA+Bc,WAAA;EAeqC;EAbpD,GAAA;EAauE;EAXvE,IAAA;AAAA;;;;;;;iBAWoB,gBAAA,CAAiB,IAAA,WAAe,OAAA,CAAQ,WAAA"}
|
package/dist/unplugin/tunnel.js
CHANGED
|
@@ -26,20 +26,33 @@ function parseTrycloudflareUrl(line) {
|
|
|
26
26
|
}
|
|
27
27
|
const LAUNCHER_URL = "https://devtools.aitc.dev/launcher/";
|
|
28
28
|
/**
|
|
29
|
+
* Build the deep-link URL that QR codes encode: when the launcher PWA is
|
|
30
|
+
* already on the phone's home screen, scanning this opens it directly into the
|
|
31
|
+
* live view for `tunnelUrl` (the launcher consumes `?url=` and clears it).
|
|
32
|
+
* Plain-text raw URL is no longer enough — the launcher gates its setup UI to
|
|
33
|
+
* the installed PWA, so a raw tunnel URL opened in a normal browser tab would
|
|
34
|
+
* land on a "please install" screen.
|
|
35
|
+
*/
|
|
36
|
+
function buildLauncherDeepLink(tunnelUrl) {
|
|
37
|
+
return `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
29
40
|
* Print the terminal banner announcing the live tunnel: the public URL, an ASCII
|
|
30
|
-
* QR encoding
|
|
31
|
-
* unauthenticated and not for production. Pure w.r.t. side effects
|
|
32
|
-
* the injected `log` sink and `qrcode-terminal` — unit-tested.
|
|
41
|
+
* QR encoding a launcher deep-link, and a one-line note that quick tunnels are
|
|
42
|
+
* ephemeral, unauthenticated and not for production. Pure w.r.t. side effects
|
|
43
|
+
* other than the injected `log` sink and `qrcode-terminal` — unit-tested.
|
|
33
44
|
*/
|
|
34
45
|
async function printTunnelBanner(url, opts = {}) {
|
|
35
46
|
const log = opts.log ?? ((m) => console.log(m));
|
|
47
|
+
const deepLink = buildLauncherDeepLink(url);
|
|
36
48
|
log([
|
|
37
49
|
"",
|
|
38
50
|
" ┌─ @ait-co/devtools · live tunnel ────────────────────────────",
|
|
39
51
|
` │ ${url}`,
|
|
40
52
|
" │",
|
|
41
|
-
` │
|
|
42
|
-
" │
|
|
53
|
+
` │ Install the launcher PWA once: ${LAUNCHER_URL}`,
|
|
54
|
+
" │ Then scan the QR below — it opens the launcher directly",
|
|
55
|
+
" │ into this tunnel URL (no manual paste needed).",
|
|
43
56
|
" │ Quick tunnels are unauthenticated, change every run, and are",
|
|
44
57
|
" │ not for production use.",
|
|
45
58
|
" └──────────────────────────────────────────────────────────────",
|
|
@@ -48,7 +61,7 @@ async function printTunnelBanner(url, opts = {}) {
|
|
|
48
61
|
if (opts.qr !== false) {
|
|
49
62
|
const qrcode = (await import("qrcode-terminal")).default;
|
|
50
63
|
await new Promise((resolve) => {
|
|
51
|
-
qrcode.generate(
|
|
64
|
+
qrcode.generate(deepLink, { small: true }, (out) => {
|
|
52
65
|
log(out);
|
|
53
66
|
resolve();
|
|
54
67
|
});
|
|
@@ -109,6 +122,6 @@ async function startQuickTunnel(port) {
|
|
|
109
122
|
});
|
|
110
123
|
}
|
|
111
124
|
//#endregion
|
|
112
|
-
export { parseTrycloudflareUrl, printTunnelBanner, startQuickTunnel };
|
|
125
|
+
export { buildLauncherDeepLink, parseTrycloudflareUrl, printTunnelBanner, startQuickTunnel };
|
|
113
126
|
|
|
114
127
|
//# 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\nconst LAUNCHER_URL = 'https://devtools.aitc.dev/launcher/';\n\n/**\n * Print the terminal banner announcing the live tunnel: the public URL, an ASCII\n * QR encoding
|
|
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\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 */\nexport function buildLauncherDeepLink(tunnelUrl: string): string {\n return `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}`;\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 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 ' │ 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 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 tunnel.off('stdout', onUrl);\n tunnel.off('stderr', onUrl);\n resolve({ url: found, stop });\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 stop();\n reject(err);\n });\n tunnel.once('exit', (code: number | null) => {\n if (stopped) return;\n clearTimeout(timer);\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;;AAUpB,MAAM,eAAe;;;;;;;;;AAUrB,SAAgB,sBAAsB,WAA2B;AAC/D,QAAO,GAAG,aAAa,OAAO,mBAAmB,UAAU;;;;;;;;AAS7D,eAAsB,kBACpB,KACA,OAAiC,EAAE,EACpB;CACf,MAAM,MAAM,KAAK,SAAS,MAAc,QAAQ,IAAI,EAAE;CACtD,MAAM,WAAW,sBAAsB,IAAI;AAc3C,KAbwB;EACtB;EACA;EACA,QAAQ;EACR;EACA,wCAAwC;EACxC;EACA;EACA;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,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,UAAO,IAAI,UAAU,MAAM;AAC3B,UAAO,IAAI,UAAU,MAAM;AAC3B,WAAQ;IAAE,KAAK;IAAO;IAAM,CAAC;;AAK/B,SAAO,KAAK,OAAO,MAAM;AACzB,SAAO,GAAG,UAAU,MAAM;AAC1B,SAAO,GAAG,UAAU,MAAM;AAC1B,SAAO,KAAK,UAAU,QAAe;AACnC,gBAAa,MAAM;AACnB,SAAM;AACN,UAAO,IAAI;IACX;AACF,SAAO,KAAK,SAAS,SAAwB;AAC3C,OAAI,QAAS;AACb,gBAAa,MAAM;AACnB,0BACE,IAAI,MACF,+CAA+C,QAAQ,OAAO,kCAC/D,CACF;IACD;GACF"}
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tunnel-BbcgVy4L.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\nconst LAUNCHER_URL = 'https://devtools.aitc.dev/launcher/';\n\n/**\n * Print the terminal banner announcing the live tunnel: the public URL, an ASCII\n * QR encoding it, and a one-line note that quick tunnels are ephemeral,\n * unauthenticated and not for production. Pure w.r.t. side effects other than\n * 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 lines: string[] = [\n '',\n ' ┌─ @ait-co/devtools · live tunnel ────────────────────────────',\n ` │ ${url}`,\n ' │',\n ` │ Open the launcher on your phone: ${LAUNCHER_URL}`,\n ' │ Scan the QR below from there (or paste the URL).',\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(url, { 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 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 tunnel.off('stdout', onUrl);\n tunnel.off('stderr', onUrl);\n resolve({ url: found, stop });\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 stop();\n reject(err);\n });\n tunnel.once('exit', (code: number | null) => {\n if (stopped) return;\n clearTimeout(timer);\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;;AAUpB,MAAM,eAAe;;;;;;;AAQrB,eAAsB,kBACpB,KACA,OAAiC,EAAE,EACpB;CACf,MAAM,MAAM,KAAK,SAAS,MAAc,QAAQ,IAAI,EAAE;AAatD,KAZwB;EACtB;EACA;EACA,QAAQ;EACR;EACA,0CAA0C;EAC1C;EACA;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,KAAK,EAAE,OAAO,MAAM,GAAG,QAAQ;AAC7C,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,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,UAAO,IAAI,UAAU,MAAM;AAC3B,UAAO,IAAI,UAAU,MAAM;AAC3B,WAAQ;IAAE,KAAK;IAAO;IAAM,CAAC;;AAK/B,SAAO,KAAK,OAAO,MAAM;AACzB,SAAO,GAAG,UAAU,MAAM;AAC1B,SAAO,GAAG,UAAU,MAAM;AAC1B,SAAO,KAAK,UAAU,QAAe;AACnC,gBAAa,MAAM;AACnB,SAAM;AACN,UAAO,IAAI;IACX;AACF,SAAO,KAAK,SAAS,SAAwB;AAC3C,OAAI,QAAS;AACb,gBAAa,MAAM;AACnB,0BACE,IAAI,MACF,+CAA+C,QAAQ,OAAO,kCAC/D,CACF;IACD;GACF"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tunnel-DeXfLGRl.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\nconst LAUNCHER_URL = 'https://devtools.aitc.dev/launcher/';\n\n/**\n * Print the terminal banner announcing the live tunnel: the public URL, an ASCII\n * QR encoding it, and a one-line note that quick tunnels are ephemeral,\n * unauthenticated and not for production. Pure w.r.t. side effects other than\n * 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 lines: string[] = [\n '',\n ' ┌─ @ait-co/devtools · live tunnel ────────────────────────────',\n ` │ ${url}`,\n ' │',\n ` │ Open the launcher on your phone: ${LAUNCHER_URL}`,\n ' │ Scan the QR below from there (or paste the URL).',\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(url, { 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 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 tunnel.off('stdout', onUrl);\n tunnel.off('stderr', onUrl);\n resolve({ url: found, stop });\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 stop();\n reject(err);\n });\n tunnel.once('exit', (code: number | null) => {\n if (stopped) return;\n clearTimeout(timer);\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;;AAUpB,MAAM,eAAe;;;;;;;AAQrB,eAAsB,kBACpB,KACA,OAAiC,EAAE,EACpB;CACf,MAAM,MAAM,KAAK,SAAS,MAAc,QAAQ,IAAI,EAAE;AAatD,KAZwB;EACtB;EACA;EACA,QAAQ;EACR;EACA,0CAA0C;EAC1C;EACA;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,KAAK,EAAE,OAAO,MAAM,GAAG,QAAQ;AAC7C,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,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,UAAO,IAAI,UAAU,MAAM;AAC3B,UAAO,IAAI,UAAU,MAAM;AAC3B,WAAQ;IAAE,KAAK;IAAO;IAAM,CAAC;;AAK/B,SAAO,KAAK,OAAO,MAAM;AACzB,SAAO,GAAG,UAAU,MAAM;AAC1B,SAAO,GAAG,UAAU,MAAM;AAC1B,SAAO,KAAK,UAAU,QAAe;AACnC,gBAAa,MAAM;AACnB,SAAM;AACN,UAAO,IAAI;IACX;AACF,SAAO,KAAK,SAAS,SAAwB;AAC3C,OAAI,QAAS;AACb,gBAAa,MAAM;AACnB,0BACE,IAAI,MACF,+CAA+C,QAAQ,OAAO,kCAC/D,CACF;IACD;GACF"}
|