@ait-co/devtools 0.1.112 → 0.1.115
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bundle-KFs4t-wc.d.ts.map +1 -1
- package/dist/mcp/cli.js +218 -28
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/panel/index.js +7 -1
- package/dist/panel/index.js.map +1 -1
- package/dist/{pool-mZlgCmNQ.d.ts → pool-D23t4ibd.d.ts} +2 -2
- package/dist/{pool-mZlgCmNQ.d.ts.map → pool-D23t4ibd.d.ts.map} +1 -1
- package/dist/{qr-http-server-BVS-HZjU.cjs → qr-http-server-BpTvfeku.cjs} +92 -8
- package/dist/qr-http-server-BpTvfeku.cjs.map +1 -0
- package/dist/{qr-http-server-BJJt3ush.js → qr-http-server-BsWlOA85.js} +92 -8
- package/dist/qr-http-server-BsWlOA85.js.map +1 -0
- package/dist/{qr-http-server-C1T4RNbq.cjs → qr-http-server-CrVKno9V.cjs} +92 -8
- package/dist/qr-http-server-CrVKno9V.cjs.map +1 -0
- package/dist/{qr-http-server-Cs93vEPH.js → qr-http-server-UBV2qUEd.js} +92 -8
- package/dist/qr-http-server-UBV2qUEd.js.map +1 -0
- package/dist/{relay-worker-Dppp2yZj.d.ts → relay-worker-DERQUao2.d.ts} +2 -2
- package/dist/{relay-worker-Dppp2yZj.d.ts.map → relay-worker-DERQUao2.d.ts.map} +1 -1
- package/dist/runtime-BKMMoeMj.d.ts +153 -0
- package/dist/runtime-BKMMoeMj.d.ts.map +1 -0
- package/dist/test-runner/bundle.js +124 -18
- package/dist/test-runner/bundle.js.map +1 -1
- package/dist/test-runner/cli.js +125 -19
- package/dist/test-runner/cli.js.map +1 -1
- package/dist/test-runner/config.d.ts +1 -1
- package/dist/test-runner/pool.d.ts +1 -1
- package/dist/test-runner/relay-worker.d.ts +1 -1
- package/dist/test-runner/rpc.d.ts +1 -1
- package/dist/test-runner/runtime.d.ts +2 -2
- package/dist/test-runner/runtime.js +296 -18
- package/dist/test-runner/runtime.js.map +1 -1
- package/dist/test-runner/task-graph.d.ts +1 -1
- package/dist/{tunnel-Cpn3mA4u.js → tunnel-B7U5k1xa.js} +2 -2
- package/dist/{tunnel-Cpn3mA4u.js.map → tunnel-B7U5k1xa.js.map} +1 -1
- package/dist/{tunnel-Dj8Kf2QS.cjs → tunnel-C-cgMnyY.cjs} +2 -2
- package/dist/{tunnel-Dj8Kf2QS.cjs.map → tunnel-C-cgMnyY.cjs.map} +1 -1
- package/dist/unplugin/index.cjs +1 -1
- package/dist/unplugin/index.js +1 -1
- package/dist/unplugin/tunnel.cjs +1 -1
- package/dist/unplugin/tunnel.js +1 -1
- package/package.json +1 -1
- package/dist/qr-http-server-BJJt3ush.js.map +0 -1
- package/dist/qr-http-server-BVS-HZjU.cjs.map +0 -1
- package/dist/qr-http-server-C1T4RNbq.cjs.map +0 -1
- package/dist/qr-http-server-Cs93vEPH.js.map +0 -1
- package/dist/runtime-C7uxh3Mf.d.ts +0 -62
- package/dist/runtime-C7uxh3Mf.d.ts.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bundle-KFs4t-wc.d.ts","names":[],"sources":["../src/test-runner/bundle.ts"],"mappings":";;AAqEA;;;;;AAmBA;;;;;
|
|
1
|
+
{"version":3,"file":"bundle-KFs4t-wc.d.ts","names":[],"sources":["../src/test-runner/bundle.ts"],"mappings":";;AAqEA;;;;;AAmBA;;;;;AA8TA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAjViB,aAAA;;;;;;;EAOf,cAAA;;;;;EAKA,UAAA;AAAA;;;;;UAOe,YAAA;EACf,IAAA;EACA,QAAA;AAAA;;;;;;;;;;;;;;;iBA4ToB,cAAA,CAAe,OAAA,UAAiB,IAAA,GAAO,aAAA,GAAgB,OAAA,CAAQ,YAAA"}
|
package/dist/mcp/cli.js
CHANGED
|
@@ -298,12 +298,31 @@ async function discoverTestFiles(patterns, cwd) {
|
|
|
298
298
|
*
|
|
299
299
|
* SECRET-HANDLING: the returned bundle code is caller-managed; never log it.
|
|
300
300
|
*/
|
|
301
|
+
/** The SDK package name that mini-app test code imports from. */
|
|
302
|
+
const SDK_PACKAGE = "@apps-in-toss/web-framework";
|
|
303
|
+
/**
|
|
304
|
+
* Names the runtime installs as globals before invoking the user factory.
|
|
305
|
+
* The `vitest` virtual module re-exports each as a lazy getter that reads from
|
|
306
|
+
* `globalThis` at access time. Keep in sync with the globals installed in
|
|
307
|
+
* `runtime.ts#runTestModule`.
|
|
308
|
+
*/
|
|
309
|
+
const VITEST_GLOBAL_NAMES = [
|
|
310
|
+
"describe",
|
|
311
|
+
"it",
|
|
312
|
+
"test",
|
|
313
|
+
"expect",
|
|
314
|
+
"beforeAll",
|
|
315
|
+
"afterAll",
|
|
316
|
+
"beforeEach",
|
|
317
|
+
"afterEach",
|
|
318
|
+
"vi"
|
|
319
|
+
];
|
|
301
320
|
/**
|
|
302
321
|
* Matches the bare SDK package and any sub-path import
|
|
303
322
|
* (`@apps-in-toss/web-framework`, `@apps-in-toss/web-framework/foo`).
|
|
304
323
|
* Built from {@link SDK_PACKAGE} so the package name has a single source.
|
|
305
324
|
*/
|
|
306
|
-
const SDK_IMPORT_FILTER = new RegExp(`^${
|
|
325
|
+
const SDK_IMPORT_FILTER = new RegExp(`^${SDK_PACKAGE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`);
|
|
307
326
|
/**
|
|
308
327
|
* esbuild plugin that intercepts SDK imports and redirects them to the
|
|
309
328
|
* `window.__sdk` proxy that `src/in-app/auto.ts` installs at runtime.
|
|
@@ -344,6 +363,53 @@ module.exports = __proxy;
|
|
|
344
363
|
};
|
|
345
364
|
}
|
|
346
365
|
/**
|
|
366
|
+
* esbuild plugin that intercepts `import … from 'vitest'` and replaces it with
|
|
367
|
+
* a virtual module that delegates every named import to `globalThis` at ACCESS
|
|
368
|
+
* time (not at bundle-evaluation time).
|
|
369
|
+
*
|
|
370
|
+
* The runtime installs `describe/it/test/expect/beforeAll/afterAll/beforeEach/
|
|
371
|
+
* afterEach/vi` as globals inside `runTestModule`, which runs AFTER the bundle
|
|
372
|
+
* IIFE is evaluated. A value-copy redirect (`export var describe =
|
|
373
|
+
* globalThis.describe`) would therefore capture `undefined` at evaluation time
|
|
374
|
+
* and the user's `describe(...)` calls would be no-ops — registering zero tests.
|
|
375
|
+
*
|
|
376
|
+
* The fix defers the lookup to call time using per-name **getter** exports.
|
|
377
|
+
* We emit a CommonJS module that:
|
|
378
|
+
* 1. sets `__esModule = true` so esbuild's `__toESM` interop maps each named
|
|
379
|
+
* import directly to a property access on the module (NOT wrapped under a
|
|
380
|
+
* `default` shim — which is what happens for a bare Proxy whose own-keys
|
|
381
|
+
* are empty, leaving every named import `undefined`);
|
|
382
|
+
* 2. defines each global name as a getter that reads `globalThis[name]` on
|
|
383
|
+
* every access. So `import { describe } from 'vitest'` compiles to
|
|
384
|
+
* `import_vitest.describe`, whose getter returns the real `describe` only
|
|
385
|
+
* when the factory calls it — after `runTestModule` installs the globals.
|
|
386
|
+
*
|
|
387
|
+
* A plain `module.exports = new Proxy(...)` does NOT work here: esbuild routes
|
|
388
|
+
* the virtual module through `__toESM`, which enumerates own-keys (none on an
|
|
389
|
+
* empty Proxy target) and therefore exposes zero named exports. Explicit getter
|
|
390
|
+
* properties give `__toESM` real keys to map while keeping access lazy.
|
|
391
|
+
*/
|
|
392
|
+
function vitestRedirectPlugin() {
|
|
393
|
+
return {
|
|
394
|
+
name: "vitest-redirect",
|
|
395
|
+
setup(build) {
|
|
396
|
+
build.onResolve({ filter: /^vitest$/ }, () => ({
|
|
397
|
+
path: "vitest",
|
|
398
|
+
namespace: "vitest-redirect"
|
|
399
|
+
}));
|
|
400
|
+
build.onLoad({
|
|
401
|
+
filter: /^vitest$/,
|
|
402
|
+
namespace: "vitest-redirect"
|
|
403
|
+
}, () => {
|
|
404
|
+
return {
|
|
405
|
+
contents: `Object.defineProperty(exports, '__esModule', { value: true });\n${VITEST_GLOBAL_NAMES.map((name) => `Object.defineProperty(exports, ${JSON.stringify(name)}, { enumerable: true, get: function() { return globalThis[${JSON.stringify(name)}]; } });`).join("\n")}\n`,
|
|
406
|
+
loader: "js"
|
|
407
|
+
};
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
347
413
|
* esbuild plugin that transforms the user test file into a module that exports
|
|
348
414
|
* an async `__userFactory` function. The factory defers the user's top-level
|
|
349
415
|
* test registration code (describe/it/test calls) so it only runs when
|
|
@@ -351,8 +417,19 @@ module.exports = __proxy;
|
|
|
351
417
|
* installed describe/it/test/expect as globals.
|
|
352
418
|
*
|
|
353
419
|
* Algorithm:
|
|
354
|
-
* -
|
|
355
|
-
*
|
|
420
|
+
* - Import declarations and re-export statements are kept at module top-level
|
|
421
|
+
* (the only valid ESM position for static `import`). A statement that spans
|
|
422
|
+
* multiple lines — e.g. a named import with one member per line:
|
|
423
|
+
* import {
|
|
424
|
+
* appLogin,
|
|
425
|
+
* getAnonymousKey,
|
|
426
|
+
* } from '@apps-in-toss/web-framework';
|
|
427
|
+
* is tracked as a single block: every line from the opening `import {` /
|
|
428
|
+
* `export {` through the closing `from '…'` (or side-effect `'…'`) line is
|
|
429
|
+
* kept together at top-level. This prevents the member lines and the
|
|
430
|
+
* closing `} from '…'` line from leaking into the factory body, which would
|
|
431
|
+
* leave an unterminated `import {` at module scope (the #678 env3 failure:
|
|
432
|
+
* esbuild threw `Expected "as" but found "{"` on multi-line SDK imports).
|
|
356
433
|
* - All other lines (describe/it/test calls, local declarations, etc.) are
|
|
357
434
|
* moved into the body of the exported async factory function.
|
|
358
435
|
*
|
|
@@ -376,12 +453,25 @@ function userFactoryPlugin(absPath) {
|
|
|
376
453
|
const topLevelLines = [];
|
|
377
454
|
const bodyLines = [];
|
|
378
455
|
const EXPORT_DECLARATION_RE = /^(export\s+)(default\s+|async\s+function\s+|function\s+|class\s+|const\s+|let\s+|var\s+)/;
|
|
456
|
+
const isImportStart = (trimmed) => trimmed.startsWith("import ") || trimmed.startsWith("import{") || trimmed.startsWith("import'") || trimmed.startsWith("import\"");
|
|
457
|
+
const endsStatement = (trimmed) => /['"]\s*;?\s*$/.test(trimmed.replace(/\/\/.*$/, "").trimEnd());
|
|
458
|
+
let inImportBlock = false;
|
|
379
459
|
for (const line of lines) {
|
|
380
460
|
const trimmed = line.trimStart();
|
|
381
461
|
const indent = line.slice(0, line.length - trimmed.length);
|
|
382
|
-
if (
|
|
383
|
-
|
|
384
|
-
|
|
462
|
+
if (inImportBlock) {
|
|
463
|
+
topLevelLines.push(line);
|
|
464
|
+
if (endsStatement(trimmed)) inImportBlock = false;
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
if (isImportStart(trimmed)) {
|
|
468
|
+
topLevelLines.push(line);
|
|
469
|
+
if (!endsStatement(trimmed)) inImportBlock = true;
|
|
470
|
+
} else if (trimmed.startsWith("export ")) if (trimmed.match(EXPORT_DECLARATION_RE)) bodyLines.push(indent + trimmed.slice(7));
|
|
471
|
+
else {
|
|
472
|
+
topLevelLines.push(line);
|
|
473
|
+
if (/\bfrom\b/.test(trimmed) ? !endsStatement(trimmed) : trimmed.endsWith("{")) inImportBlock = true;
|
|
474
|
+
}
|
|
385
475
|
else bodyLines.push(line);
|
|
386
476
|
}
|
|
387
477
|
return {
|
|
@@ -401,21 +491,33 @@ function userFactoryPlugin(absPath) {
|
|
|
401
491
|
};
|
|
402
492
|
}
|
|
403
493
|
/**
|
|
404
|
-
* Returns the absolute path to the
|
|
494
|
+
* Returns the absolute path to the test-runner runtime module.
|
|
495
|
+
*
|
|
496
|
+
* Searches candidates in priority order:
|
|
497
|
+
* 1. Co-located `runtime.ts` / `runtime.js` — covers the source tree
|
|
498
|
+
* (tsx / ts-node) and the `dist/test-runner/` entry.
|
|
499
|
+
* 2. `../test-runner/runtime.js` — covers the `dist/mcp/cli.js` entry,
|
|
500
|
+
* where `import.meta.url` resolves to `dist/mcp/` (a sibling directory
|
|
501
|
+
* of `dist/test-runner/`). Without this second candidate the MCP entry
|
|
502
|
+
* point would look for `dist/mcp/runtime.js`, which does not exist, and
|
|
503
|
+
* every `run_tests` call would fail with an esbuild "Could not resolve"
|
|
504
|
+
* error (#678).
|
|
405
505
|
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
506
|
+
* Returns the first candidate that exists on disk. Falls back to the
|
|
507
|
+
* co-located `runtime.js` path so esbuild produces a clear "file not found"
|
|
508
|
+
* error rather than a cryptic failure.
|
|
409
509
|
*/
|
|
410
510
|
function getRuntimePath() {
|
|
411
511
|
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
512
|
+
const candidates = [
|
|
513
|
+
path.join(dir, "runtime.ts"),
|
|
514
|
+
path.join(dir, "runtime.js"),
|
|
515
|
+
path.join(dir, "..", "test-runner", "runtime.js")
|
|
516
|
+
];
|
|
517
|
+
for (const candidate of candidates) try {
|
|
518
|
+
accessSync(candidate);
|
|
519
|
+
return candidate;
|
|
520
|
+
} catch {}
|
|
419
521
|
return path.join(dir, "runtime.js");
|
|
420
522
|
}
|
|
421
523
|
/**
|
|
@@ -454,7 +556,11 @@ async function bundleTestFile(absPath, opts) {
|
|
|
454
556
|
platform: "browser",
|
|
455
557
|
target: "es2022",
|
|
456
558
|
write: false,
|
|
457
|
-
plugins: [
|
|
559
|
+
plugins: [
|
|
560
|
+
userFactoryPlugin(absPath),
|
|
561
|
+
vitestRedirectPlugin(),
|
|
562
|
+
sdkRedirectPlugin()
|
|
563
|
+
],
|
|
458
564
|
external: extraExternals,
|
|
459
565
|
treeShaking: true,
|
|
460
566
|
footer: { js: `globalThis[${JSON.stringify(globalName)}] = ${globalName};` }
|
|
@@ -2731,6 +2837,9 @@ const en = {
|
|
|
2731
2837
|
"dashboard.inspector.waiting": "Attach a page to enable the \"Open DevTools\" button",
|
|
2732
2838
|
"inspector.error.noTarget": "No page attached. Attach a device and try again.",
|
|
2733
2839
|
"inspector.error.relayDown": "Relay is not active. Start a relay session first.",
|
|
2840
|
+
"dashboard.watchdog.title": "Server has shut down",
|
|
2841
|
+
"dashboard.watchdog.body": "The MCP server has stopped. This session is no longer active. You may close this tab.",
|
|
2842
|
+
"dashboard.watchdog.close": "Close tab",
|
|
2734
2843
|
"attach.title": "AIT Debug Session — QR Scan",
|
|
2735
2844
|
"attach.deployment": "deployment: {label}",
|
|
2736
2845
|
"attach.steps.section": "How to scan",
|
|
@@ -2966,6 +3075,9 @@ const tables = {
|
|
|
2966
3075
|
"dashboard.inspector.waiting": "페이지를 attach하면 \"디버그 툴 열기\" 버튼이 표시됩니다",
|
|
2967
3076
|
"inspector.error.noTarget": "연결된 페이지가 없습니다. 기기를 attach한 후 다시 시도하세요.",
|
|
2968
3077
|
"inspector.error.relayDown": "relay가 활성화되지 않았습니다. start_debug로 relay를 기동하세요.",
|
|
3078
|
+
"dashboard.watchdog.title": "서버가 종료되었습니다",
|
|
3079
|
+
"dashboard.watchdog.body": "MCP server가 종료되어 이 세션이 더 이상 유효하지 않습니다. 이 탭을 닫아도 됩니다.",
|
|
3080
|
+
"dashboard.watchdog.close": "탭 닫기",
|
|
2969
3081
|
"attach.title": "AIT 디버그 세션 — QR 스캔",
|
|
2970
3082
|
"attach.deployment": "deployment: {label}",
|
|
2971
3083
|
"attach.steps.section": "스캔 절차",
|
|
@@ -3513,7 +3625,7 @@ function buildLangSwitcher(path, existingParams, locale, s) {
|
|
|
3513
3625
|
* - tunnel wssUrl은 "터널 연결됨" 상태 표시에서 UP/DOWN만 노출.
|
|
3514
3626
|
* wssUrl 값 자체는 dashboard HTML에 넣지 않는다.
|
|
3515
3627
|
*/
|
|
3516
|
-
function buildDashboardHtml(state, qrDataUrl, locale, path = "/", params = new URLSearchParams(), devtoolsEntryUrl = null) {
|
|
3628
|
+
function buildDashboardHtml(state, qrDataUrl, locale, path = "/", params = new URLSearchParams(), devtoolsEntryUrl = null, sseRefreshIntervalMs = 9e4) {
|
|
3517
3629
|
const s = resolveLocaleStrings(locale);
|
|
3518
3630
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3519
3631
|
const tunnelStatus = state.tunnel.up ? s("dashboard.tunnel.up") : s("dashboard.tunnel.down");
|
|
@@ -3532,6 +3644,7 @@ function buildDashboardHtml(state, qrDataUrl, locale, path = "/", params = new U
|
|
|
3532
3644
|
const pagesSection = state.pages === null ? "" : `<hr /><section id="pages-section"><h2>${escapeHtml(s("dashboard.pages.section"))}</h2><ul id="pages-list">${state.pages.length > 0 ? state.pages.map((p) => {
|
|
3533
3645
|
return `<li><span class="page-id">${escapeHtml(p.id)}</span> <span class="page-url">${escapeHtml(p.url.slice(0, 120))}</span></li>`;
|
|
3534
3646
|
}).join("\n") : `<li class="empty">${escapeHtml(s("dashboard.pages.empty"))}</li>`}</ul></section>`;
|
|
3647
|
+
const watchdogTimeoutMs = sseRefreshIntervalMs < 5e3 ? sseRefreshIntervalMs * 3 : sseRefreshIntervalMs * 2 + 3e4;
|
|
3535
3648
|
const sseStrings = {
|
|
3536
3649
|
tunnelUp: JSON.stringify(s("dashboard.tunnel.up")),
|
|
3537
3650
|
tunnelDown: JSON.stringify(s("dashboard.tunnel.down")),
|
|
@@ -3542,7 +3655,11 @@ function buildDashboardHtml(state, qrDataUrl, locale, path = "/", params = new U
|
|
|
3542
3655
|
copiedLabel: JSON.stringify(s("dashboard.url.copied")),
|
|
3543
3656
|
inspectorOpenLabel: JSON.stringify(s("dashboard.inspector.open")),
|
|
3544
3657
|
inspectorWaitingLabel: JSON.stringify(s("dashboard.inspector.waiting")),
|
|
3545
|
-
dashboardSurface: true
|
|
3658
|
+
dashboardSurface: true,
|
|
3659
|
+
watchdogTimeoutMs,
|
|
3660
|
+
watchdogTitle: JSON.stringify(s("dashboard.watchdog.title")),
|
|
3661
|
+
watchdogBody: JSON.stringify(s("dashboard.watchdog.body")),
|
|
3662
|
+
watchdogCloseLabel: JSON.stringify(s("dashboard.watchdog.close"))
|
|
3546
3663
|
};
|
|
3547
3664
|
const langSwitcher = buildLangSwitcher(path, params, locale, s);
|
|
3548
3665
|
const filled = dashboardChromeByLocale[locale].replaceAll("__LANG_SWITCHER__", langSwitcher).replaceAll("__NOW__", escapeHtml(now)).replaceAll("__TUNNEL_CLASS__", tunnelClass).replaceAll("__TUNNEL_STATUS__", escapeHtml(tunnelStatus)).replaceAll("__ATTACH_SECTION__", attachSection).replaceAll("__INSPECTOR_SECTION__", inspectorSection).replaceAll("__PAGES_SECTION__", pagesSection);
|
|
@@ -3568,6 +3685,10 @@ function buildDashboardHtml(state, qrDataUrl, locale, path = "/", params = new U
|
|
|
3568
3685
|
* - clipboard: navigator.clipboard.writeText → 실패/부재 시 textarea execCommand fallback.
|
|
3569
3686
|
* - 피드백: 버튼 라벨이 COPIED_LABEL로 ~1.5초 전환 후 COPY_LABEL로 복귀.
|
|
3570
3687
|
*
|
|
3688
|
+
* SSE watchdog (#681): SSE 주기 갱신이 `watchdogTimeoutMs` 동안 수신되지 않으면
|
|
3689
|
+
* server가 종료된 것으로 판단한다. `window.close()`를 시도하고, 닫히지 않으면
|
|
3690
|
+
* 폴백 안내 화면으로 교체해 stale QR이 계속 보이는 오해를 항상 방지한다.
|
|
3691
|
+
*
|
|
3571
3692
|
* 문자열 인자는 빌드타임에 ko/en 테이블에서 가져와 JSON.stringify로 이미 escape됨.
|
|
3572
3693
|
*
|
|
3573
3694
|
* SECRET-HANDLING: URL 값을 console.log 등으로 출력하지 않는다.
|
|
@@ -3587,6 +3708,14 @@ function buildSseScript(strings) {
|
|
|
3587
3708
|
var INSPECTOR_OPEN_LABEL = ${strings.inspectorOpenLabel};
|
|
3588
3709
|
var INSPECTOR_WAITING_LABEL = ${strings.inspectorWaitingLabel};
|
|
3589
3710
|
|
|
3711
|
+
// ── SSE watchdog 상수 (#681) ──────────────────────────────────────────
|
|
3712
|
+
// server의 SSE refresh 주기(기본 90s)보다 넉넉히 큰 값 — 주기 × 2 + 30s 여유.
|
|
3713
|
+
// token-fill 패턴으로 server에서 계산해 주입한다.
|
|
3714
|
+
var WATCHDOG_TIMEOUT_MS = ${strings.watchdogTimeoutMs};
|
|
3715
|
+
var WATCHDOG_TITLE = ${strings.watchdogTitle};
|
|
3716
|
+
var WATCHDOG_BODY = ${strings.watchdogBody};
|
|
3717
|
+
var WATCHDOG_CLOSE_LABEL = ${strings.watchdogCloseLabel};
|
|
3718
|
+
|
|
3590
3719
|
// ── 클립보드 복사 헬퍼 ────────────────────────────────────────────────
|
|
3591
3720
|
function copyText(text) {
|
|
3592
3721
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
@@ -3641,9 +3770,65 @@ function buildSseScript(strings) {
|
|
|
3641
3770
|
}
|
|
3642
3771
|
});
|
|
3643
3772
|
|
|
3773
|
+
// ── SSE watchdog (#681) ───────────────────────────────────────────────
|
|
3774
|
+
// 마지막으로 SSE 메시지를 수신한 시각. 스크립트 로드 시각으로 초기화한다.
|
|
3775
|
+
// (서버가 /events 연결 즉시 초기 상태를 push하므로 onmessage가 곧 갱신한다.)
|
|
3776
|
+
var lastSeen = Date.now();
|
|
3777
|
+
// watchdog이 한 번 발화하면 타이머를 clear해 중복 실행 방지.
|
|
3778
|
+
var watchdogFired = false;
|
|
3779
|
+
var watchdogTimer = null;
|
|
3780
|
+
|
|
3781
|
+
// 폴백 UI — window.close()가 무시되면 stale QR 대신 안내 화면을 표시한다.
|
|
3782
|
+
function showWatchdogFallback() {
|
|
3783
|
+
if (document.body) {
|
|
3784
|
+
document.body.innerHTML =
|
|
3785
|
+
'<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;' +
|
|
3786
|
+
'height:100vh;font-family:sans-serif;text-align:center;padding:2rem;box-sizing:border-box;">' +
|
|
3787
|
+
'<p style="font-size:1.4rem;font-weight:bold;margin-bottom:1rem;">' + WATCHDOG_TITLE + '</p>' +
|
|
3788
|
+
'<p style="color:#555;margin-bottom:2rem;">' + WATCHDOG_BODY + '</p>' +
|
|
3789
|
+
'<button onclick="window.close()" ' +
|
|
3790
|
+
'style="padding:0.6rem 1.4rem;font-size:1rem;cursor:pointer;border:1px solid #ccc;' +
|
|
3791
|
+
'border-radius:6px;background:#f5f5f5;">' + WATCHDOG_CLOSE_LABEL + '</button>' +
|
|
3792
|
+
'</div>';
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
|
|
3796
|
+
function fireWatchdog() {
|
|
3797
|
+
if (watchdogFired) return;
|
|
3798
|
+
watchdogFired = true;
|
|
3799
|
+
if (watchdogTimer) { clearInterval(watchdogTimer); watchdogTimer = null; }
|
|
3800
|
+
// 폴백 UI를 먼저 표시한다 — close가 성공하면 어차피 페이지가 닫혀 보이지 않는다.
|
|
3801
|
+
// close 실패(opener 없는 탭)에 대비해 stale QR 대신 안내 화면을 항상 보장한다.
|
|
3802
|
+
showWatchdogFallback();
|
|
3803
|
+
// window.close() 시도 — OS가 연 탭은 opener가 없어 무시될 수 있다.
|
|
3804
|
+
window.close();
|
|
3805
|
+
}
|
|
3806
|
+
|
|
3807
|
+
// 주기적으로 lastSeen을 검사 — WATCHDOG_TIMEOUT_MS 초과 시 발화.
|
|
3808
|
+
// 검사 간격: WATCHDOG_TIMEOUT_MS / 6, 단 최소 1s 이상이되 timeout 자체보다 크지 않게 한다.
|
|
3809
|
+
// (sseRefreshIntervalMs가 작을 때 최솟값 1s가 timeout을 초과하는 경우 방지 — 테스트 격리.)
|
|
3810
|
+
var checkIntervalMs = Math.min(
|
|
3811
|
+
Math.max(1000, Math.round(WATCHDOG_TIMEOUT_MS / 6)),
|
|
3812
|
+
WATCHDOG_TIMEOUT_MS,
|
|
3813
|
+
);
|
|
3814
|
+
watchdogTimer = setInterval(function () {
|
|
3815
|
+
if (watchdogFired) return;
|
|
3816
|
+
if (Date.now() - lastSeen > WATCHDOG_TIMEOUT_MS) {
|
|
3817
|
+
fireWatchdog();
|
|
3818
|
+
}
|
|
3819
|
+
}, checkIntervalMs);
|
|
3820
|
+
|
|
3644
3821
|
// ── SSE 구독 ──────────────────────────────────────────────────────────
|
|
3645
3822
|
var src = new EventSource('/events');
|
|
3823
|
+
src.onopen = function () {
|
|
3824
|
+
// 연결 복구 시 lastSeen 갱신 — 일시적 끊김이 watchdog을 발화시키지 않게.
|
|
3825
|
+
// watchdog이 이미 발화했으면 갱신하지 않는다(폴백 UI를 그대로 유지).
|
|
3826
|
+
if (!watchdogFired) { lastSeen = Date.now(); }
|
|
3827
|
+
};
|
|
3646
3828
|
src.onmessage = function (e) {
|
|
3829
|
+
// SSE 메시지 수신 — lastSeen 갱신으로 watchdog 리셋.
|
|
3830
|
+
// watchdog 발화 후라면 갱신해도 타이머가 이미 clear됐으므로 효과 없음.
|
|
3831
|
+
if (!watchdogFired) { lastSeen = Date.now(); }
|
|
3647
3832
|
try {
|
|
3648
3833
|
var s = JSON.parse(e.data);
|
|
3649
3834
|
// 터널 상태 갱신
|
|
@@ -3750,7 +3935,7 @@ function buildSseScript(strings) {
|
|
|
3750
3935
|
* SECRET-HANDLING: TOTP at= 코드는 attachUrl 캡슐 안에서만 노출 — 의도된 transport.
|
|
3751
3936
|
* inspectorStableUrl은 /inspector 안정 URL (127.0.0.1, 시크릿 없음) — 노출 가능.
|
|
3752
3937
|
*/
|
|
3753
|
-
function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl, locale, path = "/attach", params = new URLSearchParams(), mode, pagesAttached = false, inspectorStableUrl = null) {
|
|
3938
|
+
function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl, locale, path = "/attach", params = new URLSearchParams(), mode, pagesAttached = false, inspectorStableUrl = null, sseRefreshIntervalMs = 9e4) {
|
|
3754
3939
|
const s = resolveLocaleStrings(locale);
|
|
3755
3940
|
const langSwitcher = buildLangSwitcher(path, params, locale, s);
|
|
3756
3941
|
const family = attachFamilyForMode(mode);
|
|
@@ -3758,6 +3943,7 @@ function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl, locale, path = "/a
|
|
|
3758
3943
|
if (pagesAttached && inspectorStableUrl) inspectorSection = `<a class="inspector-link" id="inspector-link" href="${escapeHtml(inspectorStableUrl)}" target="_blank" rel="noopener noreferrer">${escapeHtml(s("dashboard.inspector.open"))}</a>`;
|
|
3759
3944
|
else inspectorSection = `<span class="inspector-hint" id="inspector-link">${escapeHtml(s("dashboard.inspector.waiting"))}</span>`;
|
|
3760
3945
|
const filled = attachChromeByLocale[locale][family].replaceAll("__LANG_SWITCHER__", langSwitcher).replaceAll("__MODE_LABEL__", buildModeLabel(mode, s)).replaceAll("__QR_DATA_URL__", qrDataUrl).replaceAll("__SAFE_LABEL__", safeLabel).replaceAll("__SAFE_ATTACH_URL__", safeAttachUrl).replaceAll("__INSPECTOR_SECTION__", inspectorSection);
|
|
3946
|
+
const watchdogTimeoutMs = sseRefreshIntervalMs < 5e3 ? sseRefreshIntervalMs * 3 : sseRefreshIntervalMs * 2 + 3e4;
|
|
3761
3947
|
const sseScript = buildSseScript({
|
|
3762
3948
|
tunnelUp: JSON.stringify(s("dashboard.tunnel.up")),
|
|
3763
3949
|
tunnelDown: JSON.stringify(s("dashboard.tunnel.down")),
|
|
@@ -3768,7 +3954,11 @@ function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl, locale, path = "/a
|
|
|
3768
3954
|
copiedLabel: JSON.stringify(s("dashboard.url.copied")),
|
|
3769
3955
|
inspectorOpenLabel: JSON.stringify(s("dashboard.inspector.open")),
|
|
3770
3956
|
inspectorWaitingLabel: JSON.stringify(s("dashboard.inspector.waiting")),
|
|
3771
|
-
dashboardSurface: false
|
|
3957
|
+
dashboardSurface: false,
|
|
3958
|
+
watchdogTimeoutMs,
|
|
3959
|
+
watchdogTitle: JSON.stringify(s("dashboard.watchdog.title")),
|
|
3960
|
+
watchdogBody: JSON.stringify(s("dashboard.watchdog.body")),
|
|
3961
|
+
watchdogCloseLabel: JSON.stringify(s("dashboard.watchdog.close"))
|
|
3772
3962
|
});
|
|
3773
3963
|
return filled.replace("</body>", `${sseScript}\n</body>`);
|
|
3774
3964
|
}
|
|
@@ -3785,6 +3975,7 @@ async function startQrHttpServer(getDashboardState, options) {
|
|
|
3785
3975
|
const { default: QRCode } = await import("qrcode");
|
|
3786
3976
|
/** SSE 활성 연결 목록 — `notifyStateChange()` 시 전체 push. */
|
|
3787
3977
|
const sseClients = [];
|
|
3978
|
+
const refreshIntervalMs = options?.sseRefreshIntervalMs ?? 9e4;
|
|
3788
3979
|
/** SSE 연결 하나에 상태 이벤트를 flush한다. */
|
|
3789
3980
|
function pushStateToClient(res, state) {
|
|
3790
3981
|
const payload = JSON.stringify({
|
|
@@ -3823,7 +4014,7 @@ async function startQrHttpServer(getDashboardState, options) {
|
|
|
3823
4014
|
if (!addr || typeof addr === "string") return null;
|
|
3824
4015
|
return `http://127.0.0.1:${addr.port}/devtools/`;
|
|
3825
4016
|
})();
|
|
3826
|
-
const html = buildDashboardHtml(state, qrDataUrl, locale, path, params, devtoolsEntryUrl);
|
|
4017
|
+
const html = buildDashboardHtml(state, qrDataUrl, locale, path, params, devtoolsEntryUrl, refreshIntervalMs);
|
|
3827
4018
|
res.writeHead(200, {
|
|
3828
4019
|
"Content-Type": "text/html; charset=utf-8",
|
|
3829
4020
|
"Cache-Control": "no-store"
|
|
@@ -3879,7 +4070,7 @@ async function startQrHttpServer(getDashboardState, options) {
|
|
|
3879
4070
|
type: "image/png",
|
|
3880
4071
|
errorCorrectionLevel: "M"
|
|
3881
4072
|
}).then((dataUrl) => {
|
|
3882
|
-
const html = buildAttachHtml(dataUrl, escapeHtml(deploymentIdLabel), escapeHtml(attachUrl), locale, path, params, mode, pagesAttached, inspectorStableUrlForAttach);
|
|
4073
|
+
const html = buildAttachHtml(dataUrl, escapeHtml(deploymentIdLabel), escapeHtml(attachUrl), locale, path, params, mode, pagesAttached, inspectorStableUrlForAttach, refreshIntervalMs);
|
|
3883
4074
|
res.writeHead(200, {
|
|
3884
4075
|
"Content-Type": "text/html; charset=utf-8",
|
|
3885
4076
|
"Cache-Control": "no-store"
|
|
@@ -3962,7 +4153,6 @@ async function startQrHttpServer(getDashboardState, options) {
|
|
|
3962
4153
|
pushStateToClient(client, state);
|
|
3963
4154
|
} catch {}
|
|
3964
4155
|
}
|
|
3965
|
-
const refreshIntervalMs = options?.sseRefreshIntervalMs ?? 9e4;
|
|
3966
4156
|
const refreshHandle = setInterval(() => {
|
|
3967
4157
|
if (sseClients.length > 0 && getDashboardState) notifyStateChangeInternal();
|
|
3968
4158
|
}, refreshIntervalMs).unref();
|
|
@@ -5375,7 +5565,7 @@ async function readMcpSdkVersion() {
|
|
|
5375
5565
|
* some test environments that skip the build step).
|
|
5376
5566
|
*/
|
|
5377
5567
|
function readDevtoolsVersion() {
|
|
5378
|
-
return "0.1.
|
|
5568
|
+
return "0.1.115";
|
|
5379
5569
|
}
|
|
5380
5570
|
/**
|
|
5381
5571
|
* Derives the next recommended action from a completed diagnostics snapshot.
|
|
@@ -6295,7 +6485,7 @@ function createDebugServer(deps) {
|
|
|
6295
6485
|
}
|
|
6296
6486
|
const server = new Server({
|
|
6297
6487
|
name: "ait-debug",
|
|
6298
|
-
version: "0.1.
|
|
6488
|
+
version: "0.1.115"
|
|
6299
6489
|
}, { capabilities: { tools: { listChanged: true } } });
|
|
6300
6490
|
server.setRequestHandler(ListToolsRequestSchema, () => {
|
|
6301
6491
|
const conn = router.active;
|
|
@@ -8195,7 +8385,7 @@ function createDevServer(deps = {}) {
|
|
|
8195
8385
|
const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
|
|
8196
8386
|
const server = new Server({
|
|
8197
8387
|
name: "ait-devtools",
|
|
8198
|
-
version: "0.1.
|
|
8388
|
+
version: "0.1.115"
|
|
8199
8389
|
}, { capabilities: { tools: {} } });
|
|
8200
8390
|
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
|
|
8201
8391
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|