@aryanduntley/pwa-debug 0.1.6 → 0.1.8
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.md +2 -0
- package/dist/main.js +113 -12
- package/extension/service-worker.js +150 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -29,6 +29,8 @@ The goal is to eliminate the "user is the AI's eyes and hands" loop. Today, debu
|
|
|
29
29
|
> - **PWA Runtime Diagnostics** — service-worker lifecycle + versions, CacheStorage contents + age, installability gaps, a live capability matrix, IndexedDB/web-storage inspection, update-propagation / version-skew analysis, and a one-shot runtime-state snapshot.
|
|
30
30
|
>
|
|
31
31
|
> **Verified on Linux** (the full suite live-tested against a real PWA). macOS/Windows code paths are implemented with unit coverage but still need real-machine retest ([help wanted](#help-wanted-macos--windows-verification)). Firefox is not supported (it doesn't speak CDP).
|
|
32
|
+
>
|
|
33
|
+
> ⚠️ **macOS / Windows users, beware:** these platforms have **never been run on real hardware** — only unit-tested with injected fakes. Expect rough edges (browser detection, profile paths, native-messaging registration, system-default resolution). Please [open an issue](../../issues) with the failing command and its output — bug reports from real macOS/Windows machines are the single most useful contribution right now. See [Help wanted](#help-wanted-macos--windows-verification) for the specific things to try.
|
|
32
34
|
|
|
33
35
|
## How it differs from `chrome-devtools-mcp`
|
|
34
36
|
|
package/dist/main.js
CHANGED
|
@@ -12797,12 +12797,18 @@ const browserUrlFor = (port) => `http://127.0.0.1:${port}`;
|
|
|
12797
12797
|
* Chromium flags for a fresh launch — shared by the exec-by-path and flatpak
|
|
12798
12798
|
* builders so the two command forms differ ONLY in the command prefix, never in
|
|
12799
12799
|
* the flag set. --no-first-run / --no-default-browser-check keep it non-interactive.
|
|
12800
|
+
*
|
|
12801
|
+
* `extraArgs` are caller-supplied Chromium startup flags (e.g.
|
|
12802
|
+
* --enable-speech-dispatcher) appended AFTER the managed flags so the caller can
|
|
12803
|
+
* override a non-essential default; they only matter on a cold spawn (this path),
|
|
12804
|
+
* never on an attach.
|
|
12800
12805
|
*/
|
|
12801
|
-
const freshFlags = (port, userDataDir) => Object.freeze([
|
|
12806
|
+
const freshFlags = (port, userDataDir, extraArgs = []) => Object.freeze([
|
|
12802
12807
|
`--remote-debugging-port=${port}`,
|
|
12803
12808
|
`--user-data-dir=${userDataDir}`,
|
|
12804
12809
|
'--no-first-run',
|
|
12805
12810
|
'--no-default-browser-check',
|
|
12811
|
+
...extraArgs,
|
|
12806
12812
|
]);
|
|
12807
12813
|
/**
|
|
12808
12814
|
* Chromium flags for a sandbox launch (dedicated profile + preloaded extension).
|
|
@@ -12826,7 +12832,7 @@ const freshFlags = (port, userDataDir) => Object.freeze([
|
|
|
12826
12832
|
* preloads pwa-debug, while the profile's other extensions stay enabled. No-op
|
|
12827
12833
|
* under 'manual-guided' (the flag is already omitted there).
|
|
12828
12834
|
*/
|
|
12829
|
-
const sandboxFlags = (port, userDataDir, extensionPath, strategy, isolate) => {
|
|
12835
|
+
const sandboxFlags = (port, userDataDir, extensionPath, strategy, isolate, extraArgs = []) => {
|
|
12830
12836
|
const extensionFlags = strategy === 'manual-guided'
|
|
12831
12837
|
? []
|
|
12832
12838
|
: [
|
|
@@ -12844,6 +12850,7 @@ const sandboxFlags = (port, userDataDir, extensionPath, strategy, isolate) => {
|
|
|
12844
12850
|
'--no-default-browser-check',
|
|
12845
12851
|
'--disable-session-crashed-bubble',
|
|
12846
12852
|
'--hide-crash-restore-bubble',
|
|
12853
|
+
...extraArgs,
|
|
12847
12854
|
]);
|
|
12848
12855
|
};
|
|
12849
12856
|
/**
|
|
@@ -12865,9 +12872,12 @@ const flatpakRun = (appId, browserFlags) => Object.freeze({
|
|
|
12865
12872
|
/**
|
|
12866
12873
|
* Fresh launch (sub-state c): bring up the debug port on the user's profile.
|
|
12867
12874
|
*/
|
|
12868
|
-
const buildFreshSpawnArgs = (execPath, port, userDataDir) => Object.freeze({
|
|
12875
|
+
const buildFreshSpawnArgs = (execPath, port, userDataDir, extraArgs = []) => Object.freeze({
|
|
12876
|
+
cmd: execPath,
|
|
12877
|
+
args: freshFlags(port, userDataDir, extraArgs),
|
|
12878
|
+
});
|
|
12869
12879
|
/** Fresh launch for a flatpak browser: `flatpak run <app-id> <fresh flags>`. */
|
|
12870
|
-
const buildFreshFlatpakArgs = (appId, port, userDataDir) => flatpakRun(appId, freshFlags(port, userDataDir));
|
|
12880
|
+
const buildFreshFlatpakArgs = (appId, port, userDataDir, extraArgs = []) => flatpakRun(appId, freshFlags(port, userDataDir, extraArgs));
|
|
12871
12881
|
/**
|
|
12872
12882
|
* New-window launch (sub-state b): re-invoke the binary so it opens a window
|
|
12873
12883
|
* in the already-running session via IPC. No debug port — that requires a full
|
|
@@ -12891,9 +12901,9 @@ const buildNewWindowFlatpakArgs = (appId) => flatpakRun(appId, Object.freeze(['-
|
|
|
12891
12901
|
* Applied to sandbox modes only — an 'existing'-mode launch is the user's real
|
|
12892
12902
|
* profile, where a genuine restore prompt should be left intact.
|
|
12893
12903
|
*/
|
|
12894
|
-
const buildSandboxSpawnArgs = (execPath, port, userDataDir, extensionPath, strategy, isolate = true) => Object.freeze({
|
|
12904
|
+
const buildSandboxSpawnArgs = (execPath, port, userDataDir, extensionPath, strategy, isolate = true, extraArgs = []) => Object.freeze({
|
|
12895
12905
|
cmd: execPath,
|
|
12896
|
-
args: sandboxFlags(port, userDataDir, extensionPath, strategy, isolate),
|
|
12906
|
+
args: sandboxFlags(port, userDataDir, extensionPath, strategy, isolate, extraArgs),
|
|
12897
12907
|
});
|
|
12898
12908
|
/**
|
|
12899
12909
|
* Sandbox launch for a flatpak browser: `flatpak run <app-id> <sandbox flags>`.
|
|
@@ -12902,7 +12912,7 @@ const buildSandboxSpawnArgs = (execPath, port, userDataDir, extensionPath, strat
|
|
|
12902
12912
|
* (`flatpak override --user --filesystem=host <app-id>`) for these to resolve
|
|
12903
12913
|
* inside the sandbox — the same prerequisite the NMH path documents.
|
|
12904
12914
|
*/
|
|
12905
|
-
const buildSandboxFlatpakArgs = (appId, port, userDataDir, extensionPath, strategy, isolate = true) => flatpakRun(appId, sandboxFlags(port, userDataDir, extensionPath, strategy, isolate));
|
|
12915
|
+
const buildSandboxFlatpakArgs = (appId, port, userDataDir, extensionPath, strategy, isolate = true, extraArgs = []) => flatpakRun(appId, sandboxFlags(port, userDataDir, extensionPath, strategy, isolate, extraArgs));
|
|
12906
12916
|
|
|
12907
12917
|
/**
|
|
12908
12918
|
* 'existing'-mode launch: the graceful-degradation triad orchestrated over the
|
|
@@ -12948,9 +12958,10 @@ const launchExisting = async (input, deps) => {
|
|
|
12948
12958
|
});
|
|
12949
12959
|
}
|
|
12950
12960
|
// spawn-fresh
|
|
12961
|
+
const extraArgs = input.extraArgs ?? [];
|
|
12951
12962
|
const { cmd, args } = input.appId
|
|
12952
|
-
? buildFreshFlatpakArgs(input.appId, input.port, input.userDataDir)
|
|
12953
|
-
: buildFreshSpawnArgs(input.execPath, input.port, input.userDataDir);
|
|
12963
|
+
? buildFreshFlatpakArgs(input.appId, input.port, input.userDataDir, extraArgs)
|
|
12964
|
+
: buildFreshSpawnArgs(input.execPath, input.port, input.userDataDir, extraArgs);
|
|
12954
12965
|
const { pid } = await deps.spawnBrowser(cmd, args);
|
|
12955
12966
|
// Chromium 136+ ignores --remote-debugging-port on the default profile: the
|
|
12956
12967
|
// browser comes up (pwa-debug extension still usable) but the port never
|
|
@@ -13012,9 +13023,10 @@ const launchSandbox = async (input, deps) => {
|
|
|
13012
13023
|
await deps.seedDeveloperMode(input.userDataDir);
|
|
13013
13024
|
// Default to isolation (clean-room) when unset; false lets other extensions coexist.
|
|
13014
13025
|
const isolate = input.isolateExtensions ?? true;
|
|
13026
|
+
const extraArgs = input.extraArgs ?? [];
|
|
13015
13027
|
const { cmd, args } = input.appId
|
|
13016
|
-
? buildSandboxFlatpakArgs(input.appId, input.port, input.userDataDir, input.extensionPath, input.loadStrategy, isolate)
|
|
13017
|
-
: buildSandboxSpawnArgs(input.execPath, input.port, input.userDataDir, input.extensionPath, input.loadStrategy, isolate);
|
|
13028
|
+
? buildSandboxFlatpakArgs(input.appId, input.port, input.userDataDir, input.extensionPath, input.loadStrategy, isolate, extraArgs)
|
|
13029
|
+
: buildSandboxSpawnArgs(input.execPath, input.port, input.userDataDir, input.extensionPath, input.loadStrategy, isolate, extraArgs);
|
|
13018
13030
|
const { pid } = await deps.spawnBrowser(cmd, args);
|
|
13019
13031
|
if (input.mode === 'sandbox-temp') {
|
|
13020
13032
|
deps.registerTempProfile(input.userDataDir);
|
|
@@ -14020,6 +14032,7 @@ const inputSchema$5 = {
|
|
|
14020
14032
|
mode: enumType(MODES).optional(),
|
|
14021
14033
|
packaging: enumType(PACKAGINGS).optional(),
|
|
14022
14034
|
isolateExtensions: booleanType().optional(),
|
|
14035
|
+
extraArgs: arrayType(stringType()).optional(),
|
|
14023
14036
|
};
|
|
14024
14037
|
const isSandboxMode = (mode) => mode === 'sandbox-persistent' || mode === 'sandbox-temp';
|
|
14025
14038
|
/**
|
|
@@ -14214,6 +14227,7 @@ const launchBrowserCore = async (args, platform, env, deps) => {
|
|
|
14214
14227
|
...(args.isolateExtensions !== undefined
|
|
14215
14228
|
? { isolateExtensions: args.isolateExtensions }
|
|
14216
14229
|
: {}),
|
|
14230
|
+
...(args.extraArgs !== undefined ? { extraArgs: args.extraArgs } : {}),
|
|
14217
14231
|
...(target.appId !== undefined ? { appId: target.appId } : {}),
|
|
14218
14232
|
...(snapPkg ? { snapPackage: snapPkg } : {}),
|
|
14219
14233
|
});
|
|
@@ -14238,6 +14252,7 @@ const launchBrowserCore = async (args, platform, env, deps) => {
|
|
|
14238
14252
|
userDataDir,
|
|
14239
14253
|
debugPortBlockedOnDefaultProfile: portBlocked,
|
|
14240
14254
|
...(target.appId !== undefined ? { appId: target.appId } : {}),
|
|
14255
|
+
...(args.extraArgs !== undefined ? { extraArgs: args.extraArgs } : {}),
|
|
14241
14256
|
});
|
|
14242
14257
|
deps.recordLaunch(result, port);
|
|
14243
14258
|
return launchOk(result, target, alternatives, args.packaging !== undefined);
|
|
@@ -14264,7 +14279,7 @@ const launchBrowserHandler = async (args, ctx) => launchBrowserCore(args, proces
|
|
|
14264
14279
|
});
|
|
14265
14280
|
const launchBrowserTool = Object.freeze({
|
|
14266
14281
|
name: 'pdl_launch_browser',
|
|
14267
|
-
description: "Launch or attach to a Chromium-family browser with a live remote-debugging port, for use alongside chrome-devtools-mcp. Modes: mode='existing' (default) targets the user's normal profile and degrades gracefully — (a) port already live → attach; (b) running without a debug port → opens a NEW WINDOW in the existing session (never kills it), attached:false + degradation message; (c) not running → spawns fresh with --remote-debugging-port + --user-data-dir=<your profile>. mode='sandbox-persistent' spawns a dedicated, persistent dev profile at ~/.pwa-debug/profiles/<browser>/ beside your normal browser, with the pwa-debug extension PRELOADED (no reload needed); mode='sandbox-temp' is the same but in a throwaway mkdtemp profile cleaned up on host shutdown. Sandbox modes always work standalone (separate profile → no lock collision) and both pwa-debug + CDP tools are available. Args: browser? (chrome|chromium|edge|brave|vivaldi|opera; defaults to system-default), port? (default 9222), mode?, packaging? (native|snap|flatpak). When the same browser is installed under multiple packagings (e.g. snap AND flatpak chromium), pass packaging to pick one; without it the default preference is native > snap > flatpak and next_steps lists the alternatives so you can re-target. isolateExtensions? (sandbox modes only, default true): true pins the dedicated profile to ONLY the pwa-debug extension (clean room — every other extension is disabled); pass false to let other extensions coexist (pwa-debug still preloads, while extensions already in the persistent profile or Load-unpacked/installed after launch stay enabled) — use this to debug a PWA alongside other extensions or to test your own extension with pwa-debug. existing mode already keeps all your normal-profile extensions. Linux is first-class; macOS/Windows deferred. Follow next_steps[] — it carries the chrome-devtools-mcp registration snippet, the profile location, the flatpak onboarding steps, or the degradation guidance.",
|
|
14282
|
+
description: "Launch or attach to a Chromium-family browser with a live remote-debugging port, for use alongside chrome-devtools-mcp. Modes: mode='existing' (default) targets the user's normal profile and degrades gracefully — (a) port already live → attach; (b) running without a debug port → opens a NEW WINDOW in the existing session (never kills it), attached:false + degradation message; (c) not running → spawns fresh with --remote-debugging-port + --user-data-dir=<your profile>. mode='sandbox-persistent' spawns a dedicated, persistent dev profile at ~/.pwa-debug/profiles/<browser>/ beside your normal browser, with the pwa-debug extension PRELOADED (no reload needed); mode='sandbox-temp' is the same but in a throwaway mkdtemp profile cleaned up on host shutdown. Sandbox modes always work standalone (separate profile → no lock collision) and both pwa-debug + CDP tools are available. Args: browser? (chrome|chromium|edge|brave|vivaldi|opera; defaults to system-default), port? (default 9222), mode?, packaging? (native|snap|flatpak). When the same browser is installed under multiple packagings (e.g. snap AND flatpak chromium), pass packaging to pick one; without it the default preference is native > snap > flatpak and next_steps lists the alternatives so you can re-target. isolateExtensions? (sandbox modes only, default true): true pins the dedicated profile to ONLY the pwa-debug extension (clean room — every other extension is disabled); pass false to let other extensions coexist (pwa-debug still preloads, while extensions already in the persistent profile or Load-unpacked/installed after launch stay enabled) — use this to debug a PWA alongside other extensions or to test your own extension with pwa-debug. existing mode already keeps all your normal-profile extensions. extraArgs? (string[]): extra Chromium startup flags appended after pwa-debug's managed flags — e.g. extraArgs=['--enable-speech-dispatcher'] to enable system TTS/speech-dispatcher, or any other --flag the browser needs at startup. Applied only on a COLD spawn (mode='existing' when the browser isn't already running, or either sandbox mode); they have no effect when attaching to an already-live port or opening a new window in a running session, since startup flags are read once at process start — fully quit the browser (or use a sandbox mode) for them to take effect. Linux is first-class; macOS/Windows deferred. Follow next_steps[] — it carries the chrome-devtools-mcp registration snippet, the profile location, the flatpak onboarding steps, or the degradation guidance.",
|
|
14268
14283
|
inputSchema: inputSchema$5,
|
|
14269
14284
|
handler: launchBrowserHandler,
|
|
14270
14285
|
});
|
|
@@ -14856,6 +14871,90 @@ const registerChromeDevtoolsTool = Object.freeze({
|
|
|
14856
14871
|
handler: registerChromeDevtoolsHandler,
|
|
14857
14872
|
});
|
|
14858
14873
|
|
|
14874
|
+
// The SW waits up to timeout_ms (capped below) for the page to finish loading
|
|
14875
|
+
// before replying, so the IPC budget must comfortably exceed it.
|
|
14876
|
+
const NAV_MAX_LOAD_TIMEOUT_MS = 12_000;
|
|
14877
|
+
const NAV_IPC_TIMEOUT_MS = NAV_MAX_LOAD_TIMEOUT_MS + 3_000;
|
|
14878
|
+
const navigateInputSchema = {
|
|
14879
|
+
extension_id: stringType().min(1).optional(),
|
|
14880
|
+
tab_id: numberType().int().optional(),
|
|
14881
|
+
url: stringType().min(1),
|
|
14882
|
+
timeout_ms: numberType().int().positive().max(NAV_MAX_LOAD_TIMEOUT_MS).optional(),
|
|
14883
|
+
};
|
|
14884
|
+
const newTabInputSchema = {
|
|
14885
|
+
extension_id: stringType().min(1).optional(),
|
|
14886
|
+
url: stringType().min(1),
|
|
14887
|
+
active: booleanType().optional(),
|
|
14888
|
+
timeout_ms: numberType().int().positive().max(NAV_MAX_LOAD_TIMEOUT_MS).optional(),
|
|
14889
|
+
};
|
|
14890
|
+
/**
|
|
14891
|
+
* Shared IPC round-trip for the navigation tools: resolve the target NMH, send a
|
|
14892
|
+
* single request envelope to the SW, and normalize transport vs. handler errors.
|
|
14893
|
+
* The SW handler (request_router) drives chrome.tabs and waits for load-complete.
|
|
14894
|
+
*/
|
|
14895
|
+
const sendNavRequest = async (ctx, extensionId, tool, payload, nextSteps) => {
|
|
14896
|
+
const target = resolveTarget$1(ctx, extensionId);
|
|
14897
|
+
if (!target.ok) {
|
|
14898
|
+
return errorResponse(target.error, [
|
|
14899
|
+
'Call host_status to see activeConnections. If empty, ensure host_register_extension has been called and the extension reloaded at chrome://extensions.',
|
|
14900
|
+
]);
|
|
14901
|
+
}
|
|
14902
|
+
const env = Object.freeze({
|
|
14903
|
+
type: 'request',
|
|
14904
|
+
requestId: randomUUID(),
|
|
14905
|
+
tool,
|
|
14906
|
+
extensionId: target.extensionId,
|
|
14907
|
+
payload,
|
|
14908
|
+
});
|
|
14909
|
+
let response;
|
|
14910
|
+
try {
|
|
14911
|
+
response = await ctx.ipcServer.request(target.extensionId, env, {
|
|
14912
|
+
timeoutMs: NAV_IPC_TIMEOUT_MS,
|
|
14913
|
+
});
|
|
14914
|
+
}
|
|
14915
|
+
catch (err) {
|
|
14916
|
+
return errorResponse(`${tool} failed: ${err.message}`, [
|
|
14917
|
+
`IPC request did not complete. Confirm the SW is connected and the ${tool} handler is wired in the SW.`,
|
|
14918
|
+
]);
|
|
14919
|
+
}
|
|
14920
|
+
if (response.error) {
|
|
14921
|
+
return errorResponse(`${tool} nmh error: ${response.error.message}`, [
|
|
14922
|
+
'The SW rejected the navigation (invalid URL, no active tab, or chrome.tabs threw). Pass an explicit tab_id, or open a tab first.',
|
|
14923
|
+
]);
|
|
14924
|
+
}
|
|
14925
|
+
const data = {
|
|
14926
|
+
extensionId: target.extensionId,
|
|
14927
|
+
...response.payload,
|
|
14928
|
+
};
|
|
14929
|
+
return okResponse(data, [...nextSteps]);
|
|
14930
|
+
};
|
|
14931
|
+
const navigateHandler = async (args, ctx) => sendNavRequest(ctx, args.extension_id, 'pdl_navigate', {
|
|
14932
|
+
url: args.url,
|
|
14933
|
+
...(args.tab_id !== undefined ? { tab_id: args.tab_id } : {}),
|
|
14934
|
+
...(args.timeout_ms !== undefined ? { timeout_ms: args.timeout_ms } : {}),
|
|
14935
|
+
}, [
|
|
14936
|
+
'Navigated the active (or given) tab. Returns { tabId, url, status }: status "complete" = page finished loading; "loading" = navigation happened but the load was still in flight when the wait elapsed (raise timeout_ms, or poll session_ping). pwa-debug capture/inspection tools now target the new page.',
|
|
14937
|
+
]);
|
|
14938
|
+
const newTabHandler = async (args, ctx) => sendNavRequest(ctx, args.extension_id, 'pdl_new_tab', {
|
|
14939
|
+
url: args.url,
|
|
14940
|
+
...(args.active !== undefined ? { active: args.active } : {}),
|
|
14941
|
+
...(args.timeout_ms !== undefined ? { timeout_ms: args.timeout_ms } : {}),
|
|
14942
|
+
}, [
|
|
14943
|
+
'Opened a new tab. Returns { tabId, url, status, created:true }. Pass the returned tabId to other pwa-debug tools (react_tree, console_tail, pdl_click, …) to target this tab; without tab_id they use the active tab.',
|
|
14944
|
+
]);
|
|
14945
|
+
const navigateTool = Object.freeze({
|
|
14946
|
+
name: 'pdl_navigate',
|
|
14947
|
+
description: "Navigate a browser tab to a URL, driven through the pwa-debug extension's service worker (chrome.tabs.update) — NO CDP, so it works on the user's real, logged-in profile without chrome-devtools-mcp being registered or attached. Targets the active tab in the last-focused window unless tab_id is given. The URL may omit the scheme (https:// is assumed); a javascript: URL is rejected. Waits for the page to reach document 'complete' (up to timeout_ms, default 10000, max 12000) before returning { extensionId, tabId, url, status, windowId? }, where status is 'complete' or 'loading' (load still in flight at timeout — the navigation still happened). Args: { url: non-empty string, tab_id?, timeout_ms?, extension_id? }. With no extension_id/tab_id, targets the single connected NMH and the active tab. CALL host_status FIRST to confirm a connection.",
|
|
14948
|
+
inputSchema: navigateInputSchema,
|
|
14949
|
+
handler: navigateHandler,
|
|
14950
|
+
});
|
|
14951
|
+
const newTabTool = Object.freeze({
|
|
14952
|
+
name: 'pdl_new_tab',
|
|
14953
|
+
description: "Open a NEW browser tab at a URL via the pwa-debug extension's service worker (chrome.tabs.create) — NO CDP, works on the user's real profile with no chrome-devtools-mcp dependency. The URL may omit the scheme (https:// is assumed); a javascript: URL is rejected. active? controls foreground vs. background (defaults to foreground). Waits for the page to reach document 'complete' (up to timeout_ms, default 10000, max 12000) before returning { extensionId, tabId, url, status, windowId?, created:true }. Pass the returned tabId to other pwa-debug tools to target this tab specifically. Args: { url: non-empty string, active?, timeout_ms?, extension_id? }. With no extension_id, targets the single connected NMH. CALL host_status FIRST to confirm a connection.",
|
|
14954
|
+
inputSchema: newTabInputSchema,
|
|
14955
|
+
handler: newTabHandler,
|
|
14956
|
+
});
|
|
14957
|
+
|
|
14859
14958
|
const ACTION_IPC_TIMEOUT_MS = 5000;
|
|
14860
14959
|
// Locator + routing fields shared by every action tool.
|
|
14861
14960
|
const locatorSchema = {
|
|
@@ -15026,6 +15125,8 @@ const TOOLS = Object.freeze([
|
|
|
15026
15125
|
storageGetTool,
|
|
15027
15126
|
idbListTool,
|
|
15028
15127
|
idbQueryTool,
|
|
15128
|
+
navigateTool,
|
|
15129
|
+
newTabTool,
|
|
15029
15130
|
launchBrowserTool,
|
|
15030
15131
|
browserStatusTool,
|
|
15031
15132
|
closeBrowserTool,
|
|
@@ -176,6 +176,110 @@ const dispatchToTabClassified = async (tabId, req, opts = {}) => {
|
|
|
176
176
|
return { ok: true, response };
|
|
177
177
|
};
|
|
178
178
|
|
|
179
|
+
// SW-side navigation primitives — wraps chrome.tabs URL navigation so the request
|
|
180
|
+
// router stays a thin orchestrator. SW-handled (NOT page-world): chrome.tabs is
|
|
181
|
+
// only reachable in the extension service worker, and driving the tab to a new URL
|
|
182
|
+
// is a browser action, not an in-page read. This is the pwa-debug counterpart to
|
|
183
|
+
// chrome-devtools-mcp's navigate_page/new_page — it works through the loaded
|
|
184
|
+
// extension on the user's real profile, with no CDP attach required.
|
|
185
|
+
const DEFAULT_LOAD_TIMEOUT_MS = 10_000;
|
|
186
|
+
// A bare javascript: URL would run script in the target page context on navigate;
|
|
187
|
+
// reject it. Everything else (http(s), file:, about:, data:, chrome:, localhost
|
|
188
|
+
// shorthand) is allowed — this is a debugging driver, not a sandbox.
|
|
189
|
+
const JAVASCRIPT_SCHEME = /^javascript:/i;
|
|
190
|
+
const HAS_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
|
|
191
|
+
/**
|
|
192
|
+
* Normalize a caller-supplied URL: trim, reject empty / javascript:, and assume
|
|
193
|
+
* https:// when no scheme is given (so `example.com` works). Returns null when the
|
|
194
|
+
* input is unusable so the caller can surface a clear error.
|
|
195
|
+
*/
|
|
196
|
+
const normalizeUrl = (raw) => {
|
|
197
|
+
const trimmed = raw.trim();
|
|
198
|
+
if (trimmed.length === 0)
|
|
199
|
+
return null;
|
|
200
|
+
if (JAVASCRIPT_SCHEME.test(trimmed))
|
|
201
|
+
return null;
|
|
202
|
+
return HAS_SCHEME.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
203
|
+
};
|
|
204
|
+
/** Active tab id in the last-focused window, or undefined when there is none. */
|
|
205
|
+
const activeTabId = async () => {
|
|
206
|
+
const tabs = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
|
|
207
|
+
return tabs[0]?.id;
|
|
208
|
+
};
|
|
209
|
+
/**
|
|
210
|
+
* Resolve once the tab reports document 'complete', or 'timeout' after timeoutMs.
|
|
211
|
+
* The onUpdated listener is added BEFORE the caller triggers the load so the
|
|
212
|
+
* 'complete' event can't be missed; it is always removed and the timer cleared.
|
|
213
|
+
*/
|
|
214
|
+
const waitForComplete = (tabId, timeoutMs) => new Promise((resolve) => {
|
|
215
|
+
let settled = false;
|
|
216
|
+
const settle = (outcome) => {
|
|
217
|
+
if (settled)
|
|
218
|
+
return;
|
|
219
|
+
settled = true;
|
|
220
|
+
chrome.tabs.onUpdated.removeListener(onUpdated);
|
|
221
|
+
clearTimeout(timer);
|
|
222
|
+
resolve(outcome);
|
|
223
|
+
};
|
|
224
|
+
const onUpdated = (id, info) => {
|
|
225
|
+
if (id === tabId && info.status === 'complete')
|
|
226
|
+
settle('complete');
|
|
227
|
+
};
|
|
228
|
+
const timer = setTimeout(() => settle('timeout'), timeoutMs);
|
|
229
|
+
chrome.tabs.onUpdated.addListener(onUpdated);
|
|
230
|
+
});
|
|
231
|
+
/**
|
|
232
|
+
* Navigate the active (or given) tab to url and wait for it to finish loading.
|
|
233
|
+
* Throws on an invalid url or when no tab can be resolved. The listener is armed
|
|
234
|
+
* before chrome.tabs.update so the fresh load's 'complete' is observed.
|
|
235
|
+
*/
|
|
236
|
+
const navigateTab = async (input) => {
|
|
237
|
+
const url = normalizeUrl(input.url);
|
|
238
|
+
if (url === null) {
|
|
239
|
+
throw new Error(`navigate: invalid or unsupported url: ${JSON.stringify(input.url)}`);
|
|
240
|
+
}
|
|
241
|
+
const tabId = input.tabId ?? (await activeTabId());
|
|
242
|
+
if (tabId === undefined) {
|
|
243
|
+
throw new Error('navigate: no active tab (open a tab or pass tab_id)');
|
|
244
|
+
}
|
|
245
|
+
const pending = waitForComplete(tabId, input.timeoutMs ?? DEFAULT_LOAD_TIMEOUT_MS);
|
|
246
|
+
const tab = await chrome.tabs.update(tabId, { url });
|
|
247
|
+
const outcome = await pending;
|
|
248
|
+
return {
|
|
249
|
+
tabId,
|
|
250
|
+
url,
|
|
251
|
+
...(tab?.windowId !== undefined ? { windowId: tab.windowId } : {}),
|
|
252
|
+
status: outcome === 'complete' ? 'complete' : 'loading',
|
|
253
|
+
};
|
|
254
|
+
};
|
|
255
|
+
/**
|
|
256
|
+
* Open a new tab at url and wait for it to finish loading. active defaults to
|
|
257
|
+
* Chrome's behavior (foreground). Throws on an invalid url or when Chrome returns
|
|
258
|
+
* no tab id.
|
|
259
|
+
*/
|
|
260
|
+
const openNewTab = async (input) => {
|
|
261
|
+
const url = normalizeUrl(input.url);
|
|
262
|
+
if (url === null) {
|
|
263
|
+
throw new Error(`new_tab: invalid or unsupported url: ${JSON.stringify(input.url)}`);
|
|
264
|
+
}
|
|
265
|
+
const tab = await chrome.tabs.create({
|
|
266
|
+
url,
|
|
267
|
+
...(input.active !== undefined ? { active: input.active } : {}),
|
|
268
|
+
});
|
|
269
|
+
const tabId = tab.id;
|
|
270
|
+
if (tabId === undefined) {
|
|
271
|
+
throw new Error('new_tab: chrome did not return a tab id');
|
|
272
|
+
}
|
|
273
|
+
const outcome = await waitForComplete(tabId, input.timeoutMs ?? DEFAULT_LOAD_TIMEOUT_MS);
|
|
274
|
+
return {
|
|
275
|
+
tabId,
|
|
276
|
+
url,
|
|
277
|
+
...(tab.windowId !== undefined ? { windowId: tab.windowId } : {}),
|
|
278
|
+
status: outcome === 'complete' ? 'complete' : 'loading',
|
|
279
|
+
created: true,
|
|
280
|
+
};
|
|
281
|
+
};
|
|
282
|
+
|
|
179
283
|
const compilePatternList = (sources, fieldPathPrefix) => {
|
|
180
284
|
if (sources === undefined || sources.length === 0) {
|
|
181
285
|
return { ok: true, value: [] };
|
|
@@ -1610,6 +1714,50 @@ const handleSessionRecord = async (env) => {
|
|
|
1610
1714
|
}
|
|
1611
1715
|
return response.payload;
|
|
1612
1716
|
};
|
|
1717
|
+
// --- Navigation tools (pdl_navigate / pdl_new_tab) ---------------------------
|
|
1718
|
+
// SW-handled, NOT page-world: chrome.tabs drives the browser to a URL. Forwarded
|
|
1719
|
+
// to the sw_navigation module, which validates the URL and waits for load.
|
|
1720
|
+
const readTimeoutMs = (r) => typeof r['timeout_ms'] === 'number' &&
|
|
1721
|
+
Number.isInteger(r['timeout_ms']) &&
|
|
1722
|
+
r['timeout_ms'] > 0
|
|
1723
|
+
? r['timeout_ms']
|
|
1724
|
+
: undefined;
|
|
1725
|
+
const handleNavigate = async (env) => {
|
|
1726
|
+
const raw = env.payload;
|
|
1727
|
+
if (raw === null || typeof raw !== 'object') {
|
|
1728
|
+
throw new Error('pdl_navigate: payload must be { url: non-empty string, tab_id?, timeout_ms? }');
|
|
1729
|
+
}
|
|
1730
|
+
const r = raw;
|
|
1731
|
+
const url = r['url'];
|
|
1732
|
+
if (typeof url !== 'string' || url.length === 0) {
|
|
1733
|
+
throw new Error('pdl_navigate: payload must include { url: non-empty string }');
|
|
1734
|
+
}
|
|
1735
|
+
const tabId = readTabId(raw);
|
|
1736
|
+
const timeoutMs = readTimeoutMs(r);
|
|
1737
|
+
return navigateTab({
|
|
1738
|
+
url,
|
|
1739
|
+
...(tabId !== undefined ? { tabId } : {}),
|
|
1740
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
1741
|
+
});
|
|
1742
|
+
};
|
|
1743
|
+
const handleNewTab = async (env) => {
|
|
1744
|
+
const raw = env.payload;
|
|
1745
|
+
if (raw === null || typeof raw !== 'object') {
|
|
1746
|
+
throw new Error('pdl_new_tab: payload must be { url: non-empty string, active?, timeout_ms? }');
|
|
1747
|
+
}
|
|
1748
|
+
const r = raw;
|
|
1749
|
+
const url = r['url'];
|
|
1750
|
+
if (typeof url !== 'string' || url.length === 0) {
|
|
1751
|
+
throw new Error('pdl_new_tab: payload must include { url: non-empty string }');
|
|
1752
|
+
}
|
|
1753
|
+
const active = typeof r['active'] === 'boolean' ? r['active'] : undefined;
|
|
1754
|
+
const timeoutMs = readTimeoutMs(r);
|
|
1755
|
+
return openNewTab({
|
|
1756
|
+
url,
|
|
1757
|
+
...(active !== undefined ? { active } : {}),
|
|
1758
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
1759
|
+
});
|
|
1760
|
+
};
|
|
1613
1761
|
// --- Path 7 interaction action tools (pdl_*) ---------------------------------
|
|
1614
1762
|
// One generic handler per ACTION_TOOL_SPECS entry: extract tab_id for routing,
|
|
1615
1763
|
// forward the locator + params payload to the page-world unchanged.
|
|
@@ -1643,6 +1791,8 @@ const makeActionRequestHandler = (tool) => async (env) => {
|
|
|
1643
1791
|
const actionRequestHandlers = Object.freeze(Object.fromEntries(ACTION_TOOL_SPECS.map((s) => [s.tool, makeActionRequestHandler(s.tool)])));
|
|
1644
1792
|
const HANDLERS = Object.freeze({
|
|
1645
1793
|
...actionRequestHandlers,
|
|
1794
|
+
pdl_navigate: handleNavigate,
|
|
1795
|
+
pdl_new_tab: handleNewTab,
|
|
1646
1796
|
session_ping: handleSessionPing,
|
|
1647
1797
|
recent_events: handleRecentEvents,
|
|
1648
1798
|
evaluate: handleEvaluate,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aryanduntley/pwa-debug",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
"@types/winreg": "^1.2.36",
|
|
64
64
|
"rollup": "^4.27.4",
|
|
65
65
|
"tslib": "^2.8.1",
|
|
66
|
-
"@pwa-debug/extension": "0.1.
|
|
67
|
-
"@pwa-debug/shared": "0.1.
|
|
66
|
+
"@pwa-debug/extension": "0.1.8",
|
|
67
|
+
"@pwa-debug/shared": "0.1.8"
|
|
68
68
|
},
|
|
69
69
|
"scripts": {
|
|
70
70
|
"typecheck": "tsc --noEmit",
|