@foldspace_npm/harness 0.1.2 → 0.1.4
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 +67 -16
- package/bin/attach.mjs +779 -107
- package/bin/cli.mjs +129 -44
- package/bin/deploy.mjs +37 -33
- package/bin/inject.mjs +64 -295
- package/package.json +5 -2
- package/src/action-observer.mjs +271 -0
- package/src/attach-helpers.mjs +162 -0
- package/src/attach-preflight.mjs +332 -0
- package/src/bootstrap-script.mjs +120 -0
- package/src/cdp-request-manager.mjs +61 -0
- package/src/cli-help.mjs +147 -0
- package/src/cli-registry.mjs +310 -0
- package/src/diagnostics.mjs +482 -0
- package/src/init.mjs +20 -9
- package/src/project-config.mjs +37 -0
- package/src/protocol.mjs +181 -0
- package/src/session-summary.mjs +133 -0
- package/templates/agent-starter/CLAUDE.md +31 -8
- package/templates/agent-starter/README.md +26 -3
- package/templates/agent-starter/foldspace.dev.json +1 -3
- package/bin/buildExtension.mjs +0 -90
- package/bin/packageExtension.mjs +0 -29
package/bin/attach.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Attach to the Chrome launched by inject.mjs and serve local actions to it.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* Three explicit page modes:
|
|
6
6
|
*
|
|
7
7
|
* swap (default) The client app already embeds Foldspace. We
|
|
8
8
|
* intercept the remote-actions bundle request and fulfill it
|
|
@@ -11,7 +11,11 @@
|
|
|
11
11
|
* page's own agent instead of creating a second one.
|
|
12
12
|
*
|
|
13
13
|
* --bootstrap The app has no Foldspace. Inject the SDK bootstrap from the
|
|
14
|
-
*
|
|
14
|
+
* harness bootstrap module before page scripts run.
|
|
15
|
+
*
|
|
16
|
+
* --replace The app embeds a different Foldspace product or agent.
|
|
17
|
+
* Rebind the page bootstrap to the configured product and
|
|
18
|
+
* make the configured agent the sole visible widget owner.
|
|
15
19
|
*
|
|
16
20
|
* Chrome 136+ removed --load-extension and does not run content scripts for
|
|
17
21
|
* CDP-loaded extensions, so neither path uses the extension at runtime.
|
|
@@ -19,31 +23,85 @@
|
|
|
19
23
|
import fs from "fs";
|
|
20
24
|
import path from "path";
|
|
21
25
|
import { fileURLToPath } from "url";
|
|
26
|
+
import {
|
|
27
|
+
ATTACH_MODES,
|
|
28
|
+
attachModeFromArgs,
|
|
29
|
+
characterizePageAgents,
|
|
30
|
+
hostMatches,
|
|
31
|
+
hostPatternsFromTarget,
|
|
32
|
+
parseAgentId,
|
|
33
|
+
} from "../src/attach-helpers.mjs";
|
|
34
|
+
import {
|
|
35
|
+
buildReplacePrelude,
|
|
36
|
+
detectedProductIds,
|
|
37
|
+
guardScriptForHosts,
|
|
38
|
+
guardAttachMode,
|
|
39
|
+
isSdkScriptRequest,
|
|
40
|
+
rewriteSdkRequestKey,
|
|
41
|
+
shouldFulfillActionRequest,
|
|
42
|
+
} from "../src/attach-preflight.mjs";
|
|
43
|
+
import { serializeDiagnostic } from "../src/diagnostics.mjs";
|
|
44
|
+
import {
|
|
45
|
+
ERROR_CODES,
|
|
46
|
+
createLifecycleResult,
|
|
47
|
+
} from "../src/protocol.mjs";
|
|
48
|
+
import { createCdpRequestManager } from "../src/cdp-request-manager.mjs";
|
|
49
|
+
import { buildActionObserverScript } from "../src/action-observer.mjs";
|
|
50
|
+
import { buildBootstrapScript } from "../src/bootstrap-script.mjs";
|
|
51
|
+
import {
|
|
52
|
+
createSessionCollector,
|
|
53
|
+
formatActionObservation,
|
|
54
|
+
formatSessionSummary,
|
|
55
|
+
} from "../src/session-summary.mjs";
|
|
22
56
|
|
|
23
57
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
24
58
|
// Resolve the CONSUMING repo, not this package. FOLDSPACE_PROJECT_DIR lets a
|
|
25
59
|
// hosted builder point the harness at a workspace it controls.
|
|
26
60
|
const root = process.env.FOLDSPACE_PROJECT_DIR || process.cwd();
|
|
27
|
-
const extDir = path.join(root, ".foldspace-dev", "extension");
|
|
28
61
|
const bundlePath = path.join(root, "dist", "index.js");
|
|
29
62
|
|
|
30
63
|
// Prefer what inject actually launched, then an explicit override, then the
|
|
31
64
|
// legacy default. Guessing here means attaching to the wrong browser.
|
|
32
|
-
function
|
|
65
|
+
function readDevState() {
|
|
33
66
|
try {
|
|
34
|
-
return JSON.parse(
|
|
67
|
+
return JSON.parse(
|
|
68
|
+
fs.readFileSync(
|
|
69
|
+
path.join(root, ".foldspace-dev", "state.json"),
|
|
70
|
+
"utf8",
|
|
71
|
+
),
|
|
72
|
+
);
|
|
35
73
|
} catch {
|
|
36
|
-
return
|
|
74
|
+
return {};
|
|
37
75
|
}
|
|
38
76
|
}
|
|
77
|
+
const devState = readDevState();
|
|
39
78
|
const portArgIndex = process.argv.indexOf("--port");
|
|
79
|
+
const targetArgIndex = process.argv.indexOf("--target");
|
|
80
|
+
const explicitPort =
|
|
81
|
+
portArgIndex > -1 ? process.argv[portArgIndex + 1] : null;
|
|
82
|
+
const explicitTarget =
|
|
83
|
+
targetArgIndex > -1 ? process.argv[targetArgIndex + 1] : null;
|
|
84
|
+
const savedLaunchMatches =
|
|
85
|
+
!explicitTarget || explicitTarget === devState.target;
|
|
86
|
+
const useSavedLaunch =
|
|
87
|
+
!explicitPort &&
|
|
88
|
+
!process.env.CDP_PORT &&
|
|
89
|
+
savedLaunchMatches;
|
|
40
90
|
const port = String(
|
|
41
|
-
|
|
91
|
+
explicitPort ||
|
|
42
92
|
process.env.CDP_PORT ||
|
|
43
|
-
|
|
93
|
+
(useSavedLaunch ? devState.debugPort : null) ||
|
|
44
94
|
"9222",
|
|
45
95
|
);
|
|
46
|
-
|
|
96
|
+
let attachMode;
|
|
97
|
+
try {
|
|
98
|
+
attachMode = attachModeFromArgs(process.argv.slice(2));
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error(`attach: ${error instanceof Error ? error.message : String(error)}`);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
const bootstrap = attachMode === ATTACH_MODES.BOOTSTRAP;
|
|
104
|
+
const replace = attachMode === ATTACH_MODES.REPLACE;
|
|
47
105
|
// Test mode keeps conversations out of the customer's default list. Turn it off
|
|
48
106
|
// only when you WANT the conversations and action calls to show up in the
|
|
49
107
|
// Foldspace dashboard — a brand-new agent during initial setup, where there is
|
|
@@ -53,18 +111,35 @@ const noTestMode = process.argv.includes("--no-test-mode");
|
|
|
53
111
|
// string that asserts you are safe while you are not. It now states what is
|
|
54
112
|
// actually true. --no-badge drops it entirely, for recording a demo.
|
|
55
113
|
const noBadge = process.argv.includes("--no-badge");
|
|
56
|
-
const badgeText = noTestMode
|
|
57
|
-
? "FOLDSPACE DEV \u00b7 local actions \u00b7 LIVE"
|
|
58
|
-
: "FOLDSPACE DEV \u00b7 local actions \u00b7 TEST MODE";
|
|
59
|
-
|
|
60
114
|
// Agent api name for setTestMode — from the same config inject.mjs used.
|
|
61
115
|
const cfg = JSON.parse(fs.readFileSync(path.join(root, "foldspace.dev.json"), "utf8"));
|
|
62
|
-
const
|
|
116
|
+
const targetName =
|
|
117
|
+
explicitTarget ||
|
|
118
|
+
(useSavedLaunch ? devState.target : null) ||
|
|
119
|
+
cfg.defaultTarget;
|
|
120
|
+
const configuredTarget = cfg.targets[targetName] || {};
|
|
121
|
+
const cfgTarget =
|
|
122
|
+
!explicitTarget &&
|
|
123
|
+
useSavedLaunch &&
|
|
124
|
+
devState.target === targetName &&
|
|
125
|
+
devState.resolvedTarget &&
|
|
126
|
+
typeof devState.resolvedTarget === "object"
|
|
127
|
+
? { ...configuredTarget, ...devState.resolvedTarget }
|
|
128
|
+
: configuredTarget;
|
|
63
129
|
const agentApiName =
|
|
64
|
-
process.env.AGENT_API_NAME ||
|
|
65
130
|
(process.argv.includes("--agent")
|
|
66
131
|
? process.argv[process.argv.indexOf("--agent") + 1]
|
|
67
|
-
:
|
|
132
|
+
: null) ||
|
|
133
|
+
process.env.AGENT_API_NAME ||
|
|
134
|
+
cfgTarget.agentApiName;
|
|
135
|
+
const productId = cfgTarget.productId;
|
|
136
|
+
const agentMode = String(cfgTarget.mode || "OVERLAY").toUpperCase();
|
|
137
|
+
const agentKey = cfgTarget.overrideKey || `EU-${productId}-1-1`;
|
|
138
|
+
const sdkUrl = cfgTarget.sdkUrl || cfg.sdkUrl;
|
|
139
|
+
const actionTarget = { productId, agentApiName };
|
|
140
|
+
const badgeText = noTestMode
|
|
141
|
+
? `FOLDSPACE DEV \u00b7 ${agentApiName} \u00b7 ${attachMode} \u00b7 LIVE`
|
|
142
|
+
: `FOLDSPACE DEV \u00b7 ${agentApiName} \u00b7 ${attachMode} \u00b7 TEST MODE`;
|
|
68
143
|
|
|
69
144
|
// Refuse to run against an uninitialised template. Without this, attach injects
|
|
70
145
|
// the literal placeholder as an agent api name and the page fails with an opaque
|
|
@@ -77,26 +152,38 @@ const unresolved = PLACEHOLDERS.filter((ph) =>
|
|
|
77
152
|
if (unresolved.length) {
|
|
78
153
|
console.error(
|
|
79
154
|
`attach: foldspace.dev.json still contains placeholders: ${unresolved.join(", ")}\n` +
|
|
80
|
-
` Run "
|
|
155
|
+
` Run "foldspace init" first, or pass --agent <apiName>.\n` +
|
|
81
156
|
` Refusing to run — injecting a placeholder breaks the app's agent.`
|
|
82
157
|
);
|
|
83
158
|
process.exit(1);
|
|
84
159
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
160
|
+
if (!productId || !agentApiName) {
|
|
161
|
+
console.error("attach: foldspace.dev.json must define productId and agentApiName.");
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
if (bootstrap && (!sdkUrl || typeof sdkUrl !== "string")) {
|
|
165
|
+
console.error("attach: foldspace.dev.json must define sdkUrl for --bootstrap.");
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
if (!fs.existsSync(bundlePath)) {
|
|
169
|
+
console.error(`attach: ${path.relative(root, bundlePath)} is missing — run "npm run build" first.`);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
const hostPatterns = hostPatternsFromTarget(cfgTarget);
|
|
173
|
+
if (!hostPatterns.length) {
|
|
174
|
+
console.error(
|
|
175
|
+
`attach: target "${targetName}" must define hosts or a valid startUrl.`,
|
|
95
176
|
);
|
|
96
|
-
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
97
179
|
|
|
98
|
-
//
|
|
180
|
+
// CDP patterns are intentionally broad; the paused-request handler only
|
|
181
|
+
// fulfills the configured product/agent and continues every unrelated request.
|
|
99
182
|
const ACTIONS_PATTERN = "*/agent/actions/*";
|
|
183
|
+
const SDK_PATTERNS = [
|
|
184
|
+
"*://*/*foldspace.js*",
|
|
185
|
+
"*://*/*eucera.js*",
|
|
186
|
+
];
|
|
100
187
|
|
|
101
188
|
const badgeSrc = `(() => {
|
|
102
189
|
if (window.top !== window.self) return;
|
|
@@ -202,59 +289,438 @@ const testModeSrc = `(() => {
|
|
|
202
289
|
}, 50);
|
|
203
290
|
})();`;
|
|
204
291
|
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
292
|
+
const cdpRequests = createCdpRequestManager();
|
|
293
|
+
const sessionCollector = createSessionCollector();
|
|
294
|
+
// sessionId -> preparation state used for deterministic detach cleanup.
|
|
295
|
+
const prepared = new Map();
|
|
296
|
+
const preparing = new Set();
|
|
297
|
+
// Rejections are scoped to the observed URL so a later navigation can retry.
|
|
298
|
+
const rejected = new Map();
|
|
208
299
|
// targetId -> sessionId, so a target that navigates INTO a matching host can be
|
|
209
300
|
// prepared later. Without this, opening Chrome on a new tab and then browsing
|
|
210
301
|
// to the app never arms the swap.
|
|
211
302
|
const sessions = new Map();
|
|
303
|
+
const sessionUrls = new Map();
|
|
212
304
|
let served = 0;
|
|
305
|
+
let shuttingDown = false;
|
|
306
|
+
let reconnecting = false;
|
|
307
|
+
let summaryPrinted = false;
|
|
308
|
+
const call = cdpRequests.call;
|
|
309
|
+
|
|
310
|
+
function logLifecycle(result) {
|
|
311
|
+
const detail = result.ok
|
|
312
|
+
? `${result.operation}:${result.state}`
|
|
313
|
+
: `${result.operation}:${result.state} ${result.error.code}`;
|
|
314
|
+
console.log(` [lifecycle] ${detail}`);
|
|
315
|
+
if (!result.ok && result.error?.message) {
|
|
316
|
+
console.log(` ${result.error.message}`);
|
|
317
|
+
}
|
|
318
|
+
if (!result.ok && result.details?.suggestion) {
|
|
319
|
+
console.log(` Next: ${result.details.suggestion}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function runDiagnostic(ws, sessionId, name, args = {}) {
|
|
324
|
+
const evaluation = await call(
|
|
325
|
+
ws,
|
|
326
|
+
"Runtime.evaluate",
|
|
327
|
+
{
|
|
328
|
+
expression: serializeDiagnostic(name, args),
|
|
329
|
+
awaitPromise: true,
|
|
330
|
+
returnByValue: true,
|
|
331
|
+
},
|
|
332
|
+
sessionId,
|
|
333
|
+
);
|
|
334
|
+
if (evaluation?.exceptionDetails) {
|
|
335
|
+
throw new Error(
|
|
336
|
+
`Diagnostic ${name} failed in the page: ${
|
|
337
|
+
evaluation.exceptionDetails.text || "Unknown evaluation error"
|
|
338
|
+
}`,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
return evaluation?.result?.value || null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function inspectPage(ws, sessionId) {
|
|
345
|
+
let state = null;
|
|
346
|
+
let stableNoSdkObservations = 0;
|
|
347
|
+
for (let attempt = 0; attempt < 40; attempt++) {
|
|
348
|
+
try {
|
|
349
|
+
state = await runDiagnostic(ws, sessionId, "inspectSdkState", {
|
|
350
|
+
expectedApiName: agentApiName,
|
|
351
|
+
expectedMode: agentMode,
|
|
352
|
+
});
|
|
353
|
+
} catch {
|
|
354
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const ready =
|
|
359
|
+
state?.documentReadyState === "interactive" ||
|
|
360
|
+
state?.documentReadyState === "complete";
|
|
361
|
+
const onConfiguredHost =
|
|
362
|
+
!state?.pageUrl || hostMatches(state.pageUrl, hostPatterns);
|
|
363
|
+
if (!ready || !onConfiguredHost) {
|
|
364
|
+
stableNoSdkObservations = 0;
|
|
365
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (state.sdkPresent) {
|
|
370
|
+
if (
|
|
371
|
+
attachMode !== ATTACH_MODES.SWAP ||
|
|
372
|
+
(state.sdkReady && state.agentIds.length > 0)
|
|
373
|
+
) {
|
|
374
|
+
return state;
|
|
375
|
+
}
|
|
376
|
+
stableNoSdkObservations = 0;
|
|
377
|
+
} else if (attachMode === ATTACH_MODES.BOOTSTRAP) {
|
|
378
|
+
stableNoSdkObservations += 1;
|
|
379
|
+
if (stableNoSdkObservations >= 5) return state;
|
|
380
|
+
}
|
|
381
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
382
|
+
}
|
|
383
|
+
return state;
|
|
384
|
+
}
|
|
213
385
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
386
|
+
async function verifyPreparedPage(ws, sessionId) {
|
|
387
|
+
const latest = {
|
|
388
|
+
state: null,
|
|
389
|
+
characterization: null,
|
|
390
|
+
products: [],
|
|
391
|
+
widget: null,
|
|
392
|
+
registration: null,
|
|
393
|
+
observation: null,
|
|
394
|
+
diagnosticError: null,
|
|
395
|
+
};
|
|
396
|
+
for (let attempt = 0; attempt < 40; attempt++) {
|
|
397
|
+
try {
|
|
398
|
+
const state = await runDiagnostic(ws, sessionId, "inspectSdkState", {
|
|
399
|
+
expectedApiName: agentApiName,
|
|
400
|
+
expectedMode: agentMode,
|
|
401
|
+
});
|
|
402
|
+
const characterization = characterizePageAgents({
|
|
403
|
+
sdkPresent: state?.sdkPresent === true,
|
|
404
|
+
agentIds: state?.agentIds || [],
|
|
405
|
+
expectedApiName: agentApiName,
|
|
406
|
+
expectedMode: agentMode,
|
|
407
|
+
});
|
|
408
|
+
const products = detectedProductIds(state);
|
|
409
|
+
latest.state = state;
|
|
410
|
+
latest.characterization = characterization;
|
|
411
|
+
latest.products = products;
|
|
412
|
+
const productMatches = products.includes(productId);
|
|
413
|
+
const widget = await runDiagnostic(
|
|
414
|
+
ws,
|
|
415
|
+
sessionId,
|
|
416
|
+
"verifyWidgetVisible",
|
|
417
|
+
{ ownerAgentId: characterization.expectedAgentId },
|
|
418
|
+
);
|
|
419
|
+
latest.widget = widget;
|
|
420
|
+
const visibilityMatches =
|
|
421
|
+
attachMode !== ATTACH_MODES.REPLACE ||
|
|
422
|
+
(widget?.soleVisible === true && widget?.ownerMatches === true);
|
|
423
|
+
if (
|
|
424
|
+
characterization.status === "same-agent" &&
|
|
425
|
+
productMatches &&
|
|
426
|
+
visibilityMatches
|
|
427
|
+
) {
|
|
428
|
+
const observation = await runDiagnostic(
|
|
429
|
+
ws,
|
|
430
|
+
sessionId,
|
|
431
|
+
"inspectActionObservation",
|
|
432
|
+
{ ownerAgentId: characterization.expectedAgentId },
|
|
433
|
+
);
|
|
434
|
+
latest.observation = observation;
|
|
435
|
+
const expectedActionNames =
|
|
436
|
+
observation?.installed &&
|
|
437
|
+
observation?.ownerMatches &&
|
|
438
|
+
!observation?.actionNameLimitExceeded &&
|
|
439
|
+
Array.isArray(observation.expectedActionNames)
|
|
440
|
+
? observation.expectedActionNames
|
|
441
|
+
: [];
|
|
442
|
+
const registration = await runDiagnostic(
|
|
443
|
+
ws,
|
|
444
|
+
sessionId,
|
|
445
|
+
"inspectRegistration",
|
|
446
|
+
{
|
|
447
|
+
apiName: agentApiName,
|
|
448
|
+
mode: agentMode,
|
|
449
|
+
ownerAgentId: characterization.expectedAgentId,
|
|
450
|
+
expectedActionNames,
|
|
451
|
+
},
|
|
452
|
+
);
|
|
453
|
+
latest.registration = registration;
|
|
454
|
+
if (
|
|
455
|
+
observation?.captureCount > 0 &&
|
|
456
|
+
expectedActionNames.length > 0 &&
|
|
457
|
+
registration?.agentFound &&
|
|
458
|
+
registration.missingActionNames?.length === 0 &&
|
|
459
|
+
registration.unexpectedActionNames?.length === 0
|
|
460
|
+
) {
|
|
461
|
+
return {
|
|
462
|
+
ok: true,
|
|
463
|
+
state,
|
|
464
|
+
characterization,
|
|
465
|
+
products,
|
|
466
|
+
widget,
|
|
467
|
+
registration,
|
|
468
|
+
observation,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
} catch (error) {
|
|
473
|
+
// Reload briefly destroys the execution context. Retry until the page and
|
|
474
|
+
// SDK settle instead of treating that expected transition as a mismatch.
|
|
475
|
+
latest.diagnosticError =
|
|
476
|
+
error instanceof Error ? error.message.slice(0, 500) : String(error);
|
|
477
|
+
}
|
|
478
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
479
|
+
}
|
|
480
|
+
const state = await inspectPage(ws, sessionId);
|
|
481
|
+
latest.state = state;
|
|
482
|
+
latest.characterization = characterizePageAgents({
|
|
483
|
+
sdkPresent: state?.sdkPresent === true,
|
|
484
|
+
agentIds: state?.agentIds || [],
|
|
485
|
+
expectedApiName: agentApiName,
|
|
486
|
+
expectedMode: agentMode,
|
|
219
487
|
});
|
|
488
|
+
latest.products = detectedProductIds(state);
|
|
489
|
+
return {
|
|
490
|
+
ok: false,
|
|
491
|
+
...latest,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
220
494
|
|
|
221
495
|
async function prepare(ws, sessionId, url) {
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
await call(ws, "Page.enable", {}, sessionId);
|
|
226
|
-
await call(ws, "Runtime.enable", {}, sessionId);
|
|
227
|
-
await call(ws, "Page.setBypassCSP", { enabled: true }, sessionId);
|
|
228
|
-
|
|
229
|
-
// Workers spawned by this page attach through THIS session, not the browser
|
|
230
|
-
// one, so the browser-level setAutoAttach does not cover them and they start
|
|
231
|
-
// paused. Say so here too, or the app's workers freeze for the session.
|
|
232
|
-
await call(ws, "Target.setAutoAttach", {
|
|
233
|
-
autoAttach: true, waitForDebuggerOnStart: false, flatten: true,
|
|
234
|
-
}, sessionId);
|
|
235
|
-
|
|
236
|
-
await call(ws, "Fetch.enable", {
|
|
237
|
-
patterns: [{ urlPattern: ACTIONS_PATTERN, requestStage: "Request" }],
|
|
238
|
-
}, sessionId);
|
|
239
|
-
|
|
240
|
-
// The action bundle, injected rather than fetched. This is what lets the
|
|
241
|
-
// harness drop the dev server — and what makes a non-local browser possible,
|
|
242
|
-
// since a remote Chrome can never reach localhost.
|
|
243
|
-
const scripts = [];
|
|
244
|
-
if (fs.existsSync(bundlePath)) {
|
|
245
|
-
scripts.push(fs.readFileSync(bundlePath, "utf8"));
|
|
246
|
-
} else {
|
|
247
|
-
console.log(` !! ${path.relative(root, bundlePath)} missing — run "npm run dev"`);
|
|
496
|
+
if (rejected.has(sessionId) && rejected.get(sessionId) !== url) {
|
|
497
|
+
rejected.delete(sessionId);
|
|
248
498
|
}
|
|
249
|
-
if (
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
499
|
+
if (
|
|
500
|
+
prepared.has(sessionId) ||
|
|
501
|
+
preparing.has(sessionId) ||
|
|
502
|
+
rejected.has(sessionId)
|
|
503
|
+
) {
|
|
504
|
+
return;
|
|
254
505
|
}
|
|
506
|
+
preparing.add(sessionId);
|
|
507
|
+
|
|
508
|
+
try {
|
|
509
|
+
await call(ws, "Page.enable", {}, sessionId);
|
|
510
|
+
await call(ws, "Runtime.enable", {}, sessionId);
|
|
511
|
+
|
|
512
|
+
const pageState = await inspectPage(ws, sessionId);
|
|
513
|
+
if (!pageState?.pageUrl || !hostMatches(pageState.pageUrl, hostPatterns)) {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const characterization = characterizePageAgents({
|
|
517
|
+
sdkPresent: pageState?.sdkPresent === true,
|
|
518
|
+
agentIds: pageState?.agentIds || [],
|
|
519
|
+
expectedApiName: agentApiName,
|
|
520
|
+
expectedMode: agentMode,
|
|
521
|
+
});
|
|
522
|
+
const pageProductIds = detectedProductIds(pageState);
|
|
523
|
+
const guard = guardAttachMode({
|
|
524
|
+
mode: attachMode,
|
|
525
|
+
characterization,
|
|
526
|
+
expectedProductId: productId,
|
|
527
|
+
pageProductIds,
|
|
528
|
+
});
|
|
529
|
+
if (!guard.ok) {
|
|
530
|
+
rejected.set(sessionId, url);
|
|
531
|
+
logLifecycle(
|
|
532
|
+
createLifecycleResult({
|
|
533
|
+
operation: "prepare_page",
|
|
534
|
+
ok: false,
|
|
535
|
+
state: "rejected",
|
|
536
|
+
error: { code: guard.code, message: guard.message },
|
|
537
|
+
details: {
|
|
538
|
+
url,
|
|
539
|
+
mode: attachMode,
|
|
540
|
+
expectedAgentId: characterization.expectedAgentId,
|
|
541
|
+
detectedAgentIds: pageState?.agentIds || [],
|
|
542
|
+
expectedProductId: productId,
|
|
543
|
+
detectedProductIds: pageProductIds,
|
|
544
|
+
suggestion: guard.suggestion || "",
|
|
545
|
+
},
|
|
546
|
+
}),
|
|
547
|
+
);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
logLifecycle(
|
|
552
|
+
createLifecycleResult({
|
|
553
|
+
operation: "prepare_page",
|
|
554
|
+
ok: true,
|
|
555
|
+
state: "preflight_ok",
|
|
556
|
+
details: {
|
|
557
|
+
url,
|
|
558
|
+
mode: attachMode,
|
|
559
|
+
expectedAgentId: characterization.expectedAgentId,
|
|
560
|
+
detectedAgentIds: pageState?.agentIds || [],
|
|
561
|
+
expectedProductId: productId,
|
|
562
|
+
detectedProductIds: pageProductIds,
|
|
563
|
+
},
|
|
564
|
+
}),
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
await call(ws, "Page.setBypassCSP", { enabled: true }, sessionId);
|
|
568
|
+
|
|
569
|
+
// Workers spawned by this page attach through THIS session, not the browser
|
|
570
|
+
// one, so the browser-level setAutoAttach does not cover them and they start
|
|
571
|
+
// paused. Say so here too, or the app's workers freeze for the session.
|
|
572
|
+
await call(ws, "Target.setAutoAttach", {
|
|
573
|
+
autoAttach: true, waitForDebuggerOnStart: false, flatten: true,
|
|
574
|
+
}, sessionId);
|
|
575
|
+
|
|
576
|
+
const fetchPatterns = [
|
|
577
|
+
{ urlPattern: ACTIONS_PATTERN, requestStage: "Request" },
|
|
578
|
+
...(replace
|
|
579
|
+
? SDK_PATTERNS.map((urlPattern) => ({
|
|
580
|
+
urlPattern,
|
|
581
|
+
requestStage: "Request",
|
|
582
|
+
}))
|
|
583
|
+
: []),
|
|
584
|
+
];
|
|
585
|
+
await call(ws, "Fetch.enable", { patterns: fetchPatterns }, sessionId);
|
|
255
586
|
|
|
256
|
-
|
|
257
|
-
|
|
587
|
+
// The action bundle, injected rather than fetched. This is what lets the
|
|
588
|
+
// harness drop the dev server — and what makes a non-local browser possible,
|
|
589
|
+
// since a remote Chrome can never reach localhost.
|
|
590
|
+
const scriptIds = [];
|
|
591
|
+
const observerResult = await call(
|
|
592
|
+
ws,
|
|
593
|
+
"Page.addScriptToEvaluateOnNewDocument",
|
|
594
|
+
{
|
|
595
|
+
source: guardScriptForHosts(
|
|
596
|
+
buildActionObserverScript({
|
|
597
|
+
agentApiName,
|
|
598
|
+
mode: agentMode,
|
|
599
|
+
namespace: pageState?.namespace || "foldspace",
|
|
600
|
+
}),
|
|
601
|
+
hostPatterns,
|
|
602
|
+
),
|
|
603
|
+
},
|
|
604
|
+
sessionId,
|
|
605
|
+
);
|
|
606
|
+
if (observerResult?.identifier) {
|
|
607
|
+
scriptIds.push(observerResult.identifier);
|
|
608
|
+
}
|
|
609
|
+
if (replace) {
|
|
610
|
+
const aliases = characterization.agents
|
|
611
|
+
.map((agent) => agent.apiName)
|
|
612
|
+
.filter((apiName) => apiName !== agentApiName);
|
|
613
|
+
const result = await call(
|
|
614
|
+
ws,
|
|
615
|
+
"Page.addScriptToEvaluateOnNewDocument",
|
|
616
|
+
{
|
|
617
|
+
source: guardScriptForHosts(
|
|
618
|
+
buildReplacePrelude({
|
|
619
|
+
agentKey,
|
|
620
|
+
agentApiName,
|
|
621
|
+
mode: agentMode,
|
|
622
|
+
namespace: pageState?.namespace || "foldspace",
|
|
623
|
+
aliases,
|
|
624
|
+
}),
|
|
625
|
+
hostPatterns,
|
|
626
|
+
),
|
|
627
|
+
},
|
|
628
|
+
sessionId,
|
|
629
|
+
);
|
|
630
|
+
if (result?.identifier) scriptIds.push(result.identifier);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const scripts = [];
|
|
634
|
+
scripts.push(fs.readFileSync(bundlePath, "utf8"));
|
|
635
|
+
if (!noBadge) scripts.push(badgeSrc);
|
|
636
|
+
if (!noTestMode) scripts.push(testModeSrc);
|
|
637
|
+
if (bootstrap) {
|
|
638
|
+
scripts.push(
|
|
639
|
+
buildBootstrapScript({
|
|
640
|
+
sdkUrl,
|
|
641
|
+
productId,
|
|
642
|
+
agentApiName,
|
|
643
|
+
mode: agentMode,
|
|
644
|
+
overrideKey: cfgTarget.overrideKey || null,
|
|
645
|
+
namespace: pageState?.namespace || "foldspace",
|
|
646
|
+
}),
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
for (const source of scripts) {
|
|
650
|
+
const result = await call(
|
|
651
|
+
ws,
|
|
652
|
+
"Page.addScriptToEvaluateOnNewDocument",
|
|
653
|
+
{
|
|
654
|
+
source: guardScriptForHosts(source, hostPatterns),
|
|
655
|
+
runImmediately: true,
|
|
656
|
+
},
|
|
657
|
+
sessionId,
|
|
658
|
+
);
|
|
659
|
+
if (result?.identifier) scriptIds.push(result.identifier);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
prepared.set(sessionId, {
|
|
663
|
+
scriptIds,
|
|
664
|
+
mode: attachMode,
|
|
665
|
+
url,
|
|
666
|
+
observing: true,
|
|
667
|
+
fetchEnabled: true,
|
|
668
|
+
sdkScriptUrls: (pageState?.scripts || []).filter((source) =>
|
|
669
|
+
isSdkScriptRequest(source),
|
|
670
|
+
),
|
|
671
|
+
});
|
|
672
|
+
console.log(` attached -> ${url}`);
|
|
673
|
+
await call(ws, "Page.reload", { ignoreCache: true }, sessionId);
|
|
674
|
+
|
|
675
|
+
{
|
|
676
|
+
const verification = await verifyPreparedPage(ws, sessionId);
|
|
677
|
+
logLifecycle(
|
|
678
|
+
createLifecycleResult({
|
|
679
|
+
operation: "inspect_registration",
|
|
680
|
+
ok: verification.ok,
|
|
681
|
+
state: verification.ok ? "registration_ok" : "registration_mismatch",
|
|
682
|
+
error: verification.ok
|
|
683
|
+
? undefined
|
|
684
|
+
: {
|
|
685
|
+
code: ERROR_CODES.REGISTRATION_MISMATCH,
|
|
686
|
+
message:
|
|
687
|
+
"The local action registry did not match the configured page agent.",
|
|
688
|
+
},
|
|
689
|
+
details: {
|
|
690
|
+
mode: attachMode,
|
|
691
|
+
expectedAgentId: verification.characterization.expectedAgentId,
|
|
692
|
+
detectedAgentIds: verification.state?.agentIds || [],
|
|
693
|
+
expectedProductId: productId,
|
|
694
|
+
detectedProductIds: verification.products,
|
|
695
|
+
widget: verification.widget,
|
|
696
|
+
registeredActionNames:
|
|
697
|
+
verification.registration?.actionNames || [],
|
|
698
|
+
expectedActionNames:
|
|
699
|
+
verification.observation?.expectedActionNames || [],
|
|
700
|
+
missingActionNames:
|
|
701
|
+
verification.registration?.missingActionNames || [],
|
|
702
|
+
unexpectedActionNames:
|
|
703
|
+
verification.registration?.unexpectedActionNames || [],
|
|
704
|
+
actionObserverSubscribed:
|
|
705
|
+
verification.observation?.subscribed === true,
|
|
706
|
+
actionNameLimitExceeded:
|
|
707
|
+
verification.observation?.actionNameLimitExceeded === true,
|
|
708
|
+
diagnosticError: verification.diagnosticError,
|
|
709
|
+
},
|
|
710
|
+
}),
|
|
711
|
+
);
|
|
712
|
+
if (!verification.ok) {
|
|
713
|
+
const state = prepared.get(sessionId);
|
|
714
|
+
if (state) {
|
|
715
|
+
await cleanupPreparedSession(ws, sessionId, state);
|
|
716
|
+
prepared.delete(sessionId);
|
|
717
|
+
}
|
|
718
|
+
rejected.set(sessionId, url);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
} finally {
|
|
722
|
+
preparing.delete(sessionId);
|
|
723
|
+
}
|
|
258
724
|
}
|
|
259
725
|
|
|
260
726
|
async function onEvent(ws, msg) {
|
|
@@ -264,7 +730,10 @@ async function onEvent(ws, msg) {
|
|
|
264
730
|
const t = params.targetInfo;
|
|
265
731
|
if (t.type === "page") {
|
|
266
732
|
sessions.set(t.targetId, params.sessionId);
|
|
267
|
-
|
|
733
|
+
sessionUrls.set(params.sessionId, t.url);
|
|
734
|
+
if (hostMatches(t.url, hostPatterns)) {
|
|
735
|
+
await prepare(ws, params.sessionId, t.url);
|
|
736
|
+
}
|
|
268
737
|
} else {
|
|
269
738
|
// Auto-attach pauses workers on start until the attaching client says go.
|
|
270
739
|
// We do not instrument workers — but if we never release them they stay
|
|
@@ -281,17 +750,76 @@ async function onEvent(ws, msg) {
|
|
|
281
750
|
// host, arm it now.
|
|
282
751
|
if (method === "Target.targetInfoChanged") {
|
|
283
752
|
const t = params.targetInfo;
|
|
284
|
-
if (t.type === "page"
|
|
753
|
+
if (t.type === "page") {
|
|
285
754
|
const sid = sessions.get(t.targetId);
|
|
286
|
-
if (sid)
|
|
755
|
+
if (!sid) return;
|
|
756
|
+
sessionUrls.set(sid, t.url);
|
|
757
|
+
if (hostMatches(t.url, hostPatterns)) {
|
|
758
|
+
await prepare(ws, sid, t.url);
|
|
759
|
+
} else {
|
|
760
|
+
const state = prepared.get(sid);
|
|
761
|
+
if (state) {
|
|
762
|
+
await cleanupPreparedSession(ws, sid, state, { reload: false });
|
|
763
|
+
prepared.delete(sid);
|
|
764
|
+
}
|
|
765
|
+
rejected.delete(sid);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
if (method === "Target.detachedFromTarget") {
|
|
772
|
+
const sid = params.sessionId;
|
|
773
|
+
prepared.delete(sid);
|
|
774
|
+
preparing.delete(sid);
|
|
775
|
+
rejected.delete(sid);
|
|
776
|
+
sessionUrls.delete(sid);
|
|
777
|
+
for (const [targetId, mappedSessionId] of sessions) {
|
|
778
|
+
if (mappedSessionId === sid) sessions.delete(targetId);
|
|
287
779
|
}
|
|
288
780
|
return;
|
|
289
781
|
}
|
|
290
782
|
|
|
291
783
|
if (method === "Fetch.requestPaused") {
|
|
292
784
|
const { requestId, request } = params;
|
|
293
|
-
if (!
|
|
294
|
-
|
|
785
|
+
if (!hostMatches(sessionUrls.get(sessionId) || "", hostPatterns)) {
|
|
786
|
+
await call(ws, "Fetch.continueRequest", { requestId }, sessionId);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
const preparation = prepared.get(sessionId);
|
|
790
|
+
if (
|
|
791
|
+
isSdkScriptRequest(
|
|
792
|
+
request.url,
|
|
793
|
+
preparation?.sdkScriptUrls || [],
|
|
794
|
+
)
|
|
795
|
+
) {
|
|
796
|
+
if (replace) {
|
|
797
|
+
const rewrittenUrl = rewriteSdkRequestKey(
|
|
798
|
+
request.url,
|
|
799
|
+
agentKey,
|
|
800
|
+
preparation?.sdkScriptUrls || [],
|
|
801
|
+
);
|
|
802
|
+
console.log(
|
|
803
|
+
` rebound SDK request to product ${productId} for ${agentApiName}`,
|
|
804
|
+
);
|
|
805
|
+
await call(
|
|
806
|
+
ws,
|
|
807
|
+
"Fetch.continueRequest",
|
|
808
|
+
{ requestId, url: rewrittenUrl },
|
|
809
|
+
sessionId,
|
|
810
|
+
);
|
|
811
|
+
} else {
|
|
812
|
+
await call(
|
|
813
|
+
ws,
|
|
814
|
+
"Fetch.continueRequest",
|
|
815
|
+
{ requestId },
|
|
816
|
+
sessionId,
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
if (!shouldFulfillActionRequest(request.url, actionTarget)) {
|
|
822
|
+
console.log(` continued unrelated actions request -> ${request.url.slice(0, 100)}`);
|
|
295
823
|
await call(ws, "Fetch.continueRequest", { requestId }, sessionId);
|
|
296
824
|
return;
|
|
297
825
|
}
|
|
@@ -308,14 +836,42 @@ async function onEvent(ws, msg) {
|
|
|
308
836
|
],
|
|
309
837
|
body: body.toString("base64"),
|
|
310
838
|
}, sessionId);
|
|
839
|
+
logLifecycle(
|
|
840
|
+
createLifecycleResult({
|
|
841
|
+
operation: "load_artifact",
|
|
842
|
+
ok: true,
|
|
843
|
+
state: "artifact_served",
|
|
844
|
+
details: {
|
|
845
|
+
productId,
|
|
846
|
+
agentApiName,
|
|
847
|
+
bytes: body.length,
|
|
848
|
+
requestUrl: request.url.slice(0, 2_000),
|
|
849
|
+
},
|
|
850
|
+
}),
|
|
851
|
+
);
|
|
311
852
|
return;
|
|
312
853
|
}
|
|
313
854
|
|
|
314
855
|
if (method === "Runtime.consoleAPICalled") {
|
|
856
|
+
const state = prepared.get(sessionId);
|
|
857
|
+
const currentUrl = sessionUrls.get(sessionId);
|
|
858
|
+
if (
|
|
859
|
+
state?.observing !== true ||
|
|
860
|
+
!currentUrl ||
|
|
861
|
+
!hostMatches(currentUrl, hostPatterns)
|
|
862
|
+
) {
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
315
865
|
const text = (params.args || [])
|
|
316
866
|
.map((a) => (a.value !== undefined ? a.value : a.description || ""))
|
|
317
867
|
.join(" ");
|
|
318
|
-
|
|
868
|
+
const actionEvent = sessionCollector.recordConsole({
|
|
869
|
+
level: params.type || "log",
|
|
870
|
+
text,
|
|
871
|
+
});
|
|
872
|
+
if (actionEvent) {
|
|
873
|
+
console.log(` ${formatActionObservation(actionEvent)}`);
|
|
874
|
+
} else if (/foldspace-dev|remote actions|identified|Error/i.test(text) && !/IO (entry|callback)/.test(text)) {
|
|
319
875
|
console.log(` [page] ${text.slice(0, 160)}`);
|
|
320
876
|
}
|
|
321
877
|
}
|
|
@@ -330,33 +886,138 @@ const keepAlive = setInterval(() => {}, 1 << 30);
|
|
|
330
886
|
|
|
331
887
|
async function connect() {
|
|
332
888
|
const ver = await (await fetch(`http://localhost:${port}/json/version`)).json();
|
|
333
|
-
|
|
889
|
+
const socket = new WebSocket(ver.webSocketDebuggerUrl);
|
|
334
890
|
await new Promise((resolve, reject) => {
|
|
335
|
-
|
|
336
|
-
|
|
891
|
+
socket.onopen = resolve;
|
|
892
|
+
socket.onerror = reject;
|
|
337
893
|
});
|
|
338
|
-
ws
|
|
894
|
+
ws = socket;
|
|
895
|
+
socket.onmessage = (e) => {
|
|
339
896
|
const m = JSON.parse(e.data);
|
|
340
|
-
if (
|
|
341
|
-
|
|
897
|
+
if (cdpRequests.handleMessage(m)) return;
|
|
898
|
+
if (m.method) {
|
|
899
|
+
void onEvent(socket, m).catch((error) => {
|
|
900
|
+
console.error(
|
|
901
|
+
` attach event failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
902
|
+
);
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
};
|
|
906
|
+
socket.onclose = () => {
|
|
907
|
+
if (socket !== ws) return;
|
|
908
|
+
cdpRequests.failAll(new Error("CDP socket closed"));
|
|
909
|
+
if (!shuttingDown) void onDisconnect(socket);
|
|
342
910
|
};
|
|
343
|
-
|
|
344
|
-
await call(
|
|
345
|
-
await call(ws, "Target.setAutoAttach", { autoAttach: true, waitForDebuggerOnStart: false, flatten: true });
|
|
911
|
+
await call(socket, "Target.setDiscoverTargets", { discover: true });
|
|
912
|
+
await call(socket, "Target.setAutoAttach", { autoAttach: true, waitForDebuggerOnStart: false, flatten: true });
|
|
346
913
|
return ver;
|
|
347
914
|
}
|
|
348
915
|
|
|
349
916
|
const ver = await connect();
|
|
350
917
|
|
|
351
918
|
console.log(`Attached to ${ver.Browser} on :${port}`);
|
|
352
|
-
console.log(`Mode: ${
|
|
919
|
+
console.log(`Mode: ${attachMode}`);
|
|
920
|
+
console.log(`Agent: ${agentApiName} (product ${productId}, ${agentMode})`);
|
|
353
921
|
console.log(`Test: ${noTestMode ? "OFF — conversations WILL appear in the dashboard" : "on"}`);
|
|
354
922
|
console.log(`Hosts: ${hostPatterns.join(", ")}`);
|
|
355
923
|
console.log(`Serving: ${path.relative(root, bundlePath)}\n`);
|
|
356
924
|
|
|
925
|
+
function printSessionSummary() {
|
|
926
|
+
if (summaryPrinted) return;
|
|
927
|
+
summaryPrinted = true;
|
|
928
|
+
const summary = formatSessionSummary(sessionCollector.snapshot());
|
|
929
|
+
if (summary) console.log(`\n${summary}`);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
async function cleanupPreparedSession(
|
|
933
|
+
socket,
|
|
934
|
+
sessionId,
|
|
935
|
+
state,
|
|
936
|
+
{ reload = true } = {},
|
|
937
|
+
) {
|
|
938
|
+
state.observing = false;
|
|
939
|
+
const failures = [];
|
|
940
|
+
if (state.fetchEnabled) {
|
|
941
|
+
try {
|
|
942
|
+
await call(socket, "Fetch.disable", {}, sessionId);
|
|
943
|
+
state.fetchEnabled = false;
|
|
944
|
+
} catch (error) {
|
|
945
|
+
failures.push(`Fetch.disable: ${error instanceof Error ? error.message : String(error)}`);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
for (const identifier of state.scriptIds) {
|
|
949
|
+
try {
|
|
950
|
+
await call(
|
|
951
|
+
socket,
|
|
952
|
+
"Page.removeScriptToEvaluateOnNewDocument",
|
|
953
|
+
{ identifier },
|
|
954
|
+
sessionId,
|
|
955
|
+
);
|
|
956
|
+
} catch (error) {
|
|
957
|
+
failures.push(
|
|
958
|
+
`remove preload ${identifier}: ${error instanceof Error ? error.message : String(error)}`,
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
state.scriptIds.length = 0;
|
|
963
|
+
try {
|
|
964
|
+
await call(
|
|
965
|
+
socket,
|
|
966
|
+
"Page.setBypassCSP",
|
|
967
|
+
{ enabled: false },
|
|
968
|
+
sessionId,
|
|
969
|
+
);
|
|
970
|
+
} catch (error) {
|
|
971
|
+
failures.push(`restore CSP: ${error instanceof Error ? error.message : String(error)}`);
|
|
972
|
+
}
|
|
973
|
+
// Preload scripts have already executed in the current document. Reloading
|
|
974
|
+
// is the generic cleanup for swap handlers, bootstrap SDKs, and replacement
|
|
975
|
+
// identity aliases alike.
|
|
976
|
+
if (reload) {
|
|
977
|
+
try {
|
|
978
|
+
await call(socket, "Page.reload", { ignoreCache: true }, sessionId);
|
|
979
|
+
} catch (error) {
|
|
980
|
+
failures.push(`restore reload: ${error instanceof Error ? error.message : String(error)}`);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
return failures;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
async function cleanup() {
|
|
987
|
+
const failures = [];
|
|
988
|
+
for (const [sessionId, state] of prepared) {
|
|
989
|
+
failures.push(...(await cleanupPreparedSession(ws, sessionId, state)));
|
|
990
|
+
}
|
|
991
|
+
const result = createLifecycleResult({
|
|
992
|
+
operation: "cleanup",
|
|
993
|
+
ok: failures.length === 0,
|
|
994
|
+
state: failures.length === 0 ? "detached" : "cleanup_failed",
|
|
995
|
+
error:
|
|
996
|
+
failures.length === 0
|
|
997
|
+
? undefined
|
|
998
|
+
: {
|
|
999
|
+
code: ERROR_CODES.TRANSPORT_ERROR,
|
|
1000
|
+
message: failures.join("; "),
|
|
1001
|
+
},
|
|
1002
|
+
details: {
|
|
1003
|
+
sessions: prepared.size,
|
|
1004
|
+
served,
|
|
1005
|
+
pagesReloaded: prepared.size,
|
|
1006
|
+
},
|
|
1007
|
+
});
|
|
1008
|
+
logLifecycle(result);
|
|
1009
|
+
printSessionSummary();
|
|
1010
|
+
return result;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
357
1013
|
process.on("SIGINT", () => {
|
|
358
|
-
|
|
359
|
-
|
|
1014
|
+
if (shuttingDown) return;
|
|
1015
|
+
shuttingDown = true;
|
|
1016
|
+
void cleanup().finally(() => {
|
|
1017
|
+
clearInterval(keepAlive);
|
|
1018
|
+
console.log(`\nDetached. Served the local bundle ${served} time(s).`);
|
|
1019
|
+
process.exit(0);
|
|
1020
|
+
});
|
|
360
1021
|
});
|
|
361
1022
|
|
|
362
1023
|
// A closed socket does not mean the browser closed. Chrome drops the CDP
|
|
@@ -373,27 +1034,38 @@ async function browserIsUp() {
|
|
|
373
1034
|
}
|
|
374
1035
|
}
|
|
375
1036
|
|
|
376
|
-
async function onDisconnect() {
|
|
1037
|
+
async function onDisconnect(closedSocket) {
|
|
1038
|
+
if (closedSocket !== ws || reconnecting || shuttingDown) return;
|
|
1039
|
+
reconnecting = true;
|
|
377
1040
|
prepared.clear();
|
|
1041
|
+
preparing.clear();
|
|
1042
|
+
rejected.clear();
|
|
378
1043
|
sessions.clear();
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
1044
|
+
sessionUrls.clear();
|
|
1045
|
+
try {
|
|
1046
|
+
for (let attempt = 1; attempt <= 10; attempt++) {
|
|
1047
|
+
await new Promise((r) => setTimeout(r, Math.min(500 * attempt, 3000)));
|
|
1048
|
+
if (!(await browserIsUp())) {
|
|
1049
|
+
clearInterval(keepAlive);
|
|
1050
|
+
printSessionSummary();
|
|
1051
|
+
console.log("\nBrowser closed. Detaching.");
|
|
1052
|
+
process.exit(0);
|
|
1053
|
+
}
|
|
1054
|
+
try {
|
|
1055
|
+
console.log(` connection lost — reconnecting (${attempt}/10)…`);
|
|
1056
|
+
await connect();
|
|
1057
|
+
console.log(` reconnected. ${attachMode} mode is live again.`);
|
|
1058
|
+
return;
|
|
1059
|
+
} catch {
|
|
1060
|
+
// fall through and retry
|
|
1061
|
+
}
|
|
393
1062
|
}
|
|
1063
|
+
clearInterval(keepAlive);
|
|
1064
|
+
printSessionSummary();
|
|
1065
|
+
console.error("\nCould not reconnect after 10 attempts. Detaching — "
|
|
1066
|
+
+ "the page is now loading the DEPLOYED bundle, not your local build.");
|
|
1067
|
+
process.exit(1);
|
|
1068
|
+
} finally {
|
|
1069
|
+
reconnecting = false;
|
|
394
1070
|
}
|
|
395
|
-
clearInterval(keepAlive);
|
|
396
|
-
console.error("\nCould not reconnect after 10 attempts. Detaching — "
|
|
397
|
-
+ "the page is now loading the DEPLOYED bundle, not your local build.");
|
|
398
|
-
process.exit(1);
|
|
399
1071
|
}
|