@q-agent/agent 0.1.13 → 0.1.15
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/src/api.js +33 -0
- package/dist/src/paths.js +14 -0
- package/dist/src/runner.js +205 -0
- package/package.json +1 -1
- package/vendor/authoring_browser.cjs +128 -0
package/dist/src/api.js
CHANGED
|
@@ -56,6 +56,9 @@ exports.postExploreDecide = postExploreDecide;
|
|
|
56
56
|
exports.postExploreEvent = postExploreEvent;
|
|
57
57
|
exports.postExploreFinalize = postExploreFinalize;
|
|
58
58
|
exports.postComplete = postComplete;
|
|
59
|
+
exports.claimNextAuthoring = claimNextAuthoring;
|
|
60
|
+
exports.postAuthoringEvent = postAuthoringEvent;
|
|
61
|
+
exports.postAuthoringFinalize = postAuthoringFinalize;
|
|
59
62
|
const fs = __importStar(require("fs"));
|
|
60
63
|
/** Raised for any non-2xx/204 response; carries the HTTP status for callers that care (e.g. 409). */
|
|
61
64
|
class ApiError extends Error {
|
|
@@ -288,3 +291,33 @@ async function postComplete(cfg, executionId, body) {
|
|
|
288
291
|
});
|
|
289
292
|
await throwIfNotOk(res);
|
|
290
293
|
}
|
|
294
|
+
/** Claim the next queued authoring session, or null (204) if none. */
|
|
295
|
+
async function claimNextAuthoring(cfg) {
|
|
296
|
+
const res = await fetch(`${cfg.serverUrl}/agent/authoring/next`, {
|
|
297
|
+
method: "POST",
|
|
298
|
+
headers: authHeaders(cfg.deviceToken),
|
|
299
|
+
});
|
|
300
|
+
if (res.status === 204)
|
|
301
|
+
return null;
|
|
302
|
+
await throwIfNotOk(res);
|
|
303
|
+
return (await res.json());
|
|
304
|
+
}
|
|
305
|
+
/** Relay an authoring progress event onto the run's WebSocket (server-side). */
|
|
306
|
+
async function postAuthoringEvent(cfg, sessionId, event, payload) {
|
|
307
|
+
const res = await fetch(`${cfg.serverUrl}/agent/authoring/${sessionId}/events`, {
|
|
308
|
+
method: "POST",
|
|
309
|
+
headers: { ...authHeaders(cfg.deviceToken), "Content-Type": "application/json" },
|
|
310
|
+
body: JSON.stringify({ event, payload }),
|
|
311
|
+
});
|
|
312
|
+
await throwIfNotOk(res);
|
|
313
|
+
}
|
|
314
|
+
/** Finalize an authoring session: the server gates + persists the authored spec
|
|
315
|
+
* and KB-merges the runtime-verified discovery. */
|
|
316
|
+
async function postAuthoringFinalize(cfg, sessionId, body) {
|
|
317
|
+
const res = await fetchWithTimeout(`${cfg.serverUrl}/agent/authoring/${sessionId}/finalize`, {
|
|
318
|
+
method: "POST",
|
|
319
|
+
headers: { ...authHeaders(cfg.deviceToken), "Content-Type": "application/json" },
|
|
320
|
+
body: JSON.stringify(body),
|
|
321
|
+
}, HEAL_FIX_REQUEST_TIMEOUT_MS);
|
|
322
|
+
await throwIfNotOk(res);
|
|
323
|
+
}
|
package/dist/src/paths.js
CHANGED
|
@@ -42,6 +42,7 @@ exports.agentNodeModules = agentNodeModules;
|
|
|
42
42
|
exports.playwrightCli = playwrightCli;
|
|
43
43
|
exports.vendorCaptureScript = vendorCaptureScript;
|
|
44
44
|
exports.vendorExploreScript = vendorExploreScript;
|
|
45
|
+
exports.vendorAuthoringScript = vendorAuthoringScript;
|
|
45
46
|
/**
|
|
46
47
|
* Runtime path resolution for the agent's child processes (Playwright CLI +
|
|
47
48
|
* the headed-login capture script), working in three layouts:
|
|
@@ -163,3 +164,16 @@ function vendorExploreScript() {
|
|
|
163
164
|
throw new Error("explore_session.cjs not found — the agent package is missing vendor/");
|
|
164
165
|
return found;
|
|
165
166
|
}
|
|
167
|
+
/** The vendored live-authoring Chrome launcher (`authoring_browser.cjs`, #403).
|
|
168
|
+
* Uses only Node built-ins (no Playwright), so it runs with a bare `node`. */
|
|
169
|
+
function vendorAuthoringScript() {
|
|
170
|
+
const root = packagedRoot();
|
|
171
|
+
const found = firstExisting([
|
|
172
|
+
...(root ? [path.join(root, "vendor", "authoring_browser.cjs")] : []),
|
|
173
|
+
path.join(__dirname, "..", "..", "vendor", "authoring_browser.cjs"),
|
|
174
|
+
path.join(__dirname, "..", "vendor", "authoring_browser.cjs"),
|
|
175
|
+
]);
|
|
176
|
+
if (!found)
|
|
177
|
+
throw new Error("authoring_browser.cjs not found — the agent package is missing vendor/");
|
|
178
|
+
return found;
|
|
179
|
+
}
|
package/dist/src/runner.js
CHANGED
|
@@ -44,9 +44,11 @@ exports.killActiveChild = killActiveChild;
|
|
|
44
44
|
exports.processJob = processJob;
|
|
45
45
|
exports.runExplorationLoop = runExplorationLoop;
|
|
46
46
|
exports.processExplorationJob = processExplorationJob;
|
|
47
|
+
exports.processAuthoringJob = processAuthoringJob;
|
|
47
48
|
exports.runAgentLoop = runAgentLoop;
|
|
48
49
|
const child_process_1 = require("child_process");
|
|
49
50
|
const fs = __importStar(require("fs"));
|
|
51
|
+
const net = __importStar(require("net"));
|
|
50
52
|
const os = __importStar(require("os"));
|
|
51
53
|
const path = __importStar(require("path"));
|
|
52
54
|
const readline = __importStar(require("readline"));
|
|
@@ -919,6 +921,187 @@ async function processExplorationJob(cfg, session) {
|
|
|
919
921
|
* Long-poll loop: claim → process → repeat, backing off `IDLE_POLL_MS`
|
|
920
922
|
* between empty claims. Runs until `signal.aborted`.
|
|
921
923
|
*/
|
|
924
|
+
// --- Live authoring (#403) — drive `claude` + browser-harness locally ----------
|
|
925
|
+
const AUTHORING_CDP_READY_TIMEOUT_MS = 30_000;
|
|
926
|
+
/** Grab a free localhost TCP port for the dedicated Chrome's CDP endpoint. */
|
|
927
|
+
async function getFreePort() {
|
|
928
|
+
return new Promise((resolve, reject) => {
|
|
929
|
+
const srv = net.createServer();
|
|
930
|
+
srv.on("error", reject);
|
|
931
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
932
|
+
const addr = srv.address();
|
|
933
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
934
|
+
srv.close(() => (port ? resolve(port) : reject(new Error("no free port"))));
|
|
935
|
+
});
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
/** Poll Chrome's /json/version until it responds (CDP is up) or we time out. */
|
|
939
|
+
async function waitForCdp(port, timeoutMs) {
|
|
940
|
+
const deadline = Date.now() + timeoutMs;
|
|
941
|
+
while (Date.now() < deadline) {
|
|
942
|
+
try {
|
|
943
|
+
const r = await fetch(`http://127.0.0.1:${port}/json/version`);
|
|
944
|
+
if (r.ok)
|
|
945
|
+
return true;
|
|
946
|
+
}
|
|
947
|
+
catch {
|
|
948
|
+
/* endpoint not up yet */
|
|
949
|
+
}
|
|
950
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
951
|
+
}
|
|
952
|
+
return false;
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Author one spec live on this machine (#403): launch the dedicated,
|
|
956
|
+
* pre-authenticated Chrome (vendored launcher), point the local `browser-harness`
|
|
957
|
+
* at it via BU_CDP_URL, and run the local `claude` agentically (prompts composed
|
|
958
|
+
* server-side) to perform the case and write `<specFilename>` + `discovered.json`
|
|
959
|
+
* into a temp workspace. Posts the result back for the server to gate + persist.
|
|
960
|
+
* Requires `claude` + `browser-harness` on the agent machine's PATH and a
|
|
961
|
+
* pre-authenticated `browser-profile` for the origin (from manual-login capture).
|
|
962
|
+
*/
|
|
963
|
+
async function processAuthoringJob(cfg, job) {
|
|
964
|
+
let origin = job.origin;
|
|
965
|
+
if (!origin && job.baseUrl) {
|
|
966
|
+
try {
|
|
967
|
+
origin = new URL(job.baseUrl).origin;
|
|
968
|
+
}
|
|
969
|
+
catch {
|
|
970
|
+
origin = "";
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
const sess = origin ? (0, session_1.sessionPathsForOrigin)(origin) : null;
|
|
974
|
+
const profileDir = sess ? path.join(sess.dir, "browser-profile") : "";
|
|
975
|
+
const finalize = async (code, discovered, summary, ok) => {
|
|
976
|
+
await api
|
|
977
|
+
.postAuthoringFinalize(cfg, job.sessionId, {
|
|
978
|
+
code,
|
|
979
|
+
discovered: discovered || {},
|
|
980
|
+
summary,
|
|
981
|
+
ok,
|
|
982
|
+
})
|
|
983
|
+
.catch((err) => console.error("postAuthoringFinalize failed:", err));
|
|
984
|
+
};
|
|
985
|
+
if (!profileDir || !fs.existsSync(profileDir)) {
|
|
986
|
+
await finalize("", { routes: [], selectors: [] }, "No authenticated browser profile for this origin — capture a manual login first.", false);
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
const workDir = fs.mkdtempSync(path.join(os.tmpdir(), "qagent-authoring-"));
|
|
990
|
+
const port = await getFreePort();
|
|
991
|
+
let launcher = null;
|
|
992
|
+
let claude = null;
|
|
993
|
+
try {
|
|
994
|
+
await api
|
|
995
|
+
.postAuthoringEvent(cfg, job.sessionId, "authoring.progress", {
|
|
996
|
+
case: job.caseId, phase: "launching", message: "Starting authenticated browser",
|
|
997
|
+
})
|
|
998
|
+
.catch(() => { });
|
|
999
|
+
// 1) Dedicated pre-auth Chrome (vendored launcher — Node built-ins only).
|
|
1000
|
+
launcher = (0, child_process_1.spawn)((0, paths_1.nodeBin)(), [(0, paths_1.vendorAuthoringScript)(), job.baseUrl, String(port), profileDir], {
|
|
1001
|
+
env: { ...process.env, ...(0, paths_1.childNodeEnv)() },
|
|
1002
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1003
|
+
});
|
|
1004
|
+
activeChild = launcher;
|
|
1005
|
+
let launcherErr = "";
|
|
1006
|
+
launcher.stderr?.on("data", (d) => { launcherErr += String(d); });
|
|
1007
|
+
if (!(await waitForCdp(port, AUTHORING_CDP_READY_TIMEOUT_MS))) {
|
|
1008
|
+
await finalize("", { routes: [], selectors: [] }, `Chrome CDP did not come up on port ${port}. ${launcherErr.trim()}`.trim(), false);
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
await api
|
|
1012
|
+
.postAuthoringEvent(cfg, job.sessionId, "authoring.progress", {
|
|
1013
|
+
case: job.caseId, phase: "driving", message: "Driving the app live with browser-harness",
|
|
1014
|
+
})
|
|
1015
|
+
.catch(() => { });
|
|
1016
|
+
// 2) Local agentic Claude drives browser-harness (BU_CDP_URL → our Chrome),
|
|
1017
|
+
// writing the spec + discovered.json into workDir. System prompt via a
|
|
1018
|
+
// file (avoids a huge argv); task prompt via stdin.
|
|
1019
|
+
const systemFile = path.join(workDir, "system-prompt.txt");
|
|
1020
|
+
fs.writeFileSync(systemFile, job.systemPrompt, "utf-8");
|
|
1021
|
+
// On Windows `claude` is a .cmd shim, which Node can only launch via a shell;
|
|
1022
|
+
// quote the path-bearing args there. On POSIX we spawn without a shell.
|
|
1023
|
+
const useShell = process.platform === "win32";
|
|
1024
|
+
const q = (s) => (useShell ? `"${s}"` : s);
|
|
1025
|
+
const claudeArgs = [
|
|
1026
|
+
"-p",
|
|
1027
|
+
"--output-format", "json",
|
|
1028
|
+
"--model", job.model,
|
|
1029
|
+
"--append-system-prompt-file", q(systemFile),
|
|
1030
|
+
"--allowedTools", "Bash", "Read", "Write", "Glob", "Grep",
|
|
1031
|
+
"--dangerously-skip-permissions",
|
|
1032
|
+
"--add-dir", q(workDir),
|
|
1033
|
+
"--max-budget-usd", String(job.maxBudgetUsd),
|
|
1034
|
+
];
|
|
1035
|
+
claude = (0, child_process_1.spawn)("claude", claudeArgs, {
|
|
1036
|
+
cwd: workDir,
|
|
1037
|
+
env: { ...process.env, BU_CDP_URL: `http://127.0.0.1:${port}` },
|
|
1038
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1039
|
+
shell: useShell,
|
|
1040
|
+
});
|
|
1041
|
+
activeChild = claude;
|
|
1042
|
+
let cout = "";
|
|
1043
|
+
let cerr = "";
|
|
1044
|
+
claude.stdout?.on("data", (d) => { cout += String(d); });
|
|
1045
|
+
claude.stderr?.on("data", (d) => { cerr += String(d); });
|
|
1046
|
+
try {
|
|
1047
|
+
claude.stdin?.write(job.taskPrompt);
|
|
1048
|
+
claude.stdin?.end();
|
|
1049
|
+
}
|
|
1050
|
+
catch { }
|
|
1051
|
+
await new Promise((resolve) => {
|
|
1052
|
+
claude.on("close", () => resolve());
|
|
1053
|
+
claude.on("error", (e) => { cerr += `\nclaude spawn error: ${e.message}`; resolve(); });
|
|
1054
|
+
});
|
|
1055
|
+
// 3) Read emitted artifacts.
|
|
1056
|
+
const specPath = path.join(workDir, job.specFilename);
|
|
1057
|
+
const sidecarPath = path.join(workDir, job.sidecarFilename || "discovered.json");
|
|
1058
|
+
const code = fs.existsSync(specPath) ? fs.readFileSync(specPath, "utf-8") : "";
|
|
1059
|
+
let discovered = { routes: [], selectors: [] };
|
|
1060
|
+
if (fs.existsSync(sidecarPath)) {
|
|
1061
|
+
try {
|
|
1062
|
+
discovered = JSON.parse(fs.readFileSync(sidecarPath, "utf-8"));
|
|
1063
|
+
}
|
|
1064
|
+
catch { /* keep default */ }
|
|
1065
|
+
}
|
|
1066
|
+
let summary = "";
|
|
1067
|
+
try {
|
|
1068
|
+
summary = JSON.parse(cout).result || "";
|
|
1069
|
+
}
|
|
1070
|
+
catch {
|
|
1071
|
+
summary = (cout || cerr).slice(0, 800);
|
|
1072
|
+
}
|
|
1073
|
+
if (!code.trim() && !summary)
|
|
1074
|
+
summary = "Live authoring produced no spec.";
|
|
1075
|
+
const ok = code.trim().length > 0;
|
|
1076
|
+
await api
|
|
1077
|
+
.postAuthoringEvent(cfg, job.sessionId, "authoring.progress", {
|
|
1078
|
+
case: job.caseId, phase: ok ? "done" : "failed", message: summary.slice(0, 400),
|
|
1079
|
+
})
|
|
1080
|
+
.catch(() => { });
|
|
1081
|
+
await finalize(code, discovered, summary, ok);
|
|
1082
|
+
}
|
|
1083
|
+
finally {
|
|
1084
|
+
try {
|
|
1085
|
+
claude?.kill();
|
|
1086
|
+
}
|
|
1087
|
+
catch { }
|
|
1088
|
+
// Closing the launcher's stdin tells it to kill Chrome (cross-platform).
|
|
1089
|
+
try {
|
|
1090
|
+
launcher?.stdin?.end();
|
|
1091
|
+
}
|
|
1092
|
+
catch { }
|
|
1093
|
+
try {
|
|
1094
|
+
launcher?.kill();
|
|
1095
|
+
}
|
|
1096
|
+
catch { }
|
|
1097
|
+
if (activeChild === launcher || activeChild === claude)
|
|
1098
|
+
activeChild = null;
|
|
1099
|
+
try {
|
|
1100
|
+
fs.rmSync(workDir, { recursive: true, force: true });
|
|
1101
|
+
}
|
|
1102
|
+
catch { }
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
922
1105
|
async function runAgentLoop(cfg, signal) {
|
|
923
1106
|
if (!(await (0, ensureBrowser_1.ensureChromium)())) {
|
|
924
1107
|
console.error("Chromium is required to run tests — aborting.");
|
|
@@ -967,6 +1150,28 @@ async function runAgentLoop(cfg, signal) {
|
|
|
967
1150
|
}
|
|
968
1151
|
continue;
|
|
969
1152
|
}
|
|
1153
|
+
// No exploration either — check for a queued live-authoring session (#403):
|
|
1154
|
+
// drive `claude` + browser-harness locally to author a spec from the real app.
|
|
1155
|
+
let authoring = null;
|
|
1156
|
+
try {
|
|
1157
|
+
authoring = await api.claimNextAuthoring(cfg);
|
|
1158
|
+
}
|
|
1159
|
+
catch (err) {
|
|
1160
|
+
console.error("Authoring claim failed:", err.message);
|
|
1161
|
+
}
|
|
1162
|
+
if (authoring) {
|
|
1163
|
+
console.log(`Claimed authoring ${authoring.sessionId} (case ${authoring.caseId}, ${authoring.baseUrl})`);
|
|
1164
|
+
(0, bus_1.emit)("authoring-claimed", { sessionId: authoring.sessionId, caseId: authoring.caseId });
|
|
1165
|
+
try {
|
|
1166
|
+
await processAuthoringJob(cfg, authoring);
|
|
1167
|
+
console.log(`Authoring ${authoring.sessionId} complete`);
|
|
1168
|
+
}
|
|
1169
|
+
catch (err) {
|
|
1170
|
+
console.error(`Authoring ${authoring.sessionId} crashed:`, err);
|
|
1171
|
+
(0, bus_1.emit)("error", { message: `Authoring ${authoring.sessionId} crashed: ${err.message}` });
|
|
1172
|
+
}
|
|
1173
|
+
continue;
|
|
1174
|
+
}
|
|
970
1175
|
await new Promise((r) => setTimeout(r, IDLE_POLL_MS));
|
|
971
1176
|
continue;
|
|
972
1177
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@q-agent/agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "Q-Agent Local Agent — claims execution jobs from the Q-Agent server and runs Playwright locally, so manual login/MFA happens on the user's own machine and session credentials never leave it.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Long-lived, pre-authenticated automation Chrome for live spec-authoring (#400).
|
|
2
|
+
// Args: baseUrl, port, profileDir.
|
|
3
|
+
//
|
|
4
|
+
// Launches a real Chrome/Edge (NOT via Playwright's launcher, so no automation
|
|
5
|
+
// fingerprint) on a FIXED --remote-debugging-port using a DEDICATED, persistent
|
|
6
|
+
// --user-data-dir. A dedicated non-default profile is deliberate: it lets
|
|
7
|
+
// browser-harness attach over CDP (BU_CDP_URL=http://127.0.0.1:<port>) without
|
|
8
|
+
// the Chrome "Allow remote debugging" popup / default-profile lockdown (see
|
|
9
|
+
// browser_harness/daemon.py:128-131,148). Auth is inherited from the persistent
|
|
10
|
+
// profile — reuse the capture `browser-profile` dir (already logged in via the
|
|
11
|
+
// manual-login capture flow), so the session is present without any injection.
|
|
12
|
+
//
|
|
13
|
+
// Unlike capture_auth.cjs (a short snapshot loop) this stays ALIVE for the whole
|
|
14
|
+
// authoring session and only tears Chrome down when the parent closes our stdin
|
|
15
|
+
// (cross-platform cleanup), on SIGTERM/SIGINT, or when Chrome exits on its own.
|
|
16
|
+
//
|
|
17
|
+
// Vendored copy of api/app/services/pw_scripts/authoring_browser.cjs — keep in
|
|
18
|
+
// sync (the agent runs this locally; the server runs the api copy in Docker).
|
|
19
|
+
const { spawn } = require('child_process');
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const [, , baseUrl, portArg, profileDir] = process.argv;
|
|
23
|
+
const PORT = parseInt(portArg, 10);
|
|
24
|
+
|
|
25
|
+
process.on('unhandledRejection', (e) => console.error('authoring_browser unhandledRejection:', e && (e.message || e)));
|
|
26
|
+
process.on('uncaughtException', (e) => console.error('authoring_browser uncaughtException:', e && (e.message || e)));
|
|
27
|
+
|
|
28
|
+
function findBrowser() {
|
|
29
|
+
// Explicit override wins (the Docker image sets QAGENT_CHROME_BIN=/usr/bin/chromium).
|
|
30
|
+
const c = [
|
|
31
|
+
process.env.QAGENT_CHROME_BIN,
|
|
32
|
+
// Linux / container (Debian chromium package + common Chrome paths).
|
|
33
|
+
'/usr/bin/chromium',
|
|
34
|
+
'/usr/bin/chromium-browser',
|
|
35
|
+
'/usr/bin/google-chrome',
|
|
36
|
+
'/usr/bin/google-chrome-stable',
|
|
37
|
+
// Windows host (native dev).
|
|
38
|
+
'C:/Program Files/Google/Chrome/Application/chrome.exe',
|
|
39
|
+
'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe',
|
|
40
|
+
(process.env.LOCALAPPDATA || '') + '/Google/Chrome/Application/chrome.exe',
|
|
41
|
+
'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe',
|
|
42
|
+
'C:/Program Files/Microsoft/Edge/Application/msedge.exe',
|
|
43
|
+
];
|
|
44
|
+
for (const p of c) { try { if (p && fs.existsSync(p)) return p; } catch {} }
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Headless container flags: a Linux host with no X display can't run headed
|
|
49
|
+
// Chrome, and Chrome-as-root in a container needs --no-sandbox; the small
|
|
50
|
+
// default /dev/shm makes --disable-dev-shm-usage necessary. On a real desktop
|
|
51
|
+
// (Windows, or Linux with DISPLAY) we launch headed so the operator can watch
|
|
52
|
+
// and MSAL/federated auth behaves like a normal browser.
|
|
53
|
+
function containerFlags() {
|
|
54
|
+
const headless = process.platform !== 'win32' && !process.env.DISPLAY;
|
|
55
|
+
return headless
|
|
56
|
+
? ['--headless=new', '--no-sandbox', '--disable-dev-shm-usage', '--disable-gpu']
|
|
57
|
+
: [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function waitForCDP(port, timeoutMs) {
|
|
61
|
+
const end = Date.now() + timeoutMs;
|
|
62
|
+
while (Date.now() < end) {
|
|
63
|
+
try { const r = await fetch(`http://127.0.0.1:${port}/json/version`); if (r.ok) return true; } catch {}
|
|
64
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
(async () => {
|
|
70
|
+
if (!PORT || Number.isNaN(PORT)) { console.error('authoring_browser: invalid port', portArg); process.exit(1); }
|
|
71
|
+
const exe = findBrowser();
|
|
72
|
+
if (!exe) { console.error('authoring_browser: no Chrome/Edge found on this machine'); process.exit(1); }
|
|
73
|
+
fs.mkdirSync(profileDir, { recursive: true });
|
|
74
|
+
|
|
75
|
+
// Same third-party-cookie seed as capture: fresh profiles break the MSAL/Entra
|
|
76
|
+
// federation redirects. No-op when the profile was already seeded by capture.
|
|
77
|
+
try {
|
|
78
|
+
const defDir = path.join(profileDir, 'Default');
|
|
79
|
+
fs.mkdirSync(defDir, { recursive: true });
|
|
80
|
+
const prefsPath = path.join(defDir, 'Preferences');
|
|
81
|
+
if (!fs.existsSync(prefsPath)) {
|
|
82
|
+
fs.writeFileSync(prefsPath, JSON.stringify({
|
|
83
|
+
profile: { cookie_controls_mode: 0, block_third_party_cookies: false,
|
|
84
|
+
default_content_setting_values: { cookies: 1 } },
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
} catch (e) { console.error('pref seed failed:', e && e.message); }
|
|
88
|
+
|
|
89
|
+
const child = spawn(exe, [
|
|
90
|
+
`--remote-debugging-port=${PORT}`,
|
|
91
|
+
`--user-data-dir=${profileDir}`,
|
|
92
|
+
'--no-first-run', '--no-default-browser-check', '--new-window',
|
|
93
|
+
...containerFlags(),
|
|
94
|
+
baseUrl,
|
|
95
|
+
], { detached: false, stdio: 'ignore' });
|
|
96
|
+
console.error('authoring_browser launched:', exe, 'port', PORT);
|
|
97
|
+
|
|
98
|
+
if (!(await waitForCDP(PORT, 20000))) {
|
|
99
|
+
console.error('authoring_browser: CDP endpoint never came up on port', PORT);
|
|
100
|
+
try { child.kill(); } catch {}
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Signal readiness on stdout so the parent can proceed (it also polls
|
|
105
|
+
// /json/version independently). The daemon resolves BU_CDP_URL to the WS.
|
|
106
|
+
console.log(`AUTHORING_BROWSER_READY ${PORT}`);
|
|
107
|
+
|
|
108
|
+
let shuttingDown = false;
|
|
109
|
+
const shutdown = (code) => {
|
|
110
|
+
if (shuttingDown) return;
|
|
111
|
+
shuttingDown = true;
|
|
112
|
+
try { child.kill(); } catch {}
|
|
113
|
+
process.exit(code || 0);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Cleanup triggers: parent closes our stdin (works cross-platform, incl.
|
|
117
|
+
// Windows where a terminate() won't run signal handlers), OS signals, or
|
|
118
|
+
// Chrome exiting on its own.
|
|
119
|
+
child.on('exit', () => shutdown(0));
|
|
120
|
+
process.stdin.on('end', () => shutdown(0));
|
|
121
|
+
process.stdin.on('close', () => shutdown(0));
|
|
122
|
+
process.on('SIGTERM', () => shutdown(0));
|
|
123
|
+
process.on('SIGINT', () => shutdown(0));
|
|
124
|
+
process.stdin.resume();
|
|
125
|
+
})().catch((e) => {
|
|
126
|
+
console.error('authoring_browser fatal:', e && (e.stack || e.message || e));
|
|
127
|
+
process.exit(1);
|
|
128
|
+
});
|