@apps-in-toss/devtools 3.0.2

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.
@@ -0,0 +1,140 @@
1
+ //#region src/unplugin/index.d.ts
2
+ /**
3
+ * @apps-in-toss/devtools unplugin
4
+ *
5
+ * 모든 주요 번들러를 지원하는 단일 플러그인.
6
+ * @apps-in-toss/web-framework → @apps-in-toss/devtools/mock 으로 alias 설정.
7
+ *
8
+ * Usage:
9
+ * import aitDevtools from '@apps-in-toss/devtools/unplugin';
10
+ *
11
+ * // Vite
12
+ * export default { plugins: [aitDevtools.vite()] };
13
+ *
14
+ * // Webpack / Next.js
15
+ * config.plugins.push(aitDevtools.webpack());
16
+ *
17
+ * // Rspack
18
+ * config.plugins.push(aitDevtools.rspack());
19
+ *
20
+ * // esbuild
21
+ * { plugins: [aitDevtools.esbuild()] }
22
+ *
23
+ * // Rollup
24
+ * { plugins: [aitDevtools.rollup()] }
25
+ */
26
+ type SdkVersionSelection = "auto" | "2" | "3";
27
+ /** Resolve the consumer project's installed SDK major without importing it. */
28
+ declare function detectInstalledSdkMajor(cwd?: string): "2" | "3" | null;
29
+ interface AitDevtoolsOptions {
30
+ /**
31
+ * 패널 자동 주입 여부 (default: true)
32
+ * true이면 진입점에 floating panel import를 자동 추가한다.
33
+ */
34
+ panel?: boolean;
35
+ /** web-framework facade selection. Auto-detects the consumer SDK by default. */
36
+ sdkVersion?: SdkVersionSelection;
37
+ /**
38
+ * mock alias 활성화 여부. default: true (development), false (production)
39
+ */
40
+ mock?: boolean;
41
+ /**
42
+ * Vite dev server에 MCP state endpoint를 추가할지 여부 (default: false).
43
+ *
44
+ * `true`로 설정하면:
45
+ * - GET /api/ait-devtools/state — 마지막으로 브라우저가 push한 mock state 스냅샷 반환
46
+ * - POST /api/ait-devtools/state — 브라우저 panel이 상태 변경 시 자동 push (panel 내부 처리)
47
+ *
48
+ * 이 endpoint는 이 패키지가 여는 producer 쪽이고, MCP 서버 등 외부 도구가
49
+ * GET으로 읽어 AI 에이전트에 mock state를 노출할 수 있다.
50
+ * Vite 전용: webpack/rspack/esbuild/rollup 환경에서는 무시된다.
51
+ */
52
+ mcp?: boolean;
53
+ /**
54
+ * 미니앱의 webViewType (`granite.config.ts`의 `webViewProps.type`)을 빌드 상수
55
+ * `__WEB_VIEW_TYPE__`로 주입한다 (#580). **Vite 전용** (다른 번들러는 무시).
56
+ *
57
+ * 미지정 시 `'partner'`(web-framework `webViewProps.type`의 `@default`)로 주입한다.
58
+ * `game`이면 게임 모드로 자동 진입한다. (granite.config.ts를 config 시점에
59
+ * 자동으로 읽는 것은 TS 모듈 로더가 필요해 보류 — 명시 옵션으로 신뢰성 확보, #580.)
60
+ */
61
+ webViewType?: "partner" | "game";
62
+ /**
63
+ * 미니앱의 `granite.config.ts` `navigationBar.transparentBackground` 값
64
+ * (SDK `@apps-in-toss/plugins@2.8.0` 신규 필드, #587). `true`이면 env-2 launcher
65
+ * deep-link에 `&navBarTransparent=1`을 주입해 launcher partner bar가 투명 배경으로
66
+ * 렌더된다. granite.config를 직접 읽지 않는다(version-agnostic, #580 원칙) —
67
+ * 소비자 vite.config가 `graniteConfig.navigationBar?.transparentBackground`를
68
+ * import해 이 옵션으로 넘긴다. 미지정 시 주입 안 함(URL 청정, back-compat).
69
+ */
70
+ navBarTransparent?: boolean;
71
+ /**
72
+ * 미니앱의 `granite.config.ts` `navigationBar.theme` 값
73
+ * (SDK `@apps-in-toss/plugins@2.8.0` 신규 필드, #587). `'light'` 또는 `'dark'`이면
74
+ * env-2 launcher deep-link에 `&navBarTheme=<v>`를 주입해 launcher partner bar가
75
+ * 해당 테마 글자/아이콘 색으로 렌더된다. granite.config를 직접 읽지 않는다
76
+ * (version-agnostic, #580 원칙). 미지정 시 주입 안 함(URL 청정, back-compat).
77
+ */
78
+ navBarTheme?: "light" | "dark";
79
+ /**
80
+ * Vite dev 서버를 Cloudflare quick tunnel(`*.trycloudflare.com`, 계정 불필요)로
81
+ * 외부 노출해 실제 폰에서 미리보기. **Vite dev 모드 전용** — production에서는
82
+ * 터널을 띄우지 않는다 (의도치 않은 노출 방지). 다른 번들러는
83
+ * 무시. `true`면 기본 동작, 객체로 세부 설정 가능.
84
+ */
85
+ tunnel?: boolean | {
86
+ /** 노출할 포트 (미지정 시 dev 서버가 실제 listen한 포트 자동 감지). */
87
+ port?: number;
88
+ /** 터미널 ASCII QR 출력 (default: true). */
89
+ qr?: boolean;
90
+ };
91
+ }
92
+ /**
93
+ * Resolves the effective tunnel option (#425).
94
+ *
95
+ * An explicit `tunnel` value (including `false`) always takes priority over
96
+ * env vars — the `??` operator means `undefined` (= omitted) falls through,
97
+ * but `false` / `true` / an object are preserved as-is (non-breaking).
98
+ *
99
+ * When the option is omitted:
100
+ * - `AIT_TUNNEL=1` enables the screen-preview tunnel.
101
+ * - Not set → `false` (disabled).
102
+ *
103
+ * Extracted as a pure function so it can be unit-tested without standing up
104
+ * a full Vite dev server.
105
+ *
106
+ * @param explicit - The `tunnel` option as passed by the consumer (or `undefined` when omitted).
107
+ * @param env - The process environment (injectable for testing).
108
+ */
109
+ declare function resolveTunnelOption(explicit: AitDevtoolsOptions["tunnel"], env: Record<string, string | undefined>): AitDevtoolsOptions["tunnel"];
110
+ declare const aitDevtoolsPlugin: import("unplugin").UnpluginInstance<AitDevtoolsOptions | undefined, boolean>;
111
+ /**
112
+ * 번들러 플러그인 팩토리의 반환 타입을 의도적으로 any로 풀어놓아요.
113
+ *
114
+ * unplugin의 `UnpluginInstance`를 그대로 노출하면 vite/webpack의 Plugin 타입이
115
+ * 공개 표면에 실리는데, 소비자 프로젝트와 이 패키지가 서로 다른 vite 타입
116
+ * 인스턴스를 보는 환경(pnpm·Yarn PnP 모노레포의 가상 패키지)에서는 같은 모양의
117
+ * Plugin끼리 구조 비교가 일어나 `ts(2321) Excessive stack depth`로 터져요.
118
+ * 플러그인 핸들은 plugins 배열에 꽂는 용도라 반환 타입을 잃어도 손해가 없어요.
119
+ */
120
+ type BundlerPluginFactory = (options?: AitDevtoolsOptions) => any;
121
+ interface AitDevtoolsUnplugin {
122
+ vite: BundlerPluginFactory;
123
+ webpack: BundlerPluginFactory;
124
+ rollup: BundlerPluginFactory;
125
+ esbuild: BundlerPluginFactory;
126
+ rspack: BundlerPluginFactory;
127
+ /**
128
+ * 저수준 unplugin 팩토리예요. 소비자 번들러 설정에서는 쓸 일이 없고,
129
+ * 이 패키지의 테스트가 훅을 직접 검사할 때 써요.
130
+ */
131
+ raw: typeof aitDevtoolsPlugin.raw;
132
+ }
133
+ declare const vite: BundlerPluginFactory;
134
+ declare const webpack: BundlerPluginFactory;
135
+ declare const rollup: BundlerPluginFactory;
136
+ declare const esbuild: BundlerPluginFactory;
137
+ declare const rspack: BundlerPluginFactory;
138
+ declare const aitDevtools: AitDevtoolsUnplugin;
139
+ //#endregion
140
+ export { AitDevtoolsOptions, SdkVersionSelection, aitDevtools as default, detectInstalledSdkMajor, esbuild, resolveTunnelOption, rollup, rspack, vite, webpack };
@@ -0,0 +1,140 @@
1
+ //#region src/unplugin/index.d.ts
2
+ /**
3
+ * @apps-in-toss/devtools unplugin
4
+ *
5
+ * 모든 주요 번들러를 지원하는 단일 플러그인.
6
+ * @apps-in-toss/web-framework → @apps-in-toss/devtools/mock 으로 alias 설정.
7
+ *
8
+ * Usage:
9
+ * import aitDevtools from '@apps-in-toss/devtools/unplugin';
10
+ *
11
+ * // Vite
12
+ * export default { plugins: [aitDevtools.vite()] };
13
+ *
14
+ * // Webpack / Next.js
15
+ * config.plugins.push(aitDevtools.webpack());
16
+ *
17
+ * // Rspack
18
+ * config.plugins.push(aitDevtools.rspack());
19
+ *
20
+ * // esbuild
21
+ * { plugins: [aitDevtools.esbuild()] }
22
+ *
23
+ * // Rollup
24
+ * { plugins: [aitDevtools.rollup()] }
25
+ */
26
+ type SdkVersionSelection = "auto" | "2" | "3";
27
+ /** Resolve the consumer project's installed SDK major without importing it. */
28
+ declare function detectInstalledSdkMajor(cwd?: string): "2" | "3" | null;
29
+ interface AitDevtoolsOptions {
30
+ /**
31
+ * 패널 자동 주입 여부 (default: true)
32
+ * true이면 진입점에 floating panel import를 자동 추가한다.
33
+ */
34
+ panel?: boolean;
35
+ /** web-framework facade selection. Auto-detects the consumer SDK by default. */
36
+ sdkVersion?: SdkVersionSelection;
37
+ /**
38
+ * mock alias 활성화 여부. default: true (development), false (production)
39
+ */
40
+ mock?: boolean;
41
+ /**
42
+ * Vite dev server에 MCP state endpoint를 추가할지 여부 (default: false).
43
+ *
44
+ * `true`로 설정하면:
45
+ * - GET /api/ait-devtools/state — 마지막으로 브라우저가 push한 mock state 스냅샷 반환
46
+ * - POST /api/ait-devtools/state — 브라우저 panel이 상태 변경 시 자동 push (panel 내부 처리)
47
+ *
48
+ * 이 endpoint는 이 패키지가 여는 producer 쪽이고, MCP 서버 등 외부 도구가
49
+ * GET으로 읽어 AI 에이전트에 mock state를 노출할 수 있다.
50
+ * Vite 전용: webpack/rspack/esbuild/rollup 환경에서는 무시된다.
51
+ */
52
+ mcp?: boolean;
53
+ /**
54
+ * 미니앱의 webViewType (`granite.config.ts`의 `webViewProps.type`)을 빌드 상수
55
+ * `__WEB_VIEW_TYPE__`로 주입한다 (#580). **Vite 전용** (다른 번들러는 무시).
56
+ *
57
+ * 미지정 시 `'partner'`(web-framework `webViewProps.type`의 `@default`)로 주입한다.
58
+ * `game`이면 게임 모드로 자동 진입한다. (granite.config.ts를 config 시점에
59
+ * 자동으로 읽는 것은 TS 모듈 로더가 필요해 보류 — 명시 옵션으로 신뢰성 확보, #580.)
60
+ */
61
+ webViewType?: "partner" | "game";
62
+ /**
63
+ * 미니앱의 `granite.config.ts` `navigationBar.transparentBackground` 값
64
+ * (SDK `@apps-in-toss/plugins@2.8.0` 신규 필드, #587). `true`이면 env-2 launcher
65
+ * deep-link에 `&navBarTransparent=1`을 주입해 launcher partner bar가 투명 배경으로
66
+ * 렌더된다. granite.config를 직접 읽지 않는다(version-agnostic, #580 원칙) —
67
+ * 소비자 vite.config가 `graniteConfig.navigationBar?.transparentBackground`를
68
+ * import해 이 옵션으로 넘긴다. 미지정 시 주입 안 함(URL 청정, back-compat).
69
+ */
70
+ navBarTransparent?: boolean;
71
+ /**
72
+ * 미니앱의 `granite.config.ts` `navigationBar.theme` 값
73
+ * (SDK `@apps-in-toss/plugins@2.8.0` 신규 필드, #587). `'light'` 또는 `'dark'`이면
74
+ * env-2 launcher deep-link에 `&navBarTheme=<v>`를 주입해 launcher partner bar가
75
+ * 해당 테마 글자/아이콘 색으로 렌더된다. granite.config를 직접 읽지 않는다
76
+ * (version-agnostic, #580 원칙). 미지정 시 주입 안 함(URL 청정, back-compat).
77
+ */
78
+ navBarTheme?: "light" | "dark";
79
+ /**
80
+ * Vite dev 서버를 Cloudflare quick tunnel(`*.trycloudflare.com`, 계정 불필요)로
81
+ * 외부 노출해 실제 폰에서 미리보기. **Vite dev 모드 전용** — production에서는
82
+ * 터널을 띄우지 않는다 (의도치 않은 노출 방지). 다른 번들러는
83
+ * 무시. `true`면 기본 동작, 객체로 세부 설정 가능.
84
+ */
85
+ tunnel?: boolean | {
86
+ /** 노출할 포트 (미지정 시 dev 서버가 실제 listen한 포트 자동 감지). */
87
+ port?: number;
88
+ /** 터미널 ASCII QR 출력 (default: true). */
89
+ qr?: boolean;
90
+ };
91
+ }
92
+ /**
93
+ * Resolves the effective tunnel option (#425).
94
+ *
95
+ * An explicit `tunnel` value (including `false`) always takes priority over
96
+ * env vars — the `??` operator means `undefined` (= omitted) falls through,
97
+ * but `false` / `true` / an object are preserved as-is (non-breaking).
98
+ *
99
+ * When the option is omitted:
100
+ * - `AIT_TUNNEL=1` enables the screen-preview tunnel.
101
+ * - Not set → `false` (disabled).
102
+ *
103
+ * Extracted as a pure function so it can be unit-tested without standing up
104
+ * a full Vite dev server.
105
+ *
106
+ * @param explicit - The `tunnel` option as passed by the consumer (or `undefined` when omitted).
107
+ * @param env - The process environment (injectable for testing).
108
+ */
109
+ declare function resolveTunnelOption(explicit: AitDevtoolsOptions["tunnel"], env: Record<string, string | undefined>): AitDevtoolsOptions["tunnel"];
110
+ declare const aitDevtoolsPlugin: import("unplugin").UnpluginInstance<AitDevtoolsOptions | undefined, boolean>;
111
+ /**
112
+ * 번들러 플러그인 팩토리의 반환 타입을 의도적으로 any로 풀어놓아요.
113
+ *
114
+ * unplugin의 `UnpluginInstance`를 그대로 노출하면 vite/webpack의 Plugin 타입이
115
+ * 공개 표면에 실리는데, 소비자 프로젝트와 이 패키지가 서로 다른 vite 타입
116
+ * 인스턴스를 보는 환경(pnpm·Yarn PnP 모노레포의 가상 패키지)에서는 같은 모양의
117
+ * Plugin끼리 구조 비교가 일어나 `ts(2321) Excessive stack depth`로 터져요.
118
+ * 플러그인 핸들은 plugins 배열에 꽂는 용도라 반환 타입을 잃어도 손해가 없어요.
119
+ */
120
+ type BundlerPluginFactory = (options?: AitDevtoolsOptions) => any;
121
+ interface AitDevtoolsUnplugin {
122
+ vite: BundlerPluginFactory;
123
+ webpack: BundlerPluginFactory;
124
+ rollup: BundlerPluginFactory;
125
+ esbuild: BundlerPluginFactory;
126
+ rspack: BundlerPluginFactory;
127
+ /**
128
+ * 저수준 unplugin 팩토리예요. 소비자 번들러 설정에서는 쓸 일이 없고,
129
+ * 이 패키지의 테스트가 훅을 직접 검사할 때 써요.
130
+ */
131
+ raw: typeof aitDevtoolsPlugin.raw;
132
+ }
133
+ declare const vite: BundlerPluginFactory;
134
+ declare const webpack: BundlerPluginFactory;
135
+ declare const rollup: BundlerPluginFactory;
136
+ declare const esbuild: BundlerPluginFactory;
137
+ declare const rspack: BundlerPluginFactory;
138
+ declare const aitDevtools: AitDevtoolsUnplugin;
139
+ //#endregion
140
+ export { AitDevtoolsOptions, SdkVersionSelection, aitDevtools as default, detectInstalledSdkMajor, esbuild, resolveTunnelOption, rollup, rspack, vite, webpack };
@@ -0,0 +1,301 @@
1
+ import { createRequire } from "node:module";
2
+ import { readFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { createUnplugin } from "unplugin";
5
+ //#region src/shared/parent-watcher.ts
6
+ /**
7
+ * Shared parent-PID watcher — used by both the MCP debug daemon and the
8
+ * unplugin tunnel path to self-terminate when the parent process (e.g. Claude
9
+ * Code, vite) has died or been reparented without sending SIGTERM/SIGHUP.
10
+ *
11
+ * Intentionally react-free and Node-stdlib-only so this module is safe to
12
+ * import from the MCP daemon bundle (`dist/mcp/cli.js`) without violating the
13
+ * install-graph invariant.
14
+ */
15
+ /**
16
+ * Returns `true` when the given PID refers to a running process.
17
+ *
18
+ * Uses `process.kill(pid, 0)` — a no-op signal that succeeds when the process
19
+ * exists and we have permission to signal it; throws ESRCH when it doesn't exist.
20
+ */
21
+ function isPidAlive(pid) {
22
+ try {
23
+ process.kill(pid, 0);
24
+ return true;
25
+ } catch (err) {
26
+ if (err.code === "EPERM") return true;
27
+ return false;
28
+ }
29
+ }
30
+ /**
31
+ * Starts a periodic watcher that detects when the parent process (e.g. Claude
32
+ * Code) has died without sending SIGTERM/SIGHUP, and calls `onOrphaned` so the
33
+ * daemon can self-terminate rather than running as a zombie.
34
+ *
35
+ * Mirrors the `startAttachWatcher` pattern: `setInterval`-based, returns
36
+ * `{ stop(): void }`, injectable deps for testability.
37
+ *
38
+ * @param onOrphaned - Called once when the parent is gone.
39
+ * @param opts.intervalMs - Poll interval in milliseconds (default 5 000).
40
+ * @param opts.initialPpid - Parent PID to watch (default `process.ppid`).
41
+ * @param opts.isAlive - Predicate to test if a PID is running (default `isPidAlive`).
42
+ * @param opts.getPpid - Supplier of current ppid (default `() => process.ppid`).
43
+ * Detects ppid changes as well as death.
44
+ * @param opts.log - Logger (default `process.stderr.write`).
45
+ *
46
+ * @returns `stop` — call during shutdown to clear the interval.
47
+ */
48
+ function startParentWatcher(onOrphaned, opts) {
49
+ const { intervalMs = 5e3, initialPpid = process.ppid, isAlive = isPidAlive, getPpid = () => process.ppid, log = (msg) => process.stderr.write(msg) } = opts ?? {};
50
+ if (initialPpid <= 1) {
51
+ log("[ait-debug] parent-pid watcher: no parent to watch (ppid<=1), skipping\n");
52
+ return { stop() {} };
53
+ }
54
+ let fired = false;
55
+ const handle = setInterval(() => {
56
+ if (fired) return;
57
+ const currentPpid = getPpid();
58
+ if (currentPpid !== initialPpid || !isAlive(initialPpid)) {
59
+ fired = true;
60
+ clearInterval(handle);
61
+ log(`[ait-debug] parent-pid watcher: parent PID ${initialPpid} is gone (currentPpid=${currentPpid}) — shutting down\n`);
62
+ onOrphaned();
63
+ }
64
+ }, intervalMs);
65
+ return { stop() {
66
+ clearInterval(handle);
67
+ } };
68
+ }
69
+ //#endregion
70
+ //#region src/unplugin/index.ts
71
+ /**
72
+ * @apps-in-toss/devtools unplugin
73
+ *
74
+ * 모든 주요 번들러를 지원하는 단일 플러그인.
75
+ * @apps-in-toss/web-framework → @apps-in-toss/devtools/mock 으로 alias 설정.
76
+ *
77
+ * Usage:
78
+ * import aitDevtools from '@apps-in-toss/devtools/unplugin';
79
+ *
80
+ * // Vite
81
+ * export default { plugins: [aitDevtools.vite()] };
82
+ *
83
+ * // Webpack / Next.js
84
+ * config.plugins.push(aitDevtools.webpack());
85
+ *
86
+ * // Rspack
87
+ * config.plugins.push(aitDevtools.rspack());
88
+ *
89
+ * // esbuild
90
+ * { plugins: [aitDevtools.esbuild()] }
91
+ *
92
+ * // Rollup
93
+ * { plugins: [aitDevtools.rollup()] }
94
+ */
95
+ /**
96
+ * Resolve `@apps-in-toss/devtools/mock` to its real file path at plugin-load time.
97
+ *
98
+ * Returning the bare specifier from `resolveId` would stop the bundler from
99
+ * walking node_modules for it — Vite 8+ treats such a non-null string as the
100
+ * final resolved id and serves it via the virtual `/@id/` prefix, which 404s
101
+ * because we don't provide a `load` hook. Resolving to an absolute path here
102
+ * lets every supported bundler load the file the normal way.
103
+ */
104
+ function resolveMockPath(specifier) {
105
+ try {
106
+ return createRequire(resolve(process.cwd(), "package.json")).resolve(specifier);
107
+ } catch {
108
+ return specifier;
109
+ }
110
+ }
111
+ const MOCK_PATH_2X = resolveMockPath("@apps-in-toss/devtools/mock/2x");
112
+ const MOCK_PATH_3X = resolveMockPath("@apps-in-toss/devtools/mock/3x");
113
+ /** Resolve the consumer project's installed SDK major without importing it. */
114
+ function detectInstalledSdkMajor(cwd = process.cwd()) {
115
+ try {
116
+ const packageJsonPath = createRequire(resolve(cwd, "package.json")).resolve(`${FRAMEWORK_ID}/package.json`);
117
+ const version = JSON.parse(readFileSync(packageJsonPath, "utf8")).version;
118
+ if (version?.startsWith("2.")) return "2";
119
+ if (version?.startsWith("3.")) return "3";
120
+ } catch {}
121
+ return null;
122
+ }
123
+ const FRAMEWORK_ID = "@apps-in-toss/web-framework";
124
+ const BRIDGE_ID = "@apps-in-toss/web-bridge";
125
+ const ANALYTICS_ID = "@apps-in-toss/web-analytics";
126
+ const WEBVIEW_BRIDGE_ID = "@apps-in-toss/webview-bridge";
127
+ /** MCP state endpoint path — browser panel POSTs here, MCP server GETs here */
128
+ const MCP_STATE_PATH = "/api/ait-devtools/state";
129
+ /** Browser runtime opt-in consumed by the panel's state-sync helper. */
130
+ const MCP_ENABLE_SNIPPET = "globalThis.__AIT_DEVTOOLS_MCP_ENABLED__ = true;";
131
+ /**
132
+ * Resolves the effective tunnel option (#425).
133
+ *
134
+ * An explicit `tunnel` value (including `false`) always takes priority over
135
+ * env vars — the `??` operator means `undefined` (= omitted) falls through,
136
+ * but `false` / `true` / an object are preserved as-is (non-breaking).
137
+ *
138
+ * When the option is omitted:
139
+ * - `AIT_TUNNEL=1` enables the screen-preview tunnel.
140
+ * - Not set → `false` (disabled).
141
+ *
142
+ * Extracted as a pure function so it can be unit-tested without standing up
143
+ * a full Vite dev server.
144
+ *
145
+ * @param explicit - The `tunnel` option as passed by the consumer (or `undefined` when omitted).
146
+ * @param env - The process environment (injectable for testing).
147
+ */
148
+ function resolveTunnelOption(explicit, env) {
149
+ return explicit ?? !!env.AIT_TUNNEL;
150
+ }
151
+ const aitDevtoolsPlugin = createUnplugin((options) => {
152
+ const isDev = process.env.NODE_ENV !== "production";
153
+ const shouldEnable = isDev;
154
+ const shouldMock = shouldEnable && (options?.mock ?? isDev);
155
+ const sdkMajor = options?.sdkVersion === "2" || options?.sdkVersion === "3" ? options.sdkVersion : detectInstalledSdkMajor() ?? "3";
156
+ const shouldPanel = shouldEnable && (options?.panel ?? true);
157
+ const shouldMcp = shouldEnable && (options?.mcp ?? false);
158
+ let lastState = null;
159
+ const tunnelOpt = resolveTunnelOption(options?.tunnel, process.env);
160
+ const shouldTunnel = isDev && !!tunnelOpt;
161
+ const webViewType = options?.webViewType ?? "partner";
162
+ const navBarTransparent = options?.navBarTransparent;
163
+ const navBarTheme = options?.navBarTheme;
164
+ const tunnelConfig = typeof tunnelOpt === "object" ? tunnelOpt : {};
165
+ return {
166
+ name: "ait-co-devtools",
167
+ enforce: "pre",
168
+ resolveId(id) {
169
+ if (!shouldMock) return null;
170
+ if (id === FRAMEWORK_ID || id === WEBVIEW_BRIDGE_ID || id === BRIDGE_ID || id === ANALYTICS_ID) {
171
+ if (id === BRIDGE_ID || id === ANALYTICS_ID) return MOCK_PATH_2X;
172
+ if (id === WEBVIEW_BRIDGE_ID) return MOCK_PATH_3X;
173
+ return sdkMajor === "2" ? MOCK_PATH_2X : MOCK_PATH_3X;
174
+ }
175
+ return null;
176
+ },
177
+ transformInclude(id) {
178
+ if (!shouldPanel && !shouldMcp) return false;
179
+ return /\.(tsx?|jsx?)$/.test(id) && /\/(main|index|entry|app)\.[tj]sx?$/i.test(id) && !id.includes("node_modules");
180
+ },
181
+ transform(code) {
182
+ let result = code;
183
+ let changed = false;
184
+ if (shouldMcp && !code.includes("__AIT_DEVTOOLS_MCP_ENABLED__")) {
185
+ result = `${MCP_ENABLE_SNIPPET}\n${result}`;
186
+ changed = true;
187
+ }
188
+ if (shouldPanel && !code.includes("@apps-in-toss/devtools/panel")) {
189
+ result = `import '@apps-in-toss/devtools/panel';\n${result}`;
190
+ changed = true;
191
+ }
192
+ return changed ? result : null;
193
+ },
194
+ vite: {
195
+ config() {
196
+ const define = { __WEB_VIEW_TYPE__: JSON.stringify(webViewType) };
197
+ if (!shouldTunnel) return { define };
198
+ return {
199
+ define,
200
+ server: { allowedHosts: [".trycloudflare.com"] }
201
+ };
202
+ },
203
+ configureServer(server) {
204
+ if (shouldMcp) server.middlewares.use(MCP_STATE_PATH, (req, res) => {
205
+ res.setHeader("Access-Control-Allow-Origin", "*");
206
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
207
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
208
+ if (req.method === "OPTIONS") {
209
+ res.writeHead(204);
210
+ res.end();
211
+ return;
212
+ }
213
+ if (req.method === "GET") {
214
+ if (lastState === null) {
215
+ res.writeHead(503, { "Content-Type": "application/json" });
216
+ res.end(JSON.stringify({ error: "No state received yet. Open the app in a browser first." }));
217
+ return;
218
+ }
219
+ res.writeHead(200, { "Content-Type": "application/json" });
220
+ res.end(lastState);
221
+ return;
222
+ }
223
+ if (req.method === "POST") {
224
+ const chunks = [];
225
+ req.on("data", (chunk) => chunks.push(chunk));
226
+ req.on("end", () => {
227
+ try {
228
+ const body = Buffer.concat(chunks).toString("utf-8");
229
+ JSON.parse(body);
230
+ lastState = body;
231
+ res.writeHead(204);
232
+ res.end();
233
+ } catch {
234
+ res.writeHead(400, { "Content-Type": "application/json" });
235
+ res.end(JSON.stringify({ error: "Invalid JSON" }));
236
+ }
237
+ });
238
+ return;
239
+ }
240
+ res.writeHead(405, { "Content-Type": "application/json" });
241
+ res.end(JSON.stringify({ error: "Method not allowed" }));
242
+ });
243
+ if (shouldTunnel) {
244
+ let tunnel = null;
245
+ let parentWatcher = null;
246
+ const httpServer = server.httpServer;
247
+ httpServer?.once("listening", () => {
248
+ const address = httpServer?.address();
249
+ const port = tunnelConfig.port ?? (address && typeof address === "object" ? address.port : void 0);
250
+ if (!port) {
251
+ console.warn("[@apps-in-toss/devtools] tunnel: could not determine the dev server port; skipping.");
252
+ return;
253
+ }
254
+ import("../tunnel-BvEf1qGV.js").then(async ({ startQuickTunnel, printTunnelBanner }) => {
255
+ const t = await startQuickTunnel(port);
256
+ tunnel = t;
257
+ let tunnelAppName;
258
+ try {
259
+ const { readFileSync } = await import("node:fs");
260
+ const pkgRaw = readFileSync(`${server.config.root}/package.json`, "utf8");
261
+ const pkg = JSON.parse(pkgRaw);
262
+ const rawName = typeof pkg.name === "string" ? pkg.name : "";
263
+ tunnelAppName = (rawName.includes("/") ? rawName.slice(rawName.indexOf("/") + 1) : rawName).trim() || void 0;
264
+ } catch {}
265
+ await printTunnelBanner(t.url, {
266
+ qr: tunnelConfig.qr,
267
+ name: tunnelAppName,
268
+ webViewType,
269
+ navBarTransparent,
270
+ navBarTheme
271
+ });
272
+ parentWatcher = startParentWatcher(() => {
273
+ cleanup();
274
+ process.exit(0);
275
+ });
276
+ }).catch((err) => {
277
+ console.warn(`[@apps-in-toss/devtools] tunnel failed to start: ${err instanceof Error ? err.message : String(err)}`);
278
+ });
279
+ });
280
+ const cleanup = () => {
281
+ parentWatcher?.stop();
282
+ tunnel?.stop();
283
+ };
284
+ httpServer?.once("close", cleanup);
285
+ process.once("SIGINT", cleanup);
286
+ process.once("SIGTERM", cleanup);
287
+ process.once("SIGHUP", cleanup);
288
+ process.once("exit", cleanup);
289
+ }
290
+ }
291
+ }
292
+ };
293
+ });
294
+ const vite = aitDevtoolsPlugin.vite;
295
+ const webpack = aitDevtoolsPlugin.webpack;
296
+ const rollup = aitDevtoolsPlugin.rollup;
297
+ const esbuild = aitDevtoolsPlugin.esbuild;
298
+ const rspack = aitDevtoolsPlugin.rspack;
299
+ const aitDevtools = aitDevtoolsPlugin;
300
+ //#endregion
301
+ export { aitDevtools as default, detectInstalledSdkMajor, esbuild, resolveTunnelOption, rollup, rspack, vite, webpack };