@apps-in-toss/devtools 3.1.0-beta.1 → 3.1.1

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.
@@ -25515,6 +25515,16 @@ const en = {
25515
25515
  "viewport.status.aitNavBar": "AIT nav bar",
25516
25516
  "viewport.status.aitNavBarValue": "{height}px → SafeArea top · {type}",
25517
25517
  "viewport.orientation.autoSuffix": "{orient} (auto)",
25518
+ "iap.section.products": "Product Catalog ({count})",
25519
+ "iap.empty.products": "(no products)",
25520
+ "iap.btn.addProduct": "+ Add Product",
25521
+ "iap.btn.removeProduct": "Remove",
25522
+ "iap.label.untitledProduct": "(untitled)",
25523
+ "iap.row.sku": "SKU",
25524
+ "iap.row.type": "Type",
25525
+ "iap.row.displayName": "Display Name",
25526
+ "iap.row.displayAmount": "Display Amount",
25527
+ "iap.row.renewalCycle": "Renewal Cycle",
25518
25528
  "iap.section.simulator": "IAP Simulator",
25519
25529
  "iap.row.nextResult": "Next Purchase Result",
25520
25530
  "iap.section.tossPay": "TossPay",
@@ -25570,40 +25580,7 @@ const en = {
25570
25580
  "notifications.section.title": "requestNotificationAgreement",
25571
25581
  "notifications.option.newAgreement": "newAgreement (first-time agree)",
25572
25582
  "notifications.option.alreadyAgreed": "alreadyAgreed (already opted-in)",
25573
- "notifications.option.agreementRejected": "agreementRejected (user declined)",
25574
- "launcher.title": "AITC DevTools Launcher",
25575
- "launcher.description": "Scan the terminal QR code or paste the tunnel URL.",
25576
- "launcher.installCta": "Install launcher to your phone",
25577
- "launcher.urlPlaceholder": "https://example.trycloudflare.com",
25578
- "launcher.openBtn": "Open",
25579
- "launcher.scanBtn": "Scan QR with camera",
25580
- "launcher.noCamera": "No camera available — paste the URL instead.",
25581
- "launcher.cameraError": "Could not access the camera — paste the URL instead.",
25582
- "launcher.invalidUrlHttps": "Enter a valid https:// URL (the tunnel URL from your terminal).",
25583
- "launcher.invalidUrl": "Enter a valid http(s):// URL.",
25584
- "launcher.debugAuthFailed": "Debug connection authentication failed",
25585
- "launcher.debugAuthFailedHint": "The QR code may have expired. Scan a fresh QR code.",
25586
- "launcher.debugAuthExpiredHint": "The debug session has expired. Scan a fresh QR from the attach page on your Mac.",
25587
- "launcher.debugAuthRescanCta": "Scan a new QR",
25588
- "launcher.diagTitle": "Viewport diagnostics",
25589
- "launcher.diagYes": "yes",
25590
- "launcher.diagNo": "no",
25591
- "launcher.letterboxDetected": "An iOS viewport constraint may clip the bottom {pt}pt — rotating to landscape and back to portrait may resolve it.",
25592
- "launcher.letterboxClipped": "An iOS viewport bug makes the bottom {pt}pt unusable — rotating to landscape and back to portrait may recover it.",
25593
- "launcher.diagVerdictLabel": "Verdict reason",
25594
- "launcher.diagSafeAreaTrace": "top re-measure trace",
25595
- "launcher.diagVerdict.detected": "✓ letterbox correction",
25596
- "launcher.diagVerdict.notStandalone": "not standalone",
25597
- "launcher.diagVerdict.landscape": "landscape",
25598
- "launcher.diagVerdict.shortfallTooSmall": "shortfall too small",
25599
- "launcher.diagVerdict.safeAreaTopZero": "top=0 (env() stale?)",
25600
- "launcher.navbar.defaultTitle": "Mini App",
25601
- "launcher.navbar.back": "Back",
25602
- "launcher.navbar.menu": "Menu",
25603
- "launcher.navbar.close": "Close",
25604
- "launcher.navbar.menuRescan": "Rescan",
25605
- "launcher.navbar.menuDiag": "Viewport diagnostics",
25606
- "launcher.navbar.menuLanguage": "Language"
25583
+ "notifications.option.agreementRejected": "agreementRejected (user declined)"
25607
25584
  };
25608
25585
  //#endregion
25609
25586
  //#region src/i18n/ko.ts
@@ -25693,6 +25670,16 @@ const ko = {
25693
25670
  "viewport.status.aitNavBar": "AIT nav bar",
25694
25671
  "viewport.status.aitNavBarValue": "{height}px → SafeArea top · {type}",
25695
25672
  "viewport.orientation.autoSuffix": "{orient} (auto)",
25673
+ "iap.section.products": "Product Catalog ({count})",
25674
+ "iap.empty.products": "(등록된 상품 없음)",
25675
+ "iap.btn.addProduct": "+ Add Product",
25676
+ "iap.btn.removeProduct": "Remove",
25677
+ "iap.label.untitledProduct": "(제목 없음)",
25678
+ "iap.row.sku": "SKU",
25679
+ "iap.row.type": "Type",
25680
+ "iap.row.displayName": "Display Name",
25681
+ "iap.row.displayAmount": "Display Amount",
25682
+ "iap.row.renewalCycle": "Renewal Cycle",
25696
25683
  "iap.section.simulator": "IAP Simulator",
25697
25684
  "iap.row.nextResult": "Next Purchase Result",
25698
25685
  "iap.section.tossPay": "TossPay",
@@ -25748,40 +25735,7 @@ const ko = {
25748
25735
  "notifications.section.title": "requestNotificationAgreement",
25749
25736
  "notifications.option.newAgreement": "newAgreement (최초 동의)",
25750
25737
  "notifications.option.alreadyAgreed": "alreadyAgreed (이미 동의됨)",
25751
- "notifications.option.agreementRejected": "agreementRejected (사용자 거절)",
25752
- "launcher.title": "AITC DevTools Launcher",
25753
- "launcher.description": "터미널 QR을 스캔하거나 URL을 입력하세요.",
25754
- "launcher.installCta": "폰에 런처 설치하기",
25755
- "launcher.urlPlaceholder": "https://example.trycloudflare.com",
25756
- "launcher.openBtn": "Open",
25757
- "launcher.scanBtn": "QR 카메라로 스캔",
25758
- "launcher.noCamera": "카메라를 사용할 수 없습니다 — URL을 직접 붙여넣으세요.",
25759
- "launcher.cameraError": "카메라에 접근할 수 없습니다 — URL을 직접 붙여넣으세요.",
25760
- "launcher.invalidUrlHttps": "올바른 https:// URL을 입력하세요 (터미널의 터널 URL).",
25761
- "launcher.invalidUrl": "올바른 http(s):// URL을 입력하세요.",
25762
- "launcher.debugAuthFailed": "디버그 연결 인증 실패",
25763
- "launcher.debugAuthFailedHint": "QR 코드가 만료되었을 수 있어요. 새 QR을 다시 스캔하세요.",
25764
- "launcher.debugAuthExpiredHint": "디버그 세션이 만료됐어요. Mac의 attach 페이지에서 새 QR을 스캔하세요.",
25765
- "launcher.debugAuthRescanCta": "새 QR 스캔하기",
25766
- "launcher.diagTitle": "뷰포트 진단",
25767
- "launcher.diagYes": "예",
25768
- "launcher.diagNo": "아니요",
25769
- "launcher.letterboxDetected": "iOS 뷰포트 제약으로 화면 아래 {pt}pt가 잘릴 수 있습니다 — 기기를 가로로 돌렸다 세로로 복귀하면 해소될 수 있어요.",
25770
- "launcher.letterboxClipped": "iOS 뷰포트 버그로 화면 아래 {pt}pt를 쓸 수 없습니다 — 기기를 가로로 돌렸다 세로로 돌리면 복구될 수 있어요.",
25771
- "launcher.diagVerdictLabel": "판정 사유",
25772
- "launcher.diagSafeAreaTrace": "top 재측정 추이",
25773
- "launcher.diagVerdict.detected": "✓ letterbox 보정",
25774
- "launcher.diagVerdict.notStandalone": "홈 화면 앱 아님",
25775
- "launcher.diagVerdict.landscape": "가로 모드",
25776
- "launcher.diagVerdict.shortfallTooSmall": "높이 차이 미달",
25777
- "launcher.diagVerdict.safeAreaTopZero": "top=0 (env() stale?)",
25778
- "launcher.navbar.defaultTitle": "미니앱",
25779
- "launcher.navbar.back": "뒤로가기",
25780
- "launcher.navbar.menu": "메뉴",
25781
- "launcher.navbar.close": "닫기",
25782
- "launcher.navbar.menuRescan": "다시 스캔",
25783
- "launcher.navbar.menuDiag": "뷰포트 진단",
25784
- "launcher.navbar.menuLanguage": "언어"
25738
+ "notifications.option.agreementRejected": "agreementRejected (사용자 거절)"
25785
25739
  };
25786
25740
  //#endregion
25787
25741
  //#region src/i18n/index.ts
@@ -25876,7 +25830,7 @@ function t(key, vars) {
25876
25830
  *
25877
25831
  * This adds NO second i18n system — it is a thin `useSyncExternalStore` wrapper
25878
25832
  * over the existing `getLocale()` / `setLocale()` / `LOCALE_CHANGE_EVENT` so that
25879
- * React surfaces (panel, fixture, launcher) re-render their subtree when the
25833
+ * React surfaces (panel, fixture) re-render their subtree when the
25880
25834
  * locale changes, instead of the vanilla panel's old `disposePanel()` → `mount()`
25881
25835
  * full-remount dance.
25882
25836
  *
@@ -25917,6 +25871,33 @@ function useT() {
25917
25871
  return t;
25918
25872
  }
25919
25873
  //#endregion
25874
+ //#region src/mock/deep-merge.ts
25875
+ function isPlainObject(value) {
25876
+ return typeof value === "object" && value !== null && !Array.isArray(value);
25877
+ }
25878
+ function mergeValue(base, override) {
25879
+ if (override === void 0) return base;
25880
+ if (Array.isArray(override)) return override;
25881
+ if (isPlainObject(base) && isPlainObject(override)) {
25882
+ const result = { ...base };
25883
+ for (const key of Object.keys(override)) {
25884
+ if (!(key in base)) continue;
25885
+ result[key] = mergeValue(base[key], override[key]);
25886
+ }
25887
+ return result;
25888
+ }
25889
+ return override;
25890
+ }
25891
+ /**
25892
+ * `base`(보통 `DEFAULT_STATE`)에 `override`를 재귀 병합한 새 객체를
25893
+ * 반환한다. `base`는 변형하지 않는다(불변). `override`가 `undefined`이면
25894
+ * `base`를 그대로 반환한다.
25895
+ */
25896
+ function deepMergeState(base, override) {
25897
+ if (override === void 0) return base;
25898
+ return mergeValue(base, override);
25899
+ }
25900
+ //#endregion
25920
25901
  //#region src/mock/throttle-registry.ts
25921
25902
  /**
25922
25903
  * throttle 레지스트리 (devtools#834).
@@ -25946,6 +25927,10 @@ function resetThrottleRegistry() {
25946
25927
  }
25947
25928
  //#endregion
25948
25929
  //#region src/mock/state.ts
25930
+ /**
25931
+ * @apps-in-toss/devtools 중앙 상태 관리
25932
+ * DevTools Panel과 mock 구현체가 이 상태를 공유한다.
25933
+ */
25949
25934
  /** SDK 호출 로그 ring buffer 상한 */
25950
25935
  const SDK_CALL_LOG_MAX = 200;
25951
25936
  const DEFAULT_STATE = {
@@ -26005,6 +25990,14 @@ const DEFAULT_STATE = {
26005
25990
  displayAmount: "1,000원",
26006
25991
  iconUrl: "",
26007
25992
  description: "게임에서 사용할 수 있는 보석 100개"
25993
+ }, {
25994
+ sku: "mock-sub-monthly",
25995
+ type: "SUBSCRIPTION",
25996
+ displayName: "월간 구독",
25997
+ displayAmount: "4,900원",
25998
+ iconUrl: "",
25999
+ description: "매달 자동으로 갱신되는 구독 상품",
26000
+ renewalCycle: "MONTHLY"
26008
26001
  }],
26009
26002
  nextResult: "success",
26010
26003
  pendingOrders: [],
@@ -26067,6 +26060,22 @@ const DEFAULT_STATE = {
26067
26060
  },
26068
26061
  failureModes: { sdkLine: "2.x" }
26069
26062
  };
26063
+ /**
26064
+ * `initialState` unplugin 옵션(Vite `define`로 주입된 `__AIT_INITIAL_STATE__`)이
26065
+ * 있으면 `DEFAULT_STATE`에 재귀 병합한 결과를, 없으면 `DEFAULT_STATE`를 그대로
26066
+ * 반환한다. devtools 자체 빌드/테스트에는 이 define이 없으므로
26067
+ * `typeof` 가드로 ReferenceError를 막는다(env.d.ts 참고).
26068
+ *
26069
+ * 모듈 로드 시 한 번만 계산해 생성자·`reset()` 양쪽에서 재사용한다 — `reset()`도
26070
+ * 이 병합된 값으로 되돌아가야, 소비자가 선언한 커스텀 카탈로그가 패널의
26071
+ * "초기화" 이후에도 사라지지 않는다(초기화는 "SDK 기본값"이 아니라 "이
26072
+ * 프로젝트가 선언한 기본값"으로 돌아가는 것이 사용자 기대에 맞다).
26073
+ */
26074
+ function resolveDefaultState() {
26075
+ const override = typeof __AIT_INITIAL_STATE__ !== "undefined" ? __AIT_INITIAL_STATE__ : void 0;
26076
+ return override ? deepMergeState(DEFAULT_STATE, override) : DEFAULT_STATE;
26077
+ }
26078
+ const RESOLVED_DEFAULT_STATE = resolveDefaultState();
26070
26079
  function generateDeviceId() {
26071
26080
  const stored = localStorage.getItem("__ait_device_id");
26072
26081
  if (stored) return stored;
@@ -26079,7 +26088,7 @@ var AitStateManager = class {
26079
26088
  _listeners = /* @__PURE__ */ new Set();
26080
26089
  _inTransaction = false;
26081
26090
  constructor() {
26082
- this._state = structuredClone(DEFAULT_STATE);
26091
+ this._state = structuredClone(RESOLVED_DEFAULT_STATE);
26083
26092
  try {
26084
26093
  this._state.deviceId = generateDeviceId();
26085
26094
  } catch {
@@ -26175,7 +26184,7 @@ var AitStateManager = class {
26175
26184
  reset() {
26176
26185
  const deviceId = this._state.deviceId;
26177
26186
  this._state = {
26178
- ...structuredClone(DEFAULT_STATE),
26187
+ ...structuredClone(RESOLVED_DEFAULT_STATE),
26179
26188
  deviceId
26180
26189
  };
26181
26190
  resetThrottleRegistry();
@@ -27839,39 +27848,6 @@ const showFullScreenAd = withIsSupported(observe("showFullScreenAd", "faithful",
27839
27848
  setTimeout(() => args.onEvent({ type: "dismissed" }), 1500);
27840
27849
  return () => {};
27841
27850
  }));
27842
- createMockProxy("Ads", {
27843
- showTossAdOrShoppingAds: withIsSupported(observe("Ads.showTossAdOrShoppingAds", "faithful", (args) => {
27844
- if (!aitState.state.ads.isLoaded) {
27845
- args.onError(/* @__PURE__ */ new Error("Ad not loaded"));
27846
- return () => {};
27847
- }
27848
- const { rewardUnitType, rewardAmount } = aitState.state.ads;
27849
- setTimeout(() => args.onEvent({
27850
- type: "userEarnedReward",
27851
- data: {
27852
- unitType: rewardUnitType,
27853
- unitAmount: rewardAmount
27854
- }
27855
- }), 1e3);
27856
- setTimeout(() => {
27857
- args.onEvent({ type: "dismissed" });
27858
- aitState.patch("ads", { isLoaded: false });
27859
- }, 1500);
27860
- return () => {};
27861
- })),
27862
- showTossShoppingAds: withIsSupported(observe("Ads.showTossShoppingAds", "faithful", (args) => {
27863
- const { rewardUnitType, rewardAmount } = aitState.state.ads;
27864
- setTimeout(() => args.onEvent({
27865
- type: "userEarnedReward",
27866
- data: {
27867
- unitType: rewardUnitType,
27868
- unitAmount: rewardAmount
27869
- }
27870
- }), 1e3);
27871
- setTimeout(() => args.onEvent({ type: "dismissed" }), 1500);
27872
- return () => {};
27873
- }))
27874
- });
27875
27851
  //#endregion
27876
27852
  //#region src/panel/tabs/ads.ts
27877
27853
  function recordEvent(type) {
@@ -28243,6 +28219,55 @@ Object.assign(async function requestTossPayPaysBilling(options) {
28243
28219
  }, { isSupported: () => true });
28244
28220
  //#endregion
28245
28221
  //#region src/panel/tabs/iap.ts
28222
+ const PRODUCT_TYPES = [
28223
+ "CONSUMABLE",
28224
+ "NON_CONSUMABLE",
28225
+ "SUBSCRIPTION"
28226
+ ];
28227
+ const RENEWAL_CYCLES = [
28228
+ "WEEKLY",
28229
+ "MONTHLY",
28230
+ "YEARLY"
28231
+ ];
28232
+ function newDefaultProduct() {
28233
+ return {
28234
+ sku: `mock-product-${Date.now()}`,
28235
+ type: "CONSUMABLE",
28236
+ displayName: "새 상품",
28237
+ displayAmount: "0원",
28238
+ iconUrl: "",
28239
+ description: ""
28240
+ };
28241
+ }
28242
+ /**
28243
+ * products[idx]를 partial 병합해 `aitState.patch('iap', { products })`로
28244
+ * 반영한다. `aitState.state`를 매번 다시 읽어(호출 시점 최신 스냅샷)
28245
+ * 여러 필드를 연속으로 고칠 때도 이전 입력이 유실되지 않게 한다.
28246
+ */
28247
+ function updateProduct(index, partial) {
28248
+ const next = [...aitState.state.iap.products];
28249
+ const current = next[index];
28250
+ if (!current) return;
28251
+ next[index] = {
28252
+ ...current,
28253
+ ...partial
28254
+ };
28255
+ aitState.patch("iap", { products: next });
28256
+ }
28257
+ function removeProduct(index) {
28258
+ const next = [...aitState.state.iap.products];
28259
+ next.splice(index, 1);
28260
+ aitState.patch("iap", { products: next });
28261
+ }
28262
+ function renderProductCard(product, index, disabled) {
28263
+ const card = h("div", { className: "ait-product-card" });
28264
+ const removeBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, t("iap.btn.removeProduct"));
28265
+ if (disabled) removeBtn.disabled = true;
28266
+ removeBtn.addEventListener("click", () => removeProduct(index));
28267
+ card.append(h("div", { className: "ait-row" }, h("strong", {}, product.sku || t("iap.label.untitledProduct")), removeBtn), inputRow(t("iap.row.sku"), product.sku, (v) => updateProduct(index, { sku: v }), disabled), selectRow(t("iap.row.type"), PRODUCT_TYPES, product.type, (v) => updateProduct(index, { type: v }), disabled), inputRow(t("iap.row.displayName"), product.displayName, (v) => updateProduct(index, { displayName: v }), disabled), inputRow(t("iap.row.displayAmount"), product.displayAmount, (v) => updateProduct(index, { displayAmount: v }), disabled));
28268
+ if (product.type === "SUBSCRIPTION") card.appendChild(selectRow(t("iap.row.renewalCycle"), RENEWAL_CYCLES, product.renewalCycle ?? "MONTHLY", (v) => updateProduct(index, { renewalCycle: v }), disabled));
28269
+ return card;
28270
+ }
28246
28271
  function formatTimestamp(iso) {
28247
28272
  const d = new Date(iso);
28248
28273
  if (Number.isNaN(d.getTime())) return iso;
@@ -28265,6 +28290,18 @@ function renderIapTab() {
28265
28290
  "INTERNAL_ERROR"
28266
28291
  ];
28267
28292
  if (disabled) container.appendChild(monitoringNotice());
28293
+ const products = s.iap.products;
28294
+ const productsSection = h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("iap.section.products", { count: products.length })));
28295
+ if (products.length === 0) productsSection.appendChild(h("div", { className: "ait-log-entry" }, t("iap.empty.products")));
28296
+ else products.forEach((product, index) => {
28297
+ productsSection.appendChild(renderProductCard(product, index, disabled));
28298
+ });
28299
+ const addProductBtn = h("button", { className: "ait-btn-secondary" }, t("iap.btn.addProduct"));
28300
+ if (disabled) addProductBtn.disabled = true;
28301
+ addProductBtn.addEventListener("click", () => {
28302
+ aitState.patch("iap", { products: [...aitState.state.iap.products, newDefaultProduct()] });
28303
+ });
28304
+ productsSection.appendChild(h("div", { className: "ait-btn-row" }, addProductBtn));
28268
28305
  const pendingOrders = s.iap.pendingOrders;
28269
28306
  const pendingSection = h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("iap.section.pending", { count: pendingOrders.length })));
28270
28307
  if (pendingOrders.length === 0) pendingSection.appendChild(h("div", { className: "ait-log-entry" }, t("iap.empty.pending")));
@@ -28280,7 +28317,7 @@ function renderIapTab() {
28280
28317
  const completedSection = h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("iap.section.completed", { count: completedOrders.length })));
28281
28318
  if (completedOrders.length === 0) completedSection.appendChild(h("div", { className: "ait-log-entry" }, t("iap.empty.completed")));
28282
28319
  else for (const o of completedOrders) completedSection.appendChild(h("div", { className: "ait-log-entry" }, h("span", { className: "ait-log-type" }, o.status), `${o.sku} (${shortOrderId(o.orderId)}) · ${formatTimestamp(o.date)}`));
28283
- container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("iap.section.simulator")), selectRow(t("iap.row.nextResult"), results, s.iap.nextResult, (v) => {
28320
+ container.append(productsSection, h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("iap.section.simulator")), selectRow(t("iap.row.nextResult"), results, s.iap.nextResult, (v) => {
28284
28321
  aitState.patch("iap", { nextResult: v });
28285
28322
  }, disabled)), h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("iap.section.tossPay")), selectRow(t("iap.row.tossPayResult"), ["success", "fail"], s.payment.nextResult, (v) => {
28286
28323
  aitState.patch("payment", { nextResult: v });
@@ -29100,6 +29137,17 @@ const PANEL_STYLES = `
29100
29137
  background: #3a3a5a;
29101
29138
  }
29102
29139
 
29140
+ /* IAP 상품 카탈로그 편집 카드 (한 상품 = 카드 1개) */
29141
+ .ait-product-card {
29142
+ border: 1px solid #3a3a5a;
29143
+ border-radius: 6px;
29144
+ padding: 8px;
29145
+ margin-bottom: 8px;
29146
+ }
29147
+ .ait-product-card .ait-row:last-child {
29148
+ margin-bottom: 0;
29149
+ }
29150
+
29103
29151
  /* Prompt notification */
29104
29152
  .ait-prompt-banner {
29105
29153
  background: #2d1b69;
@@ -30553,7 +30601,7 @@ function Panel() {
30553
30601
  color: "#666",
30554
30602
  fontWeight: 400
30555
30603
  },
30556
- children: ["v", "3.1.0-beta.1"]
30604
+ children: ["v", "3.1.1"]
30557
30605
  }),
30558
30606
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
30559
30607
  type: "button",
@@ -6,71 +6,6 @@ let node_fs = require("node:fs");
6
6
  let node_module = require("node:module");
7
7
  let node_path = require("node:path");
8
8
  let unplugin = require("unplugin");
9
- //#region src/shared/parent-watcher.ts
10
- /**
11
- * Shared parent-PID watcher — used by both the MCP debug daemon and the
12
- * unplugin tunnel path to self-terminate when the parent process (e.g. Claude
13
- * Code, vite) has died or been reparented without sending SIGTERM/SIGHUP.
14
- *
15
- * Intentionally react-free and Node-stdlib-only so this module is safe to
16
- * import from the MCP daemon bundle (`dist/mcp/cli.js`) without violating the
17
- * install-graph invariant.
18
- */
19
- /**
20
- * Returns `true` when the given PID refers to a running process.
21
- *
22
- * Uses `process.kill(pid, 0)` — a no-op signal that succeeds when the process
23
- * exists and we have permission to signal it; throws ESRCH when it doesn't exist.
24
- */
25
- function isPidAlive(pid) {
26
- try {
27
- process.kill(pid, 0);
28
- return true;
29
- } catch (err) {
30
- if (err.code === "EPERM") return true;
31
- return false;
32
- }
33
- }
34
- /**
35
- * Starts a periodic watcher that detects when the parent process (e.g. Claude
36
- * Code) has died without sending SIGTERM/SIGHUP, and calls `onOrphaned` so the
37
- * daemon can self-terminate rather than running as a zombie.
38
- *
39
- * Mirrors the `startAttachWatcher` pattern: `setInterval`-based, returns
40
- * `{ stop(): void }`, injectable deps for testability.
41
- *
42
- * @param onOrphaned - Called once when the parent is gone.
43
- * @param opts.intervalMs - Poll interval in milliseconds (default 5 000).
44
- * @param opts.initialPpid - Parent PID to watch (default `process.ppid`).
45
- * @param opts.isAlive - Predicate to test if a PID is running (default `isPidAlive`).
46
- * @param opts.getPpid - Supplier of current ppid (default `() => process.ppid`).
47
- * Detects ppid changes as well as death.
48
- * @param opts.log - Logger (default `process.stderr.write`).
49
- *
50
- * @returns `stop` — call during shutdown to clear the interval.
51
- */
52
- function startParentWatcher(onOrphaned, opts) {
53
- const { intervalMs = 5e3, initialPpid = process.ppid, isAlive = isPidAlive, getPpid = () => process.ppid, log = (msg) => process.stderr.write(msg) } = opts ?? {};
54
- if (initialPpid <= 1) {
55
- log("[ait-debug] parent-pid watcher: no parent to watch (ppid<=1), skipping\n");
56
- return { stop() {} };
57
- }
58
- let fired = false;
59
- const handle = setInterval(() => {
60
- if (fired) return;
61
- const currentPpid = getPpid();
62
- if (currentPpid !== initialPpid || !isAlive(initialPpid)) {
63
- fired = true;
64
- clearInterval(handle);
65
- log(`[ait-debug] parent-pid watcher: parent PID ${initialPpid} is gone (currentPpid=${currentPpid}) — shutting down\n`);
66
- onOrphaned();
67
- }
68
- }, intervalMs);
69
- return { stop() {
70
- clearInterval(handle);
71
- } };
72
- }
73
- //#endregion
74
9
  //#region src/unplugin/index.ts
75
10
  /**
76
11
  * @apps-in-toss/devtools unplugin
@@ -124,6 +59,14 @@ function detectInstalledSdkMajor(cwd = process.cwd()) {
124
59
  } catch {}
125
60
  return null;
126
61
  }
62
+ /**
63
+ * 패널 자동 주입 대상 진입점 파일명 기본 패턴. `entryPattern` 옵션 미지정 시
64
+ * 사용된다. `id.test()`는 상태를 갖지 않아야 하므로(매 파일마다 호출) 이
65
+ * 패턴과 사용자 지정 `entryPattern` 모두 `g`/`y` 플래그를 붙이지 않는다는
66
+ * 전제다 — `RegExp.prototype.test`는 global/sticky 플래그가 있으면 호출마다
67
+ * `lastIndex`가 전진해 다음 파일 매칭이 간헐적으로 실패한다.
68
+ */
69
+ const DEFAULT_ENTRY_PATTERN = /\/(main|index|entry|app)\.[tj]sx?$/i;
127
70
  const FRAMEWORK_ID = "@apps-in-toss/web-framework";
128
71
  const BRIDGE_ID = "@apps-in-toss/web-bridge";
129
72
  const ANALYTICS_ID = "@apps-in-toss/web-analytics";
@@ -132,40 +75,14 @@ const WEBVIEW_BRIDGE_ID = "@apps-in-toss/webview-bridge";
132
75
  const MCP_STATE_PATH = "/api/ait-devtools/state";
133
76
  /** Browser runtime opt-in consumed by the panel's state-sync helper. */
134
77
  const MCP_ENABLE_SNIPPET = "globalThis.__AIT_DEVTOOLS_MCP_ENABLED__ = true;";
135
- /**
136
- * Resolves the effective tunnel option (#425).
137
- *
138
- * An explicit `tunnel` value (including `false`) always takes priority over
139
- * env vars — the `??` operator means `undefined` (= omitted) falls through,
140
- * but `false` / `true` / an object are preserved as-is (non-breaking).
141
- *
142
- * When the option is omitted:
143
- * - `AIT_TUNNEL=1` enables the screen-preview tunnel.
144
- * - Not set → `false` (disabled).
145
- *
146
- * Extracted as a pure function so it can be unit-tested without standing up
147
- * a full Vite dev server.
148
- *
149
- * @param explicit - The `tunnel` option as passed by the consumer (or `undefined` when omitted).
150
- * @param env - The process environment (injectable for testing).
151
- */
152
- function resolveTunnelOption(explicit, env) {
153
- return explicit ?? !!env.AIT_TUNNEL;
154
- }
155
78
  const aitDevtoolsPlugin = (0, unplugin.createUnplugin)((options) => {
156
79
  const isDev = process.env.NODE_ENV !== "production";
157
- const shouldEnable = isDev;
80
+ const shouldEnable = isDev || !!options?.forceEnable;
158
81
  const shouldMock = shouldEnable && (options?.mock ?? isDev);
159
82
  const sdkMajor = options?.sdkVersion === "2" || options?.sdkVersion === "3" ? options.sdkVersion : detectInstalledSdkMajor() ?? "3";
160
83
  const shouldPanel = shouldEnable && (options?.panel ?? true);
161
84
  const shouldMcp = shouldEnable && (options?.mcp ?? false);
162
85
  let lastState = null;
163
- const tunnelOpt = resolveTunnelOption(options?.tunnel, process.env);
164
- const shouldTunnel = isDev && !!tunnelOpt;
165
- const webViewType = options?.webViewType ?? "partner";
166
- const navBarTransparent = options?.navBarTransparent;
167
- const navBarTheme = options?.navBarTheme;
168
- const tunnelConfig = typeof tunnelOpt === "object" ? tunnelOpt : {};
169
86
  return {
170
87
  name: "ait-co-devtools",
171
88
  enforce: "pre",
@@ -180,7 +97,7 @@ const aitDevtoolsPlugin = (0, unplugin.createUnplugin)((options) => {
180
97
  },
181
98
  transformInclude(id) {
182
99
  if (!shouldPanel && !shouldMcp) return false;
183
- return /\.(tsx?|jsx?)$/.test(id) && /\/(main|index|entry|app)\.[tj]sx?$/i.test(id) && !id.includes("node_modules");
100
+ return /\.(tsx?|jsx?)$/.test(id) && (options?.entryPattern ?? DEFAULT_ENTRY_PATTERN).test(id) && !id.includes("node_modules");
184
101
  },
185
102
  transform(code) {
186
103
  let result = code;
@@ -197,12 +114,8 @@ const aitDevtoolsPlugin = (0, unplugin.createUnplugin)((options) => {
197
114
  },
198
115
  vite: {
199
116
  config() {
200
- const define = { __WEB_VIEW_TYPE__: JSON.stringify(webViewType) };
201
- if (!shouldTunnel) return { define };
202
- return {
203
- define,
204
- server: { allowedHosts: [".trycloudflare.com"] }
205
- };
117
+ if (options?.initialState === void 0) return {};
118
+ return { define: { __AIT_INITIAL_STATE__: JSON.stringify(options.initialState) } };
206
119
  },
207
120
  configureServer(server) {
208
121
  if (shouldMcp) server.middlewares.use(MCP_STATE_PATH, (req, res) => {
@@ -244,53 +157,6 @@ const aitDevtoolsPlugin = (0, unplugin.createUnplugin)((options) => {
244
157
  res.writeHead(405, { "Content-Type": "application/json" });
245
158
  res.end(JSON.stringify({ error: "Method not allowed" }));
246
159
  });
247
- if (shouldTunnel) {
248
- let tunnel = null;
249
- let parentWatcher = null;
250
- const httpServer = server.httpServer;
251
- httpServer?.once("listening", () => {
252
- const address = httpServer?.address();
253
- const port = tunnelConfig.port ?? (address && typeof address === "object" ? address.port : void 0);
254
- if (!port) {
255
- console.warn("[@apps-in-toss/devtools] tunnel: could not determine the dev server port; skipping.");
256
- return;
257
- }
258
- Promise.resolve().then(() => require("../tunnel-DtCTOUlp.cjs")).then(async ({ startQuickTunnel, printTunnelBanner }) => {
259
- const t = await startQuickTunnel(port);
260
- tunnel = t;
261
- let tunnelAppName;
262
- try {
263
- const { readFileSync } = await import("node:fs");
264
- const pkgRaw = readFileSync(`${server.config.root}/package.json`, "utf8");
265
- const pkg = JSON.parse(pkgRaw);
266
- const rawName = typeof pkg.name === "string" ? pkg.name : "";
267
- tunnelAppName = (rawName.includes("/") ? rawName.slice(rawName.indexOf("/") + 1) : rawName).trim() || void 0;
268
- } catch {}
269
- await printTunnelBanner(t.url, {
270
- qr: tunnelConfig.qr,
271
- name: tunnelAppName,
272
- webViewType,
273
- navBarTransparent,
274
- navBarTheme
275
- });
276
- parentWatcher = startParentWatcher(() => {
277
- cleanup();
278
- process.exit(0);
279
- });
280
- }).catch((err) => {
281
- console.warn(`[@apps-in-toss/devtools] tunnel failed to start: ${err instanceof Error ? err.message : String(err)}`);
282
- });
283
- });
284
- const cleanup = () => {
285
- parentWatcher?.stop();
286
- tunnel?.stop();
287
- };
288
- httpServer?.once("close", cleanup);
289
- process.once("SIGINT", cleanup);
290
- process.once("SIGTERM", cleanup);
291
- process.once("SIGHUP", cleanup);
292
- process.once("exit", cleanup);
293
- }
294
160
  }
295
161
  }
296
162
  };
@@ -305,7 +171,6 @@ const aitDevtools = aitDevtoolsPlugin;
305
171
  exports.default = aitDevtools;
306
172
  exports.detectInstalledSdkMajor = detectInstalledSdkMajor;
307
173
  exports.esbuild = esbuild;
308
- exports.resolveTunnelOption = resolveTunnelOption;
309
174
  exports.rollup = rollup;
310
175
  exports.rspack = rspack;
311
176
  exports.vite = vite;