@q-agent/agent 0.1.0 → 0.1.6
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 +52 -1
- package/dist/src/api.js +54 -0
- package/dist/src/bus.js +27 -0
- package/dist/src/cli.js +20 -6
- package/dist/src/config.js +25 -0
- package/dist/src/ensureBrowser.js +8 -20
- package/dist/src/paths.js +152 -0
- package/dist/src/playwrightConfig.js +87 -42
- package/dist/src/report.js +4 -0
- package/dist/src/runner.js +342 -100
- package/dist/src/ui.js +629 -0
- package/dist/test/api.test.js +19 -0
- package/dist/test/playwrightConfig.test.js +84 -0
- package/dist/test/report.test.js +4 -0
- package/package.json +40 -2
package/README.md
CHANGED
|
@@ -17,7 +17,18 @@ Chromium is installed automatically: the first time you run `qagent-agent start`
|
|
|
17
17
|
the agent downloads Playwright's Chromium if it isn't already present (one-time,
|
|
18
18
|
~100 MB, with visible progress). No manual `npx playwright install` step is needed.
|
|
19
19
|
|
|
20
|
-
##
|
|
20
|
+
## Run with the UI (easiest)
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx @q-agent/agent
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Running with no command opens a local web UI in your browser. Paste a pairing
|
|
27
|
+
code (from the Q-Agent app's **Local Agent** screen) and the server URL, click
|
|
28
|
+
**Connect**, and watch the pull/run progress live. The device token is stored
|
|
29
|
+
locally, so next time it reconnects and starts pulling automatically.
|
|
30
|
+
|
|
31
|
+
## Command-line
|
|
21
32
|
|
|
22
33
|
```bash
|
|
23
34
|
npx @q-agent/agent pair <code> --server https://your-qagent-server.example.com/api
|
|
@@ -38,6 +49,46 @@ node dist/src/cli.js pair <code> --server http://127.0.0.1:8787
|
|
|
38
49
|
node dist/src/cli.js start
|
|
39
50
|
```
|
|
40
51
|
|
|
52
|
+
## Windows binary (no Node required)
|
|
53
|
+
|
|
54
|
+
For machines without Node, build a self-contained Windows bundle:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
cd agent
|
|
58
|
+
npm install
|
|
59
|
+
npm run package:win
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
This produces `dist-bin/qagent-agent-win-x64/` — a `qagent-agent.exe` (the CLI as
|
|
63
|
+
a Node Single Executable Application) plus a bundled Node runtime, production
|
|
64
|
+
`node_modules`, and `vendor/`. Zip that folder for distribution; the user unzips
|
|
65
|
+
and runs it with **no global Node installed**:
|
|
66
|
+
|
|
67
|
+
```bat
|
|
68
|
+
qagent-agent.exe pair <code> --server https://your-qagent-server.example.com/api
|
|
69
|
+
qagent-agent.exe start
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Chromium still auto-installs on first `start`. Build the bundle on Windows with a
|
|
73
|
+
Node 20+ that supports SEA (the local `node.exe` is used as the executable base —
|
|
74
|
+
no compiler or download needed).
|
|
75
|
+
|
|
76
|
+
## Desktop app (Electron)
|
|
77
|
+
|
|
78
|
+
A native window that wraps the same web UI:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
cd agent
|
|
82
|
+
npm install
|
|
83
|
+
npm run desktop # dev: builds, then opens the app window
|
|
84
|
+
npm run dist:desktop # build a Windows installer (electron-builder → dist-bin/desktop/)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The Electron shell runs the agent's UI server in-process and shows it in a
|
|
88
|
+
window — pair by entering the 6-digit code + server URL, then watch progress.
|
|
89
|
+
Child processes (Playwright / login capture) run via Electron-as-Node
|
|
90
|
+
(`ELECTRON_RUN_AS_NODE`), so no separate Node install is needed.
|
|
91
|
+
|
|
41
92
|
## Commands
|
|
42
93
|
|
|
43
94
|
| Command | Description |
|
package/dist/src/api.js
CHANGED
|
@@ -41,10 +41,15 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
41
41
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
42
|
exports.ApiError = void 0;
|
|
43
43
|
exports.redeemDevice = redeemDevice;
|
|
44
|
+
exports.disconnectDevice = disconnectDevice;
|
|
44
45
|
exports.claimNextJob = claimNextJob;
|
|
46
|
+
exports.claimNextCapture = claimNextCapture;
|
|
47
|
+
exports.postCaptureComplete = postCaptureComplete;
|
|
45
48
|
exports.postEvent = postEvent;
|
|
46
49
|
exports.postResult = postResult;
|
|
47
50
|
exports.postEvidence = postEvidence;
|
|
51
|
+
exports.postHealFix = postHealFix;
|
|
52
|
+
exports.postHealFinalize = postHealFinalize;
|
|
48
53
|
exports.postComplete = postComplete;
|
|
49
54
|
const fs = __importStar(require("fs"));
|
|
50
55
|
/** Raised for any non-2xx/204 response; carries the HTTP status for callers that care (e.g. 409). */
|
|
@@ -76,6 +81,16 @@ async function redeemDevice(serverUrl, code, name) {
|
|
|
76
81
|
await throwIfNotOk(res);
|
|
77
82
|
return (await res.json());
|
|
78
83
|
}
|
|
84
|
+
/** Self-revoke this device on the server (the "Disconnect" action). Best-effort:
|
|
85
|
+
* the caller wipes the local token regardless, so a failure here only means the
|
|
86
|
+
* server list clears on `last_seen_at` staleness instead of immediately. */
|
|
87
|
+
async function disconnectDevice(cfg) {
|
|
88
|
+
const res = await fetch(`${cfg.serverUrl}/agent/disconnect`, {
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers: authHeaders(cfg.deviceToken),
|
|
91
|
+
});
|
|
92
|
+
await throwIfNotOk(res);
|
|
93
|
+
}
|
|
79
94
|
/** Long-poll claim the next queued job for this device's owner. `null` on 204 (nothing queued). */
|
|
80
95
|
async function claimNextJob(cfg) {
|
|
81
96
|
const res = await fetch(`${cfg.serverUrl}/agent/jobs/next`, {
|
|
@@ -87,6 +102,26 @@ async function claimNextJob(cfg) {
|
|
|
87
102
|
await throwIfNotOk(res);
|
|
88
103
|
return (await res.json());
|
|
89
104
|
}
|
|
105
|
+
/** Claim the next queued auth-capture for this device's owner. `null` on 204. */
|
|
106
|
+
async function claimNextCapture(cfg) {
|
|
107
|
+
const res = await fetch(`${cfg.serverUrl}/agent/auth/next`, {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: authHeaders(cfg.deviceToken),
|
|
110
|
+
});
|
|
111
|
+
if (res.status === 204)
|
|
112
|
+
return null;
|
|
113
|
+
await throwIfNotOk(res);
|
|
114
|
+
return (await res.json());
|
|
115
|
+
}
|
|
116
|
+
/** Report a capture's outcome so the server clears "capturing" + stamps the marker. */
|
|
117
|
+
async function postCaptureComplete(cfg, captureId, ok, error) {
|
|
118
|
+
const res = await fetch(`${cfg.serverUrl}/agent/auth/${captureId}/complete`, {
|
|
119
|
+
method: "POST",
|
|
120
|
+
headers: { ...authHeaders(cfg.deviceToken), "Content-Type": "application/json" },
|
|
121
|
+
body: JSON.stringify({ ok, error }),
|
|
122
|
+
});
|
|
123
|
+
await throwIfNotOk(res);
|
|
124
|
+
}
|
|
90
125
|
/** Push one progress event; the server re-emits it on the run's WebSocket unchanged. */
|
|
91
126
|
async function postEvent(cfg, executionId, event, payload) {
|
|
92
127
|
const res = await fetch(`${cfg.serverUrl}/agent/jobs/${executionId}/events`, {
|
|
@@ -120,6 +155,25 @@ async function postEvidence(cfg, executionId, ev) {
|
|
|
120
155
|
});
|
|
121
156
|
await throwIfNotOk(res);
|
|
122
157
|
}
|
|
158
|
+
/** Ask the server to classify + fix one failed heal attempt (Claude + KB live server-side). */
|
|
159
|
+
async function postHealFix(cfg, caseId, body) {
|
|
160
|
+
const res = await fetch(`${cfg.serverUrl}/agent/heal/${caseId}/fix`, {
|
|
161
|
+
method: "POST",
|
|
162
|
+
headers: { ...authHeaders(cfg.deviceToken), "Content-Type": "application/json" },
|
|
163
|
+
body: JSON.stringify(body),
|
|
164
|
+
});
|
|
165
|
+
await throwIfNotOk(res);
|
|
166
|
+
return (await res.json());
|
|
167
|
+
}
|
|
168
|
+
/** Persist the heal's final outcome + feed a passing DOM-grounded heal into the KB. */
|
|
169
|
+
async function postHealFinalize(cfg, caseId, body) {
|
|
170
|
+
const res = await fetch(`${cfg.serverUrl}/agent/heal/${caseId}/finalize`, {
|
|
171
|
+
method: "POST",
|
|
172
|
+
headers: { ...authHeaders(cfg.deviceToken), "Content-Type": "application/json" },
|
|
173
|
+
body: JSON.stringify(body),
|
|
174
|
+
});
|
|
175
|
+
await throwIfNotOk(res);
|
|
176
|
+
}
|
|
123
177
|
/** Finalize an execution with its aggregate counts + captured log tail. */
|
|
124
178
|
async function postComplete(cfg, executionId, body) {
|
|
125
179
|
const res = await fetch(`${cfg.serverUrl}/agent/jobs/${executionId}/complete`, {
|
package/dist/src/bus.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.bus = void 0;
|
|
4
|
+
exports.emit = emit;
|
|
5
|
+
exports.recentEvents = recentEvents;
|
|
6
|
+
/**
|
|
7
|
+
* In-process event bus bridging the job loop (runner.ts) to the local web UI
|
|
8
|
+
* (ui.ts). The runner emits structured activity events; the UI streams them to
|
|
9
|
+
* the browser over SSE. A small ring buffer lets a UI that connects mid-run
|
|
10
|
+
* replay recent activity.
|
|
11
|
+
*/
|
|
12
|
+
const node_events_1 = require("node:events");
|
|
13
|
+
const BUFFER_MAX = 300;
|
|
14
|
+
const buffer = [];
|
|
15
|
+
exports.bus = new node_events_1.EventEmitter();
|
|
16
|
+
/** Record + broadcast an agent activity event. */
|
|
17
|
+
function emit(type, data = {}) {
|
|
18
|
+
const event = { ts: Date.now(), type, ...data };
|
|
19
|
+
buffer.push(event);
|
|
20
|
+
if (buffer.length > BUFFER_MAX)
|
|
21
|
+
buffer.shift();
|
|
22
|
+
exports.bus.emit("event", event);
|
|
23
|
+
}
|
|
24
|
+
/** Snapshot of buffered events, for a UI client that just connected. */
|
|
25
|
+
function recentEvents() {
|
|
26
|
+
return buffer.slice();
|
|
27
|
+
}
|
package/dist/src/cli.js
CHANGED
|
@@ -48,22 +48,28 @@ const os = __importStar(require("os"));
|
|
|
48
48
|
const config_1 = require("./config");
|
|
49
49
|
const api_1 = require("./api");
|
|
50
50
|
const runner_1 = require("./runner");
|
|
51
|
+
const ui_1 = require("./ui");
|
|
52
|
+
/** How the user invoked this agent, for accurate "run X" hints: the bare command
|
|
53
|
+
* inside a packaged bundle, else the `npx` form of the published package. */
|
|
54
|
+
function invocation() {
|
|
55
|
+
return process.pkg ? "qagent-agent" : "npx @q-agent/agent";
|
|
56
|
+
}
|
|
51
57
|
const program = new commander_1.Command();
|
|
52
58
|
program.name("qagent-agent").description("Q-Agent Local Agent — runs Playwright locally for this machine's owner.");
|
|
53
59
|
program
|
|
54
60
|
.command("pair")
|
|
55
61
|
.description("Redeem a one-time pairing code (from the Q-Agent SPA) for a device token")
|
|
56
62
|
.argument("<code>", "pairing code shown in the SPA")
|
|
57
|
-
.option("--server <url>", "Q-Agent server base URL
|
|
63
|
+
.option("--server <url>", "Q-Agent server base URL (defaults to the server baked into this build)")
|
|
58
64
|
.option("--name <name>", "device name shown in the paired-devices list", os.hostname())
|
|
59
65
|
.action(async (code, opts) => {
|
|
60
|
-
const serverUrl = opts.server.replace(/\/+$/, "");
|
|
66
|
+
const serverUrl = (opts.server || (0, config_1.defaultServerUrl)() || "http://127.0.0.1:8787").replace(/\/+$/, "");
|
|
61
67
|
try {
|
|
62
68
|
const { deviceToken, deviceId } = await (0, api_1.redeemDevice)(serverUrl, code, opts.name);
|
|
63
69
|
const cfg = { serverUrl, deviceToken, deviceId, deviceName: opts.name };
|
|
64
70
|
(0, config_1.saveConfig)(cfg);
|
|
65
71
|
console.log(`Paired as device #${deviceId} (${opts.name}) against ${serverUrl}`);
|
|
66
|
-
console.log(
|
|
72
|
+
console.log(`Device token stored — run \`${invocation()} start\` to begin claiming jobs.`);
|
|
67
73
|
}
|
|
68
74
|
catch (err) {
|
|
69
75
|
console.error("Pairing failed:", err.message);
|
|
@@ -77,7 +83,7 @@ program
|
|
|
77
83
|
.action(async (opts) => {
|
|
78
84
|
const cfg = (0, config_1.loadConfig)();
|
|
79
85
|
if (!cfg) {
|
|
80
|
-
console.error(
|
|
86
|
+
console.error(`Not paired yet — run \`${invocation()} pair <code>\` first.`);
|
|
81
87
|
process.exitCode = 1;
|
|
82
88
|
return;
|
|
83
89
|
}
|
|
@@ -99,7 +105,7 @@ program
|
|
|
99
105
|
.action(() => {
|
|
100
106
|
const cfg = (0, config_1.loadConfig)();
|
|
101
107
|
if (!cfg) {
|
|
102
|
-
console.log(
|
|
108
|
+
console.log(`Not paired. Run \`${invocation()} pair <code>\` to pair this machine.`);
|
|
103
109
|
return;
|
|
104
110
|
}
|
|
105
111
|
console.log(`Paired as device #${cfg.deviceId} (${cfg.deviceName})`);
|
|
@@ -110,7 +116,15 @@ program
|
|
|
110
116
|
.description("Forget the paired device token")
|
|
111
117
|
.action(() => {
|
|
112
118
|
(0, config_1.clearConfig)();
|
|
113
|
-
console.log(
|
|
119
|
+
console.log(`Device token cleared. Run \`${invocation()} pair <code>\` to pair again.`);
|
|
120
|
+
});
|
|
121
|
+
program
|
|
122
|
+
.command("ui", { isDefault: true })
|
|
123
|
+
.description("Open the local web UI to pair and watch progress in your browser (default)")
|
|
124
|
+
.option("--port <port>", "port for the local UI server (default 7420)")
|
|
125
|
+
.option("--no-open", "start the UI server without opening a browser")
|
|
126
|
+
.action((opts) => {
|
|
127
|
+
(0, ui_1.startUi)({ port: opts.port ? Number(opts.port) : undefined, open: opts.open !== false });
|
|
114
128
|
});
|
|
115
129
|
program.parseAsync(process.argv).catch((err) => {
|
|
116
130
|
console.error(err);
|
package/dist/src/config.js
CHANGED
|
@@ -38,6 +38,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
38
38
|
};
|
|
39
39
|
})();
|
|
40
40
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.defaultServerUrl = defaultServerUrl;
|
|
41
42
|
exports.configDir = configDir;
|
|
42
43
|
exports.loadConfig = loadConfig;
|
|
43
44
|
exports.saveConfig = saveConfig;
|
|
@@ -45,6 +46,30 @@ exports.clearConfig = clearConfig;
|
|
|
45
46
|
const fs = __importStar(require("fs"));
|
|
46
47
|
const os = __importStar(require("os"));
|
|
47
48
|
const path = __importStar(require("path"));
|
|
49
|
+
/** The server URL baked into this build, so the packaged desktop app can
|
|
50
|
+
* pre-fill it and the user only types the 6-digit code (no URL to enter).
|
|
51
|
+
*
|
|
52
|
+
* Resolution: a runtime ``QAGENT_SERVER`` env override wins (dev/testing), then
|
|
53
|
+
* the ``qagentServer`` field electron-builder merges into the packaged
|
|
54
|
+
* ``package.json`` (see build.extraMetadata). Empty for the generic npm/npx
|
|
55
|
+
* build — those callers still pass ``--server`` (or type the URL) as before. */
|
|
56
|
+
function defaultServerUrl() {
|
|
57
|
+
const fromEnv = (process.env.QAGENT_SERVER || "").trim();
|
|
58
|
+
if (fromEnv)
|
|
59
|
+
return fromEnv.replace(/\/+$/, "");
|
|
60
|
+
try {
|
|
61
|
+
// dist/src/config.js → ../../package.json (the app's own manifest, which in
|
|
62
|
+
// a packaged build carries the baked-in qagentServer).
|
|
63
|
+
const pkgPath = path.join(__dirname, "..", "..", "package.json");
|
|
64
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
65
|
+
if (typeof pkg.qagentServer === "string")
|
|
66
|
+
return pkg.qagentServer.trim().replace(/\/+$/, "");
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Generic build / manifest unreadable — no baked server.
|
|
70
|
+
}
|
|
71
|
+
return "";
|
|
72
|
+
}
|
|
48
73
|
/** Directory holding the agent's persisted config + local session store. */
|
|
49
74
|
function configDir() {
|
|
50
75
|
return path.join(os.homedir(), ".qagent-agent");
|
|
@@ -44,19 +44,7 @@ exports.ensureChromium = ensureChromium;
|
|
|
44
44
|
*/
|
|
45
45
|
const child_process_1 = require("child_process");
|
|
46
46
|
const fs = __importStar(require("fs"));
|
|
47
|
-
const
|
|
48
|
-
function firstExisting(paths) {
|
|
49
|
-
for (const p of paths) {
|
|
50
|
-
if (fs.existsSync(p))
|
|
51
|
-
return p;
|
|
52
|
-
}
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
/** The agent package's own node_modules (mirrors runner.ts's resolver). */
|
|
56
|
-
function agentNodeModules() {
|
|
57
|
-
return (firstExisting([path.join(__dirname, "..", "..", "node_modules"), path.join(__dirname, "..", "node_modules")]) ??
|
|
58
|
-
path.join(__dirname, "..", "..", "node_modules"));
|
|
59
|
-
}
|
|
47
|
+
const paths_1 = require("./paths");
|
|
60
48
|
/** Path Playwright expects the Chromium build at, or null if it can't be resolved. */
|
|
61
49
|
function chromiumExecutable() {
|
|
62
50
|
try {
|
|
@@ -72,7 +60,9 @@ function chromiumExecutable() {
|
|
|
72
60
|
* Ensure Chromium is installed, running `playwright install chromium` if it's missing.
|
|
73
61
|
*
|
|
74
62
|
* Fast no-op once the browser exists. Streams the download progress so a first-run
|
|
75
|
-
* fetch (~100 MB) is visible rather than a silent hang.
|
|
63
|
+
* fetch (~100 MB) is visible rather than a silent hang. Invokes Playwright's CLI
|
|
64
|
+
* through the resolved node runtime (`node cli.js install chromium`) so it works
|
|
65
|
+
* from source, via npx, and in a packaged bundle alike.
|
|
76
66
|
*
|
|
77
67
|
* Returns:
|
|
78
68
|
* true when Chromium is available (already present, or installed successfully);
|
|
@@ -83,13 +73,11 @@ async function ensureChromium() {
|
|
|
83
73
|
if (exe && fs.existsSync(exe))
|
|
84
74
|
return true;
|
|
85
75
|
console.log("Chromium not found — installing Playwright's Chromium (one-time download)...");
|
|
86
|
-
const nm = agentNodeModules();
|
|
87
|
-
const bin = path.join(nm, ".bin", process.platform === "win32" ? "playwright.cmd" : "playwright");
|
|
88
|
-
const useBin = fs.existsSync(bin);
|
|
89
|
-
const cmd = useBin ? bin : "npx";
|
|
90
|
-
const args = useBin ? ["install", "chromium"] : ["playwright", "install", "chromium"];
|
|
91
76
|
const code = await new Promise((resolve) => {
|
|
92
|
-
const child = (0, child_process_1.spawn)(
|
|
77
|
+
const child = (0, child_process_1.spawn)((0, paths_1.nodeBin)(), [(0, paths_1.playwrightCli)(), "install", "chromium"], {
|
|
78
|
+
stdio: "inherit",
|
|
79
|
+
env: { ...process.env, ...(0, paths_1.childNodeEnv)() },
|
|
80
|
+
});
|
|
93
81
|
child.on("close", resolve);
|
|
94
82
|
child.on("error", () => resolve(null));
|
|
95
83
|
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.firstExisting = firstExisting;
|
|
37
|
+
exports.isElectron = isElectron;
|
|
38
|
+
exports.childNodeEnv = childNodeEnv;
|
|
39
|
+
exports.packagedRoot = packagedRoot;
|
|
40
|
+
exports.nodeBin = nodeBin;
|
|
41
|
+
exports.agentNodeModules = agentNodeModules;
|
|
42
|
+
exports.playwrightCli = playwrightCli;
|
|
43
|
+
exports.vendorCaptureScript = vendorCaptureScript;
|
|
44
|
+
/**
|
|
45
|
+
* Runtime path resolution for the agent's child processes (Playwright CLI +
|
|
46
|
+
* the headed-login capture script), working in three layouts:
|
|
47
|
+
*
|
|
48
|
+
* 1. from source / `npx @q-agent/agent` — `node` on PATH, deps in the package's
|
|
49
|
+
* own node_modules;
|
|
50
|
+
* 2. a packaged Windows bundle (built by `scripts/package-win.mjs`, run as a
|
|
51
|
+
* `@yao-pkg/pkg` binary) — a bundled `node.exe`, `node_modules/`, and
|
|
52
|
+
* `vendor/` sit next to the executable.
|
|
53
|
+
*
|
|
54
|
+
* Everything the agent spawns is invoked as `nodeBin() <script/cli.js> …`, so
|
|
55
|
+
* the same code path works whether `node` comes from PATH or the bundle.
|
|
56
|
+
*/
|
|
57
|
+
const fs = __importStar(require("fs"));
|
|
58
|
+
const path = __importStar(require("path"));
|
|
59
|
+
function firstExisting(candidates) {
|
|
60
|
+
for (const c of candidates) {
|
|
61
|
+
if (fs.existsSync(c))
|
|
62
|
+
return c;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
/** True when running inside Electron (the desktop app) rather than plain Node. */
|
|
67
|
+
function isElectron() {
|
|
68
|
+
return Boolean(process.versions?.electron);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Environment additions for spawned child node processes. Under Electron,
|
|
72
|
+
* `process.execPath` is the Electron binary, so `nodeBin()` returns it — set
|
|
73
|
+
* ELECTRON_RUN_AS_NODE=1 so it runs the given script as Node rather than
|
|
74
|
+
* launching another app window.
|
|
75
|
+
*/
|
|
76
|
+
function childNodeEnv() {
|
|
77
|
+
return isElectron() ? { ELECTRON_RUN_AS_NODE: "1" } : {};
|
|
78
|
+
}
|
|
79
|
+
/** True when running as a Node Single Executable Application (the packaged .exe). */
|
|
80
|
+
function isSea() {
|
|
81
|
+
try {
|
|
82
|
+
return require("node:sea").isSea();
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The folder beside the executable in a packaged bundle (holds the bundled
|
|
90
|
+
* node runtime, node_modules, and vendor/). Null when running from source/npx.
|
|
91
|
+
* Set when running as a SEA binary (the Windows bundle) — or a pkg binary.
|
|
92
|
+
*/
|
|
93
|
+
function packagedRoot() {
|
|
94
|
+
return isSea() || process.pkg ? path.dirname(process.execPath) : null;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Node runtime used to spawn child processes. Prefers the bundled `node.exe`
|
|
98
|
+
* in a packaged install (so no global Node is required), else `node` from PATH.
|
|
99
|
+
*/
|
|
100
|
+
function nodeBin() {
|
|
101
|
+
const root = packagedRoot();
|
|
102
|
+
if (root) {
|
|
103
|
+
const bundled = path.join(root, process.platform === "win32" ? "node.exe" : "node");
|
|
104
|
+
if (fs.existsSync(bundled))
|
|
105
|
+
return bundled;
|
|
106
|
+
}
|
|
107
|
+
// From source / npx, the agent itself runs under a real Node — reuse it. An
|
|
108
|
+
// absolute path lets children spawn without a shell (safe with spaces in the
|
|
109
|
+
// path); `process.execPath` is only the pkg binary when packagedRoot() is set.
|
|
110
|
+
return process.execPath;
|
|
111
|
+
}
|
|
112
|
+
/** node_modules holding `playwright` + `@playwright/test`. */
|
|
113
|
+
function agentNodeModules() {
|
|
114
|
+
const root = packagedRoot();
|
|
115
|
+
if (root) {
|
|
116
|
+
const nm = path.join(root, "node_modules");
|
|
117
|
+
if (fs.existsSync(nm))
|
|
118
|
+
return nm;
|
|
119
|
+
}
|
|
120
|
+
// From source / npx / global install, let Node's own resolver find the
|
|
121
|
+
// node_modules that actually holds the deps. npm (and `npx`) HOIST
|
|
122
|
+
// `@playwright/test` and `playwright` to a parent node_modules — the npx
|
|
123
|
+
// cache root or the global root — rather than nesting them under the package,
|
|
124
|
+
// so a hardcoded relative guess misses them (the cause of the
|
|
125
|
+
// "Cannot find module '@playwright/test'" failure on npx/global installs).
|
|
126
|
+
// `.../node_modules/@playwright/test/package.json` → `.../node_modules`.
|
|
127
|
+
const testPkgJson = require.resolve("@playwright/test/package.json");
|
|
128
|
+
return path.dirname(path.dirname(path.dirname(testPkgJson)));
|
|
129
|
+
}
|
|
130
|
+
/** Playwright's CLI entry — drives both `test` (run specs) and `install` (browsers). */
|
|
131
|
+
function playwrightCli() {
|
|
132
|
+
const root = packagedRoot();
|
|
133
|
+
if (root) {
|
|
134
|
+
const bundled = path.join(root, "node_modules", "playwright", "cli.js");
|
|
135
|
+
if (fs.existsSync(bundled))
|
|
136
|
+
return bundled;
|
|
137
|
+
}
|
|
138
|
+
// Resolve via Node so it works whether npm nested or hoisted `playwright`.
|
|
139
|
+
return path.join(path.dirname(require.resolve("playwright/package.json")), "cli.js");
|
|
140
|
+
}
|
|
141
|
+
/** The vendored headed-login capture script (`capture_auth.cjs`). */
|
|
142
|
+
function vendorCaptureScript() {
|
|
143
|
+
const root = packagedRoot();
|
|
144
|
+
const found = firstExisting([
|
|
145
|
+
...(root ? [path.join(root, "vendor", "capture_auth.cjs")] : []),
|
|
146
|
+
path.join(__dirname, "..", "..", "vendor", "capture_auth.cjs"),
|
|
147
|
+
path.join(__dirname, "..", "vendor", "capture_auth.cjs"),
|
|
148
|
+
]);
|
|
149
|
+
if (!found)
|
|
150
|
+
throw new Error("capture_auth.cjs not found — the agent package is missing vendor/");
|
|
151
|
+
return found;
|
|
152
|
+
}
|