@q-agent/agent 0.1.0 → 0.1.1
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 +24 -0
- package/dist/src/bus.js +27 -0
- package/dist/src/cli.js +9 -4
- package/dist/src/ensureBrowser.js +5 -20
- package/dist/src/paths.js +137 -0
- package/dist/src/runner.js +13 -38
- package/dist/src/ui.js +390 -0
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -38,6 +38,30 @@ node dist/src/cli.js pair <code> --server http://127.0.0.1:8787
|
|
|
38
38
|
node dist/src/cli.js start
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
## Windows binary (no Node required)
|
|
42
|
+
|
|
43
|
+
For machines without Node, build a self-contained Windows bundle:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
cd agent
|
|
47
|
+
npm install
|
|
48
|
+
npm run package:win
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
This produces `dist-bin/qagent-agent-win-x64/` — a `qagent-agent.exe` (the CLI as
|
|
52
|
+
a Node Single Executable Application) plus a bundled Node runtime, production
|
|
53
|
+
`node_modules`, and `vendor/`. Zip that folder for distribution; the user unzips
|
|
54
|
+
and runs it with **no global Node installed**:
|
|
55
|
+
|
|
56
|
+
```bat
|
|
57
|
+
qagent-agent.exe pair <code> --server https://your-qagent-server.example.com/api
|
|
58
|
+
qagent-agent.exe start
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Chromium still auto-installs on first `start`. Build the bundle on Windows with a
|
|
62
|
+
Node 20+ that supports SEA (the local `node.exe` is used as the executable base —
|
|
63
|
+
no compiler or download needed).
|
|
64
|
+
|
|
41
65
|
## Commands
|
|
42
66
|
|
|
43
67
|
| Command | Description |
|
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,6 +48,11 @@ 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
|
+
/** How the user invoked this agent, for accurate "run X" hints: the bare command
|
|
52
|
+
* inside a packaged bundle, else the `npx` form of the published package. */
|
|
53
|
+
function invocation() {
|
|
54
|
+
return process.pkg ? "qagent-agent" : "npx @q-agent/agent";
|
|
55
|
+
}
|
|
51
56
|
const program = new commander_1.Command();
|
|
52
57
|
program.name("qagent-agent").description("Q-Agent Local Agent — runs Playwright locally for this machine's owner.");
|
|
53
58
|
program
|
|
@@ -63,7 +68,7 @@ program
|
|
|
63
68
|
const cfg = { serverUrl, deviceToken, deviceId, deviceName: opts.name };
|
|
64
69
|
(0, config_1.saveConfig)(cfg);
|
|
65
70
|
console.log(`Paired as device #${deviceId} (${opts.name}) against ${serverUrl}`);
|
|
66
|
-
console.log(
|
|
71
|
+
console.log(`Device token stored — run \`${invocation()} start\` to begin claiming jobs.`);
|
|
67
72
|
}
|
|
68
73
|
catch (err) {
|
|
69
74
|
console.error("Pairing failed:", err.message);
|
|
@@ -77,7 +82,7 @@ program
|
|
|
77
82
|
.action(async (opts) => {
|
|
78
83
|
const cfg = (0, config_1.loadConfig)();
|
|
79
84
|
if (!cfg) {
|
|
80
|
-
console.error(
|
|
85
|
+
console.error(`Not paired yet — run \`${invocation()} pair <code>\` first.`);
|
|
81
86
|
process.exitCode = 1;
|
|
82
87
|
return;
|
|
83
88
|
}
|
|
@@ -99,7 +104,7 @@ program
|
|
|
99
104
|
.action(() => {
|
|
100
105
|
const cfg = (0, config_1.loadConfig)();
|
|
101
106
|
if (!cfg) {
|
|
102
|
-
console.log(
|
|
107
|
+
console.log(`Not paired. Run \`${invocation()} pair <code>\` to pair this machine.`);
|
|
103
108
|
return;
|
|
104
109
|
}
|
|
105
110
|
console.log(`Paired as device #${cfg.deviceId} (${cfg.deviceName})`);
|
|
@@ -110,7 +115,7 @@ program
|
|
|
110
115
|
.description("Forget the paired device token")
|
|
111
116
|
.action(() => {
|
|
112
117
|
(0, config_1.clearConfig)();
|
|
113
|
-
console.log(
|
|
118
|
+
console.log(`Device token cleared. Run \`${invocation()} pair <code>\` to pair again.`);
|
|
114
119
|
});
|
|
115
120
|
program.parseAsync(process.argv).catch((err) => {
|
|
116
121
|
console.error(err);
|
|
@@ -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,8 @@ 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"], { stdio: "inherit" });
|
|
93
78
|
child.on("close", resolve);
|
|
94
79
|
child.on("error", () => resolve(null));
|
|
95
80
|
});
|
|
@@ -0,0 +1,137 @@
|
|
|
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.packagedRoot = packagedRoot;
|
|
38
|
+
exports.nodeBin = nodeBin;
|
|
39
|
+
exports.agentNodeModules = agentNodeModules;
|
|
40
|
+
exports.playwrightCli = playwrightCli;
|
|
41
|
+
exports.vendorCaptureScript = vendorCaptureScript;
|
|
42
|
+
/**
|
|
43
|
+
* Runtime path resolution for the agent's child processes (Playwright CLI +
|
|
44
|
+
* the headed-login capture script), working in three layouts:
|
|
45
|
+
*
|
|
46
|
+
* 1. from source / `npx @q-agent/agent` — `node` on PATH, deps in the package's
|
|
47
|
+
* own node_modules;
|
|
48
|
+
* 2. a packaged Windows bundle (built by `scripts/package-win.mjs`, run as a
|
|
49
|
+
* `@yao-pkg/pkg` binary) — a bundled `node.exe`, `node_modules/`, and
|
|
50
|
+
* `vendor/` sit next to the executable.
|
|
51
|
+
*
|
|
52
|
+
* Everything the agent spawns is invoked as `nodeBin() <script/cli.js> …`, so
|
|
53
|
+
* the same code path works whether `node` comes from PATH or the bundle.
|
|
54
|
+
*/
|
|
55
|
+
const fs = __importStar(require("fs"));
|
|
56
|
+
const path = __importStar(require("path"));
|
|
57
|
+
function firstExisting(candidates) {
|
|
58
|
+
for (const c of candidates) {
|
|
59
|
+
if (fs.existsSync(c))
|
|
60
|
+
return c;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
/** True when running as a Node Single Executable Application (the packaged .exe). */
|
|
65
|
+
function isSea() {
|
|
66
|
+
try {
|
|
67
|
+
return require("node:sea").isSea();
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The folder beside the executable in a packaged bundle (holds the bundled
|
|
75
|
+
* node runtime, node_modules, and vendor/). Null when running from source/npx.
|
|
76
|
+
* Set when running as a SEA binary (the Windows bundle) — or a pkg binary.
|
|
77
|
+
*/
|
|
78
|
+
function packagedRoot() {
|
|
79
|
+
return isSea() || process.pkg ? path.dirname(process.execPath) : null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Node runtime used to spawn child processes. Prefers the bundled `node.exe`
|
|
83
|
+
* in a packaged install (so no global Node is required), else `node` from PATH.
|
|
84
|
+
*/
|
|
85
|
+
function nodeBin() {
|
|
86
|
+
const root = packagedRoot();
|
|
87
|
+
if (root) {
|
|
88
|
+
const bundled = path.join(root, process.platform === "win32" ? "node.exe" : "node");
|
|
89
|
+
if (fs.existsSync(bundled))
|
|
90
|
+
return bundled;
|
|
91
|
+
}
|
|
92
|
+
// From source / npx, the agent itself runs under a real Node — reuse it. An
|
|
93
|
+
// absolute path lets children spawn without a shell (safe with spaces in the
|
|
94
|
+
// path); `process.execPath` is only the pkg binary when packagedRoot() is set.
|
|
95
|
+
return process.execPath;
|
|
96
|
+
}
|
|
97
|
+
/** node_modules holding `playwright` + `@playwright/test`. */
|
|
98
|
+
function agentNodeModules() {
|
|
99
|
+
const root = packagedRoot();
|
|
100
|
+
if (root) {
|
|
101
|
+
const nm = path.join(root, "node_modules");
|
|
102
|
+
if (fs.existsSync(nm))
|
|
103
|
+
return nm;
|
|
104
|
+
}
|
|
105
|
+
// From source / npx / global install, let Node's own resolver find the
|
|
106
|
+
// node_modules that actually holds the deps. npm (and `npx`) HOIST
|
|
107
|
+
// `@playwright/test` and `playwright` to a parent node_modules — the npx
|
|
108
|
+
// cache root or the global root — rather than nesting them under the package,
|
|
109
|
+
// so a hardcoded relative guess misses them (the cause of the
|
|
110
|
+
// "Cannot find module '@playwright/test'" failure on npx/global installs).
|
|
111
|
+
// `.../node_modules/@playwright/test/package.json` → `.../node_modules`.
|
|
112
|
+
const testPkgJson = require.resolve("@playwright/test/package.json");
|
|
113
|
+
return path.dirname(path.dirname(path.dirname(testPkgJson)));
|
|
114
|
+
}
|
|
115
|
+
/** Playwright's CLI entry — drives both `test` (run specs) and `install` (browsers). */
|
|
116
|
+
function playwrightCli() {
|
|
117
|
+
const root = packagedRoot();
|
|
118
|
+
if (root) {
|
|
119
|
+
const bundled = path.join(root, "node_modules", "playwright", "cli.js");
|
|
120
|
+
if (fs.existsSync(bundled))
|
|
121
|
+
return bundled;
|
|
122
|
+
}
|
|
123
|
+
// Resolve via Node so it works whether npm nested or hoisted `playwright`.
|
|
124
|
+
return path.join(path.dirname(require.resolve("playwright/package.json")), "cli.js");
|
|
125
|
+
}
|
|
126
|
+
/** The vendored headed-login capture script (`capture_auth.cjs`). */
|
|
127
|
+
function vendorCaptureScript() {
|
|
128
|
+
const root = packagedRoot();
|
|
129
|
+
const found = firstExisting([
|
|
130
|
+
...(root ? [path.join(root, "vendor", "capture_auth.cjs")] : []),
|
|
131
|
+
path.join(__dirname, "..", "..", "vendor", "capture_auth.cjs"),
|
|
132
|
+
path.join(__dirname, "..", "vendor", "capture_auth.cjs"),
|
|
133
|
+
]);
|
|
134
|
+
if (!found)
|
|
135
|
+
throw new Error("capture_auth.cjs not found — the agent package is missing vendor/");
|
|
136
|
+
return found;
|
|
137
|
+
}
|
package/dist/src/runner.js
CHANGED
|
@@ -49,6 +49,7 @@ const os = __importStar(require("os"));
|
|
|
49
49
|
const path = __importStar(require("path"));
|
|
50
50
|
const api = __importStar(require("./api"));
|
|
51
51
|
const ensureBrowser_1 = require("./ensureBrowser");
|
|
52
|
+
const paths_1 = require("./paths");
|
|
52
53
|
const playwrightConfig_1 = require("./playwrightConfig");
|
|
53
54
|
const report_1 = require("./report");
|
|
54
55
|
const session_1 = require("./session");
|
|
@@ -68,35 +69,15 @@ function killActiveChild() {
|
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
|
-
function firstExisting(candidates) {
|
|
72
|
-
for (const c of candidates) {
|
|
73
|
-
if (fs.existsSync(c))
|
|
74
|
-
return c;
|
|
75
|
-
}
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
/** Resolve the vendored capture_auth.cjs, whether running from `dist/` or via ts-node from `src/`. */
|
|
79
|
-
function vendorCaptureScript() {
|
|
80
|
-
const found = firstExisting([
|
|
81
|
-
path.join(__dirname, "..", "..", "vendor", "capture_auth.cjs"),
|
|
82
|
-
path.join(__dirname, "..", "vendor", "capture_auth.cjs"),
|
|
83
|
-
]);
|
|
84
|
-
if (!found)
|
|
85
|
-
throw new Error("capture_auth.cjs not found — the agent package is missing vendor/");
|
|
86
|
-
return found;
|
|
87
|
-
}
|
|
88
|
-
/** The agent package's own node_modules, where @playwright/test + playwright are installed. */
|
|
89
|
-
function agentNodeModules() {
|
|
90
|
-
return (firstExisting([path.join(__dirname, "..", "..", "node_modules"), path.join(__dirname, "..", "node_modules")]) ??
|
|
91
|
-
path.join(__dirname, "..", "..", "node_modules"));
|
|
92
|
-
}
|
|
93
72
|
function nodePathEnv(nm) {
|
|
94
73
|
const existing = process.env.NODE_PATH;
|
|
95
74
|
return { ...process.env, NODE_PATH: existing ? `${nm}${path.delimiter}${existing}` : nm };
|
|
96
75
|
}
|
|
97
76
|
function runProcess(cmd, args, cwd, env, timeoutMs) {
|
|
98
77
|
return new Promise((resolve) => {
|
|
99
|
-
|
|
78
|
+
// No shell: cmd is always an absolute node path (nodeBin()) and args are
|
|
79
|
+
// absolute script/CLI paths, so this is safe even when paths contain spaces.
|
|
80
|
+
const child = (0, child_process_1.spawn)(cmd, args, { cwd, env });
|
|
100
81
|
activeChild = child;
|
|
101
82
|
let stdout = "";
|
|
102
83
|
let stderr = "";
|
|
@@ -134,9 +115,9 @@ function runProcess(cmd, args, cwd, env, timeoutMs) {
|
|
|
134
115
|
*/
|
|
135
116
|
async function captureAuth(baseUrl, storageStatePath, sessionStoragePath) {
|
|
136
117
|
fs.mkdirSync(path.dirname(storageStatePath), { recursive: true });
|
|
137
|
-
const script = vendorCaptureScript();
|
|
138
|
-
const nm = agentNodeModules();
|
|
139
|
-
const result = await runProcess(
|
|
118
|
+
const script = (0, paths_1.vendorCaptureScript)();
|
|
119
|
+
const nm = (0, paths_1.agentNodeModules)();
|
|
120
|
+
const result = await runProcess((0, paths_1.nodeBin)(), [script, baseUrl, storageStatePath, sessionStoragePath], nm, nodePathEnv(nm), AUTH_CAPTURE_TIMEOUT_MS);
|
|
140
121
|
if (result.code !== 0) {
|
|
141
122
|
console.error(`Auth capture exited ${result.code}: ${(result.stderr || result.stdout || "").slice(0, 800)}`);
|
|
142
123
|
}
|
|
@@ -147,21 +128,15 @@ async function captureAuth(baseUrl, storageStatePath, sessionStoragePath) {
|
|
|
147
128
|
return false;
|
|
148
129
|
}
|
|
149
130
|
}
|
|
150
|
-
/** Prefer the agent's own installed `playwright` binary over a fresh `npx` fetch. */
|
|
151
|
-
function playwrightCommand() {
|
|
152
|
-
const nm = agentNodeModules();
|
|
153
|
-
const bin = path.join(nm, ".bin", process.platform === "win32" ? "playwright.cmd" : "playwright");
|
|
154
|
-
if (fs.existsSync(bin))
|
|
155
|
-
return { cmd: bin, baseArgs: ["test"] };
|
|
156
|
-
return { cmd: "npx", baseArgs: ["playwright", "test"] };
|
|
157
|
-
}
|
|
158
131
|
async function runPlaywright(workDir, workers, specFile) {
|
|
159
|
-
|
|
160
|
-
|
|
132
|
+
// Invoke Playwright's CLI through the resolved node runtime (`node cli.js test …`)
|
|
133
|
+
// so the same path works from source, via npx, and in a packaged bundle where
|
|
134
|
+
// `node`/`node_modules` live beside the executable rather than on PATH.
|
|
135
|
+
const args = [(0, paths_1.playwrightCli)(), "test", `--workers=${workers}`];
|
|
161
136
|
if (specFile)
|
|
162
137
|
args.push(specFile);
|
|
163
|
-
const nm = agentNodeModules();
|
|
164
|
-
return runProcess(
|
|
138
|
+
const nm = (0, paths_1.agentNodeModules)();
|
|
139
|
+
return runProcess((0, paths_1.nodeBin)(), args, workDir, nodePathEnv(nm), EXEC_TIMEOUT_MS);
|
|
165
140
|
}
|
|
166
141
|
/**
|
|
167
142
|
* Best identity `{ticket, caseCode}` for wire events: prefers explicit
|
package/dist/src/ui.js
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
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.startUi = startUi;
|
|
37
|
+
/**
|
|
38
|
+
* Local web UI for the agent, served on 127.0.0.1 and opened in the browser.
|
|
39
|
+
*
|
|
40
|
+
* Lets the user connect by pasting a pair code + server URL (no CLI), then shows
|
|
41
|
+
* live pulling/execution progress streamed from the job loop over SSE. Styled to
|
|
42
|
+
* match the Q-Agent app (dark, glass, violet). No framework / no extra deps —
|
|
43
|
+
* Node's built-in http plus the in-process event bus (bus.ts).
|
|
44
|
+
*/
|
|
45
|
+
const node_child_process_1 = require("node:child_process");
|
|
46
|
+
const http = __importStar(require("node:http"));
|
|
47
|
+
const os = __importStar(require("node:os"));
|
|
48
|
+
const api_1 = require("./api");
|
|
49
|
+
const bus_1 = require("./bus");
|
|
50
|
+
const config_1 = require("./config");
|
|
51
|
+
const runner_1 = require("./runner");
|
|
52
|
+
let signal = { aborted: false };
|
|
53
|
+
let running = false;
|
|
54
|
+
/** Start the claim→run loop for `cfg` (no-op if already running). */
|
|
55
|
+
function startLoop(cfg) {
|
|
56
|
+
if (running)
|
|
57
|
+
return;
|
|
58
|
+
running = true;
|
|
59
|
+
signal = { aborted: false };
|
|
60
|
+
(0, bus_1.emit)("agent-status", { running: true, deviceId: cfg.deviceId, deviceName: cfg.deviceName, serverUrl: cfg.serverUrl });
|
|
61
|
+
(0, runner_1.runAgentLoop)(cfg, signal)
|
|
62
|
+
.catch((err) => (0, bus_1.emit)("error", { message: err.message || String(err) }))
|
|
63
|
+
.finally(() => {
|
|
64
|
+
running = false;
|
|
65
|
+
(0, bus_1.emit)("agent-status", { running: false });
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function stopLoop() {
|
|
69
|
+
signal.aborted = true;
|
|
70
|
+
(0, runner_1.killActiveChild)();
|
|
71
|
+
running = false;
|
|
72
|
+
}
|
|
73
|
+
function state() {
|
|
74
|
+
const cfg = (0, config_1.loadConfig)();
|
|
75
|
+
return {
|
|
76
|
+
paired: Boolean(cfg),
|
|
77
|
+
running,
|
|
78
|
+
deviceId: cfg?.deviceId ?? null,
|
|
79
|
+
deviceName: cfg?.deviceName ?? null,
|
|
80
|
+
serverUrl: cfg?.serverUrl ?? "",
|
|
81
|
+
events: (0, bus_1.recentEvents)(),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function readBody(req) {
|
|
85
|
+
return new Promise((resolve) => {
|
|
86
|
+
let data = "";
|
|
87
|
+
req.on("data", (c) => (data += c));
|
|
88
|
+
req.on("end", () => resolve(data));
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function sendJson(res, code, body) {
|
|
92
|
+
const s = JSON.stringify(body);
|
|
93
|
+
res.writeHead(code, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(s) });
|
|
94
|
+
res.end(s);
|
|
95
|
+
}
|
|
96
|
+
function openBrowser(url) {
|
|
97
|
+
try {
|
|
98
|
+
const opts = { detached: true, stdio: "ignore" };
|
|
99
|
+
if (process.platform === "win32")
|
|
100
|
+
(0, node_child_process_1.spawn)("cmd", ["/c", "start", "", url], opts).unref();
|
|
101
|
+
else if (process.platform === "darwin")
|
|
102
|
+
(0, node_child_process_1.spawn)("open", [url], opts).unref();
|
|
103
|
+
else
|
|
104
|
+
(0, node_child_process_1.spawn)("xdg-open", [url], opts).unref();
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Non-fatal: the URL is printed to the console as a fallback.
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function handle(req, res) {
|
|
111
|
+
const url = req.url || "/";
|
|
112
|
+
if (req.method === "GET" && (url === "/" || url.startsWith("/?"))) {
|
|
113
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
114
|
+
res.end(PAGE);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (req.method === "GET" && url === "/api/state") {
|
|
118
|
+
sendJson(res, 200, state());
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (req.method === "POST" && url === "/api/pair") {
|
|
122
|
+
try {
|
|
123
|
+
const { code, server } = JSON.parse((await readBody(req)) || "{}");
|
|
124
|
+
const serverUrl = String(server || "").replace(/\/+$/, "");
|
|
125
|
+
if (!code || !serverUrl)
|
|
126
|
+
return sendJson(res, 400, { error: "Pair code and server URL are required." });
|
|
127
|
+
const name = os.hostname();
|
|
128
|
+
const { deviceToken, deviceId } = await (0, api_1.redeemDevice)(serverUrl, String(code).trim(), name);
|
|
129
|
+
const cfg = { serverUrl, deviceToken, deviceId, deviceName: name };
|
|
130
|
+
(0, config_1.saveConfig)(cfg);
|
|
131
|
+
(0, bus_1.emit)("log", { message: `Paired as device #${deviceId} (${name})` });
|
|
132
|
+
startLoop(cfg);
|
|
133
|
+
sendJson(res, 200, { ok: true, deviceId });
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
sendJson(res, 400, { error: err.message || "Pairing failed" });
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (req.method === "POST" && url === "/api/disconnect") {
|
|
141
|
+
stopLoop();
|
|
142
|
+
(0, config_1.clearConfig)();
|
|
143
|
+
(0, bus_1.emit)("agent-status", { running: false });
|
|
144
|
+
sendJson(res, 200, { ok: true });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (req.method === "GET" && url === "/api/events") {
|
|
148
|
+
res.writeHead(200, {
|
|
149
|
+
"Content-Type": "text/event-stream",
|
|
150
|
+
"Cache-Control": "no-cache",
|
|
151
|
+
Connection: "keep-alive",
|
|
152
|
+
});
|
|
153
|
+
res.write(": connected\n\n");
|
|
154
|
+
for (const ev of (0, bus_1.recentEvents)())
|
|
155
|
+
res.write(`data: ${JSON.stringify(ev)}\n\n`);
|
|
156
|
+
const onEvent = (ev) => res.write(`data: ${JSON.stringify(ev)}\n\n`);
|
|
157
|
+
bus_1.bus.on("event", onEvent);
|
|
158
|
+
const ka = setInterval(() => res.write(": keep-alive\n\n"), 25_000);
|
|
159
|
+
req.on("close", () => {
|
|
160
|
+
clearInterval(ka);
|
|
161
|
+
bus_1.bus.off("event", onEvent);
|
|
162
|
+
});
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
166
|
+
res.end("Not found");
|
|
167
|
+
}
|
|
168
|
+
/** Start the UI server, begin the loop if already paired, and open the browser. */
|
|
169
|
+
function startUi(opts = {}) {
|
|
170
|
+
let port = opts.port ?? 7420;
|
|
171
|
+
const server = http.createServer((req, res) => {
|
|
172
|
+
handle(req, res).catch(() => {
|
|
173
|
+
if (!res.headersSent)
|
|
174
|
+
res.writeHead(500);
|
|
175
|
+
res.end();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
server.on("error", (err) => {
|
|
179
|
+
if (err.code === "EADDRINUSE" && port < 7440) {
|
|
180
|
+
port += 1;
|
|
181
|
+
server.listen(port, "127.0.0.1");
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
console.error("UI server error:", err.message);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
server.listen(port, "127.0.0.1", () => {
|
|
188
|
+
const addr = `http://127.0.0.1:${port}`;
|
|
189
|
+
console.log(`Local Agent UI → ${addr}`);
|
|
190
|
+
const cfg = (0, config_1.loadConfig)();
|
|
191
|
+
if (cfg)
|
|
192
|
+
startLoop(cfg);
|
|
193
|
+
if (opts.open !== false)
|
|
194
|
+
openBrowser(addr);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
// ---------------------------------------------------------------- the page
|
|
198
|
+
const PAGE = `<!doctype html>
|
|
199
|
+
<html lang="en">
|
|
200
|
+
<head>
|
|
201
|
+
<meta charset="utf-8" />
|
|
202
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
203
|
+
<title>Q-Agent · Local Agent</title>
|
|
204
|
+
<style>
|
|
205
|
+
:root {
|
|
206
|
+
--bg: #0f0f16; --panel: rgba(24,24,32,.72); --border: rgba(255,255,255,.10);
|
|
207
|
+
--ink: #ececf1; --soft: #c3c3d0; --dim: #8b8b9e; --violet: #8b5cf6; --violet-2: #a78bfa;
|
|
208
|
+
--green: #34d399; --red: #f43f5e; --amber: #f59e0b;
|
|
209
|
+
}
|
|
210
|
+
* { box-sizing: border-box; }
|
|
211
|
+
body {
|
|
212
|
+
margin: 0; font: 14px/1.5 -apple-system, "Segoe UI", Roboto, sans-serif; color: var(--ink);
|
|
213
|
+
background: radial-gradient(1000px 600px at 80% -10%, rgba(139,92,246,.18), transparent 60%), var(--bg);
|
|
214
|
+
min-height: 100vh;
|
|
215
|
+
}
|
|
216
|
+
.wrap { max-width: 760px; margin: 0 auto; padding: 40px 20px 60px; }
|
|
217
|
+
.brand { display: flex; align-items: center; gap: 11px; margin-bottom: 22px; }
|
|
218
|
+
.logo { width: 38px; height: 38px; border-radius: 11px; background: linear-gradient(135deg,#8b5cf6,#f59e0b);
|
|
219
|
+
display: flex; align-items: center; justify-content: center; font-size: 20px; }
|
|
220
|
+
.brand h1 { margin: 0; font-size: 19px; font-weight: 800; letter-spacing: -.02em; }
|
|
221
|
+
.brand .sub { font-size: 12px; color: var(--dim); }
|
|
222
|
+
.card { background: var(--panel); border: 1px solid var(--border); border-radius: 16px; padding: 18px;
|
|
223
|
+
margin-bottom: 16px; backdrop-filter: blur(8px); box-shadow: 0 30px 70px -30px #000; }
|
|
224
|
+
.card h2 { margin: 0 0 14px; font-size: 11px; letter-spacing: .1em; color: var(--dim); font-weight: 800; }
|
|
225
|
+
label { display: block; font-size: 12px; color: var(--soft); margin: 12px 0 5px; }
|
|
226
|
+
input, textarea { width: 100%; background: rgba(0,0,0,.30); border: 1px solid var(--border); border-radius: 10px;
|
|
227
|
+
color: var(--ink); padding: 10px 12px; font: inherit; }
|
|
228
|
+
input:focus, textarea:focus { outline: none; border-color: var(--violet); }
|
|
229
|
+
textarea { resize: vertical; min-height: 60px; font-family: ui-monospace, monospace; font-size: 12px; }
|
|
230
|
+
button { cursor: pointer; border: none; border-radius: 10px; font: inherit; font-weight: 700; padding: 10px 16px; }
|
|
231
|
+
.primary { background: linear-gradient(135deg,#8b5cf6,#7c3aed); color: #fff; }
|
|
232
|
+
.primary:disabled { opacity: .6; cursor: default; }
|
|
233
|
+
.ghost { background: rgba(255,255,255,.06); color: var(--soft); }
|
|
234
|
+
.row { display: flex; align-items: center; gap: 10px; }
|
|
235
|
+
.pill { font-size: 11px; font-weight: 700; padding: 3px 10px; border-radius: 999px; display: inline-flex; align-items: center; gap: 6px; }
|
|
236
|
+
.dot { width: 7px; height: 7px; border-radius: 50%; }
|
|
237
|
+
.err { color: var(--red); font-size: 12px; margin-top: 10px; min-height: 16px; }
|
|
238
|
+
.mut { color: var(--dim); font-size: 12px; }
|
|
239
|
+
.bar { height: 8px; border-radius: 999px; background: rgba(255,255,255,.08); overflow: hidden; margin: 12px 0 6px; }
|
|
240
|
+
.bar > i { display: block; height: 100%; background: linear-gradient(90deg,#8b5cf6,#a78bfa); width: 0; transition: width .3s; }
|
|
241
|
+
.counts { display: flex; gap: 16px; font-size: 12px; }
|
|
242
|
+
.banner { background: rgba(245,158,11,.12); border: 1px solid rgba(245,158,11,.35); color: #f6c177;
|
|
243
|
+
border-radius: 10px; padding: 10px 12px; font-size: 12.5px; margin-bottom: 12px; display: none; }
|
|
244
|
+
.feed { display: flex; flex-direction: column; gap: 8px; max-height: 360px; overflow: auto; }
|
|
245
|
+
.ev { display: flex; gap: 10px; align-items: baseline; font-size: 12.5px; padding: 7px 10px; border-radius: 9px;
|
|
246
|
+
background: rgba(255,255,255,.03); }
|
|
247
|
+
.ev .t { color: var(--dim); font-family: ui-monospace, monospace; font-size: 10.5px; flex-shrink: 0; }
|
|
248
|
+
.ev .m { color: var(--soft); }
|
|
249
|
+
.ev.pass { border-left: 2px solid var(--green); } .ev.fail { border-left: 2px solid var(--red); }
|
|
250
|
+
.ev.err { border-left: 2px solid var(--red); } .ev.err .m { color: #fca5a5; }
|
|
251
|
+
.badge { font-size: 10px; font-weight: 800; padding: 1px 7px; border-radius: 999px; }
|
|
252
|
+
.badge.pass { background: rgba(52,211,153,.18); color: var(--green); }
|
|
253
|
+
.badge.fail { background: rgba(244,63,94,.18); color: var(--red); }
|
|
254
|
+
.hidden { display: none; }
|
|
255
|
+
code { background: rgba(0,0,0,.3); padding: 1px 5px; border-radius: 5px; font-size: 11.5px; color: var(--violet-2); }
|
|
256
|
+
</style>
|
|
257
|
+
</head>
|
|
258
|
+
<body>
|
|
259
|
+
<div class="wrap">
|
|
260
|
+
<div class="brand">
|
|
261
|
+
<div class="logo">⭐</div>
|
|
262
|
+
<div>
|
|
263
|
+
<h1>Q-Agent · Local Agent</h1>
|
|
264
|
+
<div class="sub">Runs Playwright on this machine — your session stays local.</div>
|
|
265
|
+
</div>
|
|
266
|
+
<div style="margin-left:auto"><span id="status" class="pill"><span class="dot"></span><span id="statusText">…</span></span></div>
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
<!-- Pairing -->
|
|
270
|
+
<div id="pairCard" class="card hidden">
|
|
271
|
+
<h2>CONNECT</h2>
|
|
272
|
+
<div class="mut">Generate a pairing code on the Q-Agent app's <b>Local Agent</b> screen, then paste it here.</div>
|
|
273
|
+
<label>Server URL</label>
|
|
274
|
+
<input id="server" placeholder="https://your-qagent-server/api" />
|
|
275
|
+
<label>Pair code</label>
|
|
276
|
+
<textarea id="code" placeholder="paste the one-time pairing code"></textarea>
|
|
277
|
+
<div class="err" id="pairErr"></div>
|
|
278
|
+
<div class="row" style="margin-top:12px"><button id="connect" class="primary">Connect</button></div>
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
<!-- Connected -->
|
|
282
|
+
<div id="connCard" class="card hidden">
|
|
283
|
+
<div class="row">
|
|
284
|
+
<div>
|
|
285
|
+
<h2 style="margin-bottom:6px">DEVICE</h2>
|
|
286
|
+
<div id="devName" style="font-weight:700"></div>
|
|
287
|
+
<div class="mut" id="devServer"></div>
|
|
288
|
+
</div>
|
|
289
|
+
<button id="disconnect" class="ghost" style="margin-left:auto">Disconnect</button>
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
<div id="runCard" class="card hidden">
|
|
294
|
+
<h2>ACTIVITY</h2>
|
|
295
|
+
<div id="banner" class="banner"></div>
|
|
296
|
+
<div id="progWrap" class="hidden">
|
|
297
|
+
<div class="bar"><i id="progBar"></i></div>
|
|
298
|
+
<div class="counts">
|
|
299
|
+
<span style="color:var(--green)">✓ <b id="cPass">0</b></span>
|
|
300
|
+
<span style="color:var(--red)">✗ <b id="cFail">0</b></span>
|
|
301
|
+
<span class="mut"><b id="cRem">0</b> remaining</span>
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
<div class="feed" id="feed"></div>
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
|
|
308
|
+
<script>
|
|
309
|
+
const $ = (id) => document.getElementById(id);
|
|
310
|
+
const time = (ts) => new Date(ts).toLocaleTimeString();
|
|
311
|
+
|
|
312
|
+
function setStatus(running, paired) {
|
|
313
|
+
const dot = $("status").querySelector(".dot");
|
|
314
|
+
if (!paired) { $("statusText").textContent = "Not connected"; dot.style.background = "#8b8b9e"; }
|
|
315
|
+
else if (running) { $("statusText").textContent = "Running"; dot.style.background = "var(--green)"; }
|
|
316
|
+
else { $("statusText").textContent = "Idle"; dot.style.background = "var(--amber)"; }
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function addEv(ev) {
|
|
320
|
+
const feed = $("feed");
|
|
321
|
+
const row = document.createElement("div");
|
|
322
|
+
row.className = "ev";
|
|
323
|
+
let m = "";
|
|
324
|
+
switch (ev.type) {
|
|
325
|
+
case "job-claimed": m = \`Claimed execution #\${ev.executionId} — run \${ev.runCode} (\${ev.total} spec\${ev.total===1?"":"s"})\`; resetProgress(ev.total); break;
|
|
326
|
+
case "auth-waiting": showBanner(\`A browser opened on this machine — log in to continue.\${ev.url?" ("+ev.url+")":""}\`); m = "Waiting for manual login…"; break;
|
|
327
|
+
case "auth-captured": hideBanner(); m = "Login captured — session saved locally."; break;
|
|
328
|
+
case "case-running": m = \`Running \${ev.index}/\${ev.total} — \${ev.ticket||""} \${ev.caseCode||""}\`; break;
|
|
329
|
+
case "case-result": {
|
|
330
|
+
row.className = "ev " + (ev.status === "pass" ? "pass" : "fail");
|
|
331
|
+
const b = document.createElement("span"); b.className = "badge " + (ev.status==="pass"?"pass":"fail"); b.textContent = ev.status.toUpperCase();
|
|
332
|
+
m = \`\${ev.ticket||""} \${ev.caseCode||""}\`;
|
|
333
|
+
row.appendChild(mkTime(ev.ts)); const mm = document.createElement("span"); mm.className="m"; mm.textContent=m; row.appendChild(mm); row.appendChild(b);
|
|
334
|
+
feed.prepend(row); return;
|
|
335
|
+
}
|
|
336
|
+
case "progress": updateProgress(ev); return;
|
|
337
|
+
case "job-complete": m = \`Execution #\${ev.executionId} complete — \${ev.passed} passed, \${ev.failed} failed\`; break;
|
|
338
|
+
case "agent-status": setStatus(ev.running, true); return;
|
|
339
|
+
case "error": row.className = "ev err"; m = ev.message || "Error"; break;
|
|
340
|
+
case "log": m = ev.message || ""; break;
|
|
341
|
+
default: return;
|
|
342
|
+
}
|
|
343
|
+
if (!m) return;
|
|
344
|
+
row.appendChild(mkTime(ev.ts));
|
|
345
|
+
const mm = document.createElement("span"); mm.className = "m"; mm.textContent = m; row.appendChild(mm);
|
|
346
|
+
feed.prepend(row);
|
|
347
|
+
}
|
|
348
|
+
function mkTime(ts){ const t=document.createElement("span"); t.className="t"; t.textContent=time(ts); return t; }
|
|
349
|
+
function resetProgress(total){ $("progWrap").classList.remove("hidden"); $("progBar").style.width="0%"; $("cPass").textContent="0"; $("cFail").textContent="0"; $("cRem").textContent=String(total||0); }
|
|
350
|
+
function updateProgress(ev){ $("progWrap").classList.remove("hidden"); $("progBar").style.width=(ev.progress||0)+"%"; $("cPass").textContent=ev.passed; $("cFail").textContent=ev.failed; $("cRem").textContent=ev.remaining; }
|
|
351
|
+
function showBanner(t){ const b=$("banner"); b.textContent=t; b.style.display="block"; }
|
|
352
|
+
function hideBanner(){ $("banner").style.display="none"; }
|
|
353
|
+
|
|
354
|
+
let es = null;
|
|
355
|
+
function openStream(){ if (es) return; es = new EventSource("/api/events"); es.onmessage = (e)=>{ try{ addEv(JSON.parse(e.data)); }catch{} }; }
|
|
356
|
+
|
|
357
|
+
async function refresh() {
|
|
358
|
+
const s = await (await fetch("/api/state")).json();
|
|
359
|
+
setStatus(s.running, s.paired);
|
|
360
|
+
if (s.paired) {
|
|
361
|
+
$("pairCard").classList.add("hidden");
|
|
362
|
+
$("connCard").classList.remove("hidden"); $("runCard").classList.remove("hidden");
|
|
363
|
+
$("devName").textContent = (s.deviceName||"This device") + " · #" + s.deviceId;
|
|
364
|
+
$("devServer").textContent = s.serverUrl;
|
|
365
|
+
$("feed").innerHTML = ""; (s.events||[]).forEach(addEv);
|
|
366
|
+
openStream();
|
|
367
|
+
} else {
|
|
368
|
+
$("pairCard").classList.remove("hidden");
|
|
369
|
+
$("connCard").classList.add("hidden"); $("runCard").classList.add("hidden");
|
|
370
|
+
if (s.serverUrl) $("server").value = s.serverUrl;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
$("connect").onclick = async () => {
|
|
375
|
+
const btn = $("connect"); btn.disabled = true; $("pairErr").textContent = "";
|
|
376
|
+
try {
|
|
377
|
+
const r = await fetch("/api/pair", { method:"POST", headers:{"Content-Type":"application/json"},
|
|
378
|
+
body: JSON.stringify({ code: $("code").value.trim(), server: $("server").value.trim() }) });
|
|
379
|
+
const j = await r.json();
|
|
380
|
+
if (!r.ok) throw new Error(j.error || "Pairing failed");
|
|
381
|
+
await refresh();
|
|
382
|
+
} catch (e) { $("pairErr").textContent = e.message; }
|
|
383
|
+
finally { btn.disabled = false; }
|
|
384
|
+
};
|
|
385
|
+
$("disconnect").onclick = async () => { await fetch("/api/disconnect", { method:"POST" }); if (es){es.close(); es=null;} await refresh(); };
|
|
386
|
+
|
|
387
|
+
refresh();
|
|
388
|
+
</script>
|
|
389
|
+
</body>
|
|
390
|
+
</html>`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@q-agent/agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
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": {
|
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
"scripts": {
|
|
26
26
|
"build": "tsc -p tsconfig.json",
|
|
27
27
|
"test": "npm run build && node --test dist/test",
|
|
28
|
-
"
|
|
28
|
+
"release": "node scripts/release.mjs",
|
|
29
|
+
"prepublishOnly": "npm run build",
|
|
30
|
+
"package:win": "node scripts/package-win.mjs"
|
|
29
31
|
},
|
|
30
32
|
"dependencies": {
|
|
31
33
|
"@playwright/test": "^1.61.1",
|
|
@@ -34,6 +36,8 @@
|
|
|
34
36
|
},
|
|
35
37
|
"devDependencies": {
|
|
36
38
|
"@types/node": "^20.14.0",
|
|
39
|
+
"esbuild": "^0.24.0",
|
|
40
|
+
"postject": "^1.0.0-alpha.6",
|
|
37
41
|
"typescript": "^5.5.4"
|
|
38
42
|
}
|
|
39
43
|
}
|