trawl-devices-agent 0.1.0
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/LICENSE +21 -0
- package/README.md +61 -0
- package/dist/auth.js +23 -0
- package/dist/auth.js.map +1 -0
- package/dist/codegen.js +41 -0
- package/dist/codegen.js.map +1 -0
- package/dist/control.js +106 -0
- package/dist/control.js.map +1 -0
- package/dist/devices.js +69 -0
- package/dist/devices.js.map +1 -0
- package/dist/guide.js +14 -0
- package/dist/guide.js.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/interpolate.js +20 -0
- package/dist/interpolate.js.map +1 -0
- package/dist/mask.js +19 -0
- package/dist/mask.js.map +1 -0
- package/dist/recorder.js +99 -0
- package/dist/recorder.js.map +1 -0
- package/dist/recorderInject.js +165 -0
- package/dist/recorderInject.js.map +1 -0
- package/dist/retention.js +21 -0
- package/dist/retention.js.map +1 -0
- package/dist/routes.js +140 -0
- package/dist/routes.js.map +1 -0
- package/dist/runner.js +373 -0
- package/dist/runner.js.map +1 -0
- package/dist/sandbox.js +80 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/server.js +100 -0
- package/dist/server.js.map +1 -0
- package/dist/sessions.js +85 -0
- package/dist/sessions.js.map +1 -0
- package/dist/steps.js +27 -0
- package/dist/steps.js.map +1 -0
- package/dist/targets.js +68 -0
- package/dist/targets.js.map +1 -0
- package/dist/traffic.js +116 -0
- package/dist/traffic.js.map +1 -0
- package/dist/transform.js +110 -0
- package/dist/transform.js.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/dist/workspace.js +50 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +50 -0
- package/skills/writing-device-scripts/SKILL.md +59 -0
package/dist/sessions.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { chromium, firefox, webkit, } from "playwright";
|
|
3
|
+
import { AgentError } from "./types.js";
|
|
4
|
+
const ENGINES = { chromium, firefox, webkit };
|
|
5
|
+
/** Playwright context options derived from a device. */
|
|
6
|
+
export function contextOptionsFor(device, trawlProxyPort) {
|
|
7
|
+
const proxy = device.proxy.mode === "trawl"
|
|
8
|
+
? { server: `http://127.0.0.1:${trawlProxyPort}` }
|
|
9
|
+
: device.proxy.mode === "custom"
|
|
10
|
+
? { server: device.proxy.url }
|
|
11
|
+
: undefined;
|
|
12
|
+
return {
|
|
13
|
+
...(proxy ? { proxy } : {}),
|
|
14
|
+
ignoreHTTPSErrors: device.ignoreHTTPSErrors,
|
|
15
|
+
viewport: device.viewport,
|
|
16
|
+
...(device.userAgent ? { userAgent: device.userAgent } : {}),
|
|
17
|
+
...(device.locale ? { locale: device.locale } : {}),
|
|
18
|
+
...(device.timezone ? { timezoneId: device.timezone } : {}),
|
|
19
|
+
...(device.storageStateFile ? { storageState: device.storageStateFile } : {}),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export class SessionStore {
|
|
23
|
+
sessions = new Map();
|
|
24
|
+
async start(device, options = {}) {
|
|
25
|
+
const engine = ENGINES[device.browser];
|
|
26
|
+
const headless = options.headless ?? device.headless;
|
|
27
|
+
let browser;
|
|
28
|
+
try {
|
|
29
|
+
browser = await engine.launch({ headless });
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
throw new AgentError("device", `failed to launch ${device.browser}: ${err.message}. ` +
|
|
33
|
+
`Install it with: pnpm exec playwright install ${device.browser}`);
|
|
34
|
+
}
|
|
35
|
+
const contextOptions = contextOptionsFor(device, options.trawlProxyPort ?? 8080);
|
|
36
|
+
if (device.video && options.videoDir)
|
|
37
|
+
contextOptions.recordVideo = { dir: options.videoDir };
|
|
38
|
+
const context = await browser.newContext(contextOptions);
|
|
39
|
+
const page = await context.newPage();
|
|
40
|
+
const live = {
|
|
41
|
+
sessionId: `s_${randomBytes(6).toString("hex")}`,
|
|
42
|
+
deviceId: device.id,
|
|
43
|
+
state: "idle",
|
|
44
|
+
startedAt: Date.now(),
|
|
45
|
+
currentUrl: null,
|
|
46
|
+
browser,
|
|
47
|
+
context,
|
|
48
|
+
page,
|
|
49
|
+
};
|
|
50
|
+
page.on("framenavigated", (frame) => {
|
|
51
|
+
if (frame === page.mainFrame())
|
|
52
|
+
live.currentUrl = frame.url();
|
|
53
|
+
});
|
|
54
|
+
this.sessions.set(live.sessionId, live);
|
|
55
|
+
return this.describe(live);
|
|
56
|
+
}
|
|
57
|
+
get(sessionId) {
|
|
58
|
+
const live = this.sessions.get(sessionId);
|
|
59
|
+
if (!live)
|
|
60
|
+
throw new AgentError("device", `unknown session: ${sessionId}`);
|
|
61
|
+
return live;
|
|
62
|
+
}
|
|
63
|
+
setState(sessionId, state) {
|
|
64
|
+
this.get(sessionId).state = state;
|
|
65
|
+
}
|
|
66
|
+
list() {
|
|
67
|
+
return [...this.sessions.values()].map((s) => this.describe(s));
|
|
68
|
+
}
|
|
69
|
+
async stop(sessionId) {
|
|
70
|
+
const live = this.sessions.get(sessionId);
|
|
71
|
+
if (!live)
|
|
72
|
+
return;
|
|
73
|
+
this.sessions.delete(sessionId);
|
|
74
|
+
await live.context.close().catch(() => { });
|
|
75
|
+
await live.browser.close().catch(() => { });
|
|
76
|
+
}
|
|
77
|
+
async stopAll() {
|
|
78
|
+
await Promise.all([...this.sessions.keys()].map((id) => this.stop(id)));
|
|
79
|
+
}
|
|
80
|
+
describe(live) {
|
|
81
|
+
const { sessionId, deviceId, state, startedAt, currentUrl } = live;
|
|
82
|
+
return { sessionId, deviceId, state, startedAt, currentUrl };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=sessions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessions.js","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"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"}
|
package/dist/steps.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** The DSL vocabulary — the single source of truth. Bump DSL_VERSION when it changes. */
|
|
2
|
+
export const STEP_NAMES = [
|
|
3
|
+
// setup
|
|
4
|
+
"device", "use",
|
|
5
|
+
// navigation
|
|
6
|
+
"goto", "back", "forward", "reload",
|
|
7
|
+
// actions
|
|
8
|
+
"click", "dblclick", "fill", "type", "press", "check", "uncheck",
|
|
9
|
+
"select", "hover", "upload", "drag", "scrollTo",
|
|
10
|
+
// waits
|
|
11
|
+
"waitFor", "waitForUrl", "waitForResponse", "sleep",
|
|
12
|
+
// UI assertions
|
|
13
|
+
"expectVisible", "expectHidden", "expectText", "expectValue",
|
|
14
|
+
"expectUrl", "expectCount", "expectAttr",
|
|
15
|
+
// HTTP assertions
|
|
16
|
+
"expectRequest", "expectResponse", "expectNoRequest",
|
|
17
|
+
// reads
|
|
18
|
+
"getText", "getValue", "getAttr", "getUrl", "count",
|
|
19
|
+
// misc
|
|
20
|
+
"step", "screenshot", "note",
|
|
21
|
+
];
|
|
22
|
+
/** Globals a script may call that are not steps. `secret` and `env` come from the run scope. */
|
|
23
|
+
export const ALLOWED_GLOBALS = [
|
|
24
|
+
"console", "JSON", "Math", "Number", "String", "Boolean", "Array", "Object",
|
|
25
|
+
"Date", "RegExp", "parseInt", "parseFloat", "isNaN", "secret",
|
|
26
|
+
];
|
|
27
|
+
//# sourceMappingURL=steps.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"steps.js","sourceRoot":"","sources":["../src/steps.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,QAAQ;IACR,QAAQ,EAAE,KAAK;IACf,aAAa;IACb,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ;IACnC,UAAU;IACV,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS;IAChE,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU;IAC/C,QAAQ;IACR,SAAS,EAAE,YAAY,EAAE,iBAAiB,EAAE,OAAO;IACnD,gBAAgB;IAChB,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,aAAa;IAC5D,WAAW,EAAE,aAAa,EAAE,YAAY;IACxC,kBAAkB;IAClB,eAAe,EAAE,gBAAgB,EAAE,iBAAiB;IACpD,QAAQ;IACR,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO;IACnD,OAAO;IACP,MAAM,EAAE,YAAY,EAAE,MAAM;CACpB,CAAC;AAIX,gGAAgG;AAChG,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ;IAC3E,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ;CACrD,CAAC"}
|
package/dist/targets.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { AgentError } from "./types.js";
|
|
2
|
+
export const isRegexSpec = (v) => typeof v === "object" && v !== null && "__regex" in v;
|
|
3
|
+
/**
|
|
4
|
+
* `instanceof RegExp` lies across realms, and scripts run inside `node:vm` —
|
|
5
|
+
* their regexes come from a different realm than ours.
|
|
6
|
+
*/
|
|
7
|
+
export const isRegExp = (v) => Object.prototype.toString.call(v) === "[object RegExp]";
|
|
8
|
+
/**
|
|
9
|
+
* A script may pass a RegExp; JSON transport passes {__regex:{source,flags}}.
|
|
10
|
+
* Foreign-realm regexes are rebuilt here so Playwright's own `instanceof`
|
|
11
|
+
* checks recognise them.
|
|
12
|
+
*/
|
|
13
|
+
export function toMatcher(value) {
|
|
14
|
+
if (isRegexSpec(value))
|
|
15
|
+
return new RegExp(value.__regex.source, value.__regex.flags);
|
|
16
|
+
if (isRegExp(value)) {
|
|
17
|
+
// Rebuild only when it comes from another realm; TS cannot see that
|
|
18
|
+
// distinction, so the source/flags are read off a plain alias.
|
|
19
|
+
const foreign = value;
|
|
20
|
+
return value instanceof RegExp ? value : new RegExp(foreign.source, foreign.flags);
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
export function toLocator(scope, target) {
|
|
25
|
+
const base = target.within ? toLocator(scope, target.within) : scope;
|
|
26
|
+
let loc;
|
|
27
|
+
if (target.testId !== undefined)
|
|
28
|
+
loc = base.getByTestId(target.testId);
|
|
29
|
+
else if (target.role !== undefined) {
|
|
30
|
+
loc = base.getByRole(target.role, target.name === undefined ? undefined : { name: toMatcher(target.name) });
|
|
31
|
+
}
|
|
32
|
+
else if (target.label !== undefined)
|
|
33
|
+
loc = base.getByLabel(toMatcher(target.label));
|
|
34
|
+
else if (target.placeholder !== undefined)
|
|
35
|
+
loc = base.getByPlaceholder(toMatcher(target.placeholder));
|
|
36
|
+
else if (target.text !== undefined)
|
|
37
|
+
loc = base.getByText(toMatcher(target.text));
|
|
38
|
+
else if (target.css !== undefined)
|
|
39
|
+
loc = base.locator(target.css);
|
|
40
|
+
else
|
|
41
|
+
throw new AgentError("script", `empty target: ${JSON.stringify(target)}`);
|
|
42
|
+
return target.nth === undefined ? loc : loc.nth(target.nth);
|
|
43
|
+
}
|
|
44
|
+
const show = (v) => isRegexSpec(v) ? `/${v.__regex.source}/${v.__regex.flags}` : isRegExp(v) ? String(v) : JSON.stringify(v);
|
|
45
|
+
/** Human-readable target, used in assertion and timeout messages. */
|
|
46
|
+
export function describeTarget(target) {
|
|
47
|
+
const parts = [];
|
|
48
|
+
if (target.testId !== undefined)
|
|
49
|
+
parts.push(`testId=${target.testId}`);
|
|
50
|
+
if (target.role !== undefined)
|
|
51
|
+
parts.push(`role=${target.role}`);
|
|
52
|
+
if (target.name !== undefined)
|
|
53
|
+
parts.push(`name=${show(target.name)}`);
|
|
54
|
+
if (target.label !== undefined)
|
|
55
|
+
parts.push(`label=${show(target.label)}`);
|
|
56
|
+
if (target.placeholder !== undefined)
|
|
57
|
+
parts.push(`placeholder=${show(target.placeholder)}`);
|
|
58
|
+
if (target.text !== undefined)
|
|
59
|
+
parts.push(`text=${show(target.text)}`);
|
|
60
|
+
if (target.css !== undefined)
|
|
61
|
+
parts.push(`css=${target.css}`);
|
|
62
|
+
if (target.within !== undefined)
|
|
63
|
+
parts.push(`in(${describeTarget(target.within)})`);
|
|
64
|
+
if (target.nth !== undefined)
|
|
65
|
+
parts.push(`[${target.nth}]`);
|
|
66
|
+
return parts.join(" ");
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=targets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"targets.js","sourceRoot":"","sources":["../src/targets.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAmC,MAAM,YAAY,CAAC;AAEzE,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAU,EAAkB,EAAE,CACxD,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,IAAI,SAAS,IAAI,CAAC,CAAC;AAExD;;;GAGG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,CAAU,EAAe,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,iBAAiB,CAAC;AAE7G;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,KAAkC;IAC1D,IAAI,WAAW,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IACrF,IAAI,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QACpB,oEAAoE;QACpE,+DAA+D;QAC/D,MAAM,OAAO,GAAG,KAA0C,CAAC;QAC3D,OAAO,KAAK,YAAY,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IACrF,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAqB,EAAE,MAAkB;IACjE,MAAM,IAAI,GAAmB,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IACrF,IAAI,GAAY,CAAC;IAEjB,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS;QAAE,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;SAClE,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QACnC,GAAG,GAAG,IAAI,CAAC,SAAS,CAClB,MAAM,CAAC,IAAwC,EAC/C,MAAM,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CACzE,CAAC;IACJ,CAAC;SAAM,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS;QAAE,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;SACjF,IAAI,MAAM,CAAC,WAAW,KAAK,SAAS;QAAE,GAAG,GAAG,IAAI,CAAC,gBAAgB,CAAC,SAAS,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC;SACjG,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS;QAAE,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;SAC5E,IAAI,MAAM,CAAC,GAAG,KAAK,SAAS;QAAE,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;;QAC7D,MAAM,IAAI,UAAU,CAAC,QAAQ,EAAE,iBAAiB,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAE/E,OAAO,MAAM,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;AAC9D,CAAC;AAED,MAAM,IAAI,GAAG,CAAC,CAAU,EAAU,EAAE,CAClC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;AAE3G,qEAAqE;AACrE,MAAM,UAAU,cAAc,CAAC,MAAkB;IAC/C,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,UAAU,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;IACvE,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;IACjE,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACvE,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,SAAS,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC1E,IAAI,MAAM,CAAC,WAAW,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,eAAe,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;IAC5F,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACvE,IAAI,MAAM,CAAC,GAAG,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,OAAO,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC;IAC9D,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,MAAM,cAAc,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACpF,IAAI,MAAM,CAAC,GAAG,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC;IAC5D,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACzB,CAAC"}
|
package/dist/traffic.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { AgentError } from "./types.js";
|
|
2
|
+
const METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
3
|
+
export function parseMatcher(input) {
|
|
4
|
+
if (typeof input !== "string") {
|
|
5
|
+
const { method, host, path, url } = input;
|
|
6
|
+
return {
|
|
7
|
+
...(method ? { method: method.toUpperCase() } : {}),
|
|
8
|
+
...(host ? { host } : {}),
|
|
9
|
+
...(path ? { path } : {}),
|
|
10
|
+
...(url ? { urlPart: url } : {}),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
const [head, ...rest] = input.trim().split(/\s+/);
|
|
14
|
+
if (head && METHODS.includes(head.toUpperCase()) && rest.length) {
|
|
15
|
+
return { method: head.toUpperCase(), urlPart: rest.join(" ") };
|
|
16
|
+
}
|
|
17
|
+
return { urlPart: input.trim() };
|
|
18
|
+
}
|
|
19
|
+
export function matchesObserved(o, m) {
|
|
20
|
+
if (m.method && o.method.toUpperCase() !== m.method)
|
|
21
|
+
return false;
|
|
22
|
+
if (m.urlPart && !o.url.includes(m.urlPart))
|
|
23
|
+
return false;
|
|
24
|
+
if (m.host || m.path) {
|
|
25
|
+
let parsed;
|
|
26
|
+
try {
|
|
27
|
+
parsed = new URL(o.url);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
if (m.host && parsed.host !== m.host)
|
|
33
|
+
return false;
|
|
34
|
+
if (m.path && parsed.pathname !== m.path)
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
export const describeMatcher = (m) => [m.method, m.urlPart, m.host, m.path].filter(Boolean).join(" ");
|
|
40
|
+
export class TrafficBuffer {
|
|
41
|
+
seen = [];
|
|
42
|
+
waiters = [];
|
|
43
|
+
observe(o) {
|
|
44
|
+
const waiterIndex = this.waiters.findIndex((w) => matchesObserved(o, w.matcher));
|
|
45
|
+
if (waiterIndex >= 0) {
|
|
46
|
+
const [waiter] = this.waiters.splice(waiterIndex, 1);
|
|
47
|
+
o.consumed = true;
|
|
48
|
+
this.seen.push(o);
|
|
49
|
+
waiter.resolve(o);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
this.seen.push(o);
|
|
53
|
+
}
|
|
54
|
+
/** First unconsumed match, waiting up to `timeoutMs` for one to arrive. */
|
|
55
|
+
async consume(matcher, timeoutMs) {
|
|
56
|
+
const existing = this.seen.find((o) => !o.consumed && matchesObserved(o, matcher));
|
|
57
|
+
if (existing) {
|
|
58
|
+
existing.consumed = true;
|
|
59
|
+
return existing;
|
|
60
|
+
}
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const waiter = { matcher, resolve };
|
|
63
|
+
this.waiters.push(waiter);
|
|
64
|
+
const timer = setTimeout(() => {
|
|
65
|
+
const at = this.waiters.indexOf(waiter);
|
|
66
|
+
if (at < 0)
|
|
67
|
+
return;
|
|
68
|
+
this.waiters.splice(at, 1);
|
|
69
|
+
reject(new AgentError("assertion", `no request matched ${describeMatcher(matcher)} within ${timeoutMs}ms`, {
|
|
70
|
+
expected: describeMatcher(matcher),
|
|
71
|
+
actual: "no matching request",
|
|
72
|
+
}));
|
|
73
|
+
}, timeoutMs);
|
|
74
|
+
timer.unref?.();
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
seenSince(step, matcher) {
|
|
78
|
+
return this.seen.filter((o) => o.step >= step && matchesObserved(o, matcher));
|
|
79
|
+
}
|
|
80
|
+
all() {
|
|
81
|
+
return this.seen;
|
|
82
|
+
}
|
|
83
|
+
/** Marker headers + response observation. Returns a detach function. */
|
|
84
|
+
static async attach(context, runId, getStep, buffer) {
|
|
85
|
+
// The step is fixed when the request goes out, not when the response comes
|
|
86
|
+
// back — otherwise attribution would disagree with the marker header the
|
|
87
|
+
// plugin correlates on.
|
|
88
|
+
const stepOfRequest = new WeakMap();
|
|
89
|
+
await context.route("**", async (route) => {
|
|
90
|
+
const step = getStep();
|
|
91
|
+
stepOfRequest.set(route.request(), step);
|
|
92
|
+
await route
|
|
93
|
+
.continue({
|
|
94
|
+
headers: { ...route.request().headers(), "x-trawl-run": runId, "x-trawl-step": String(step) },
|
|
95
|
+
})
|
|
96
|
+
.catch(() => { });
|
|
97
|
+
});
|
|
98
|
+
const onResponse = (response) => {
|
|
99
|
+
buffer.observe({
|
|
100
|
+
method: response.request().method(),
|
|
101
|
+
url: response.url(),
|
|
102
|
+
status: response.status(),
|
|
103
|
+
step: stepOfRequest.get(response.request()) ?? getStep(),
|
|
104
|
+
ts: Date.now(),
|
|
105
|
+
consumed: false,
|
|
106
|
+
responseBody: () => response.text().catch(() => null),
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
context.on("response", onResponse);
|
|
110
|
+
return async () => {
|
|
111
|
+
context.off("response", onResponse);
|
|
112
|
+
await context.unroute("**").catch(() => { });
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=traffic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"traffic.js","sourceRoot":"","sources":["../src/traffic.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { parse } from "acorn";
|
|
2
|
+
import { ancestor, simple } from "acorn-walk";
|
|
3
|
+
import { AgentError } from "./types.js";
|
|
4
|
+
export function parseScript(code) {
|
|
5
|
+
try {
|
|
6
|
+
return parse(code, {
|
|
7
|
+
ecmaVersion: "latest",
|
|
8
|
+
sourceType: "script",
|
|
9
|
+
allowAwaitOutsideFunction: true,
|
|
10
|
+
allowReturnOutsideFunction: true,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
throw new AgentError("script", `syntax error: ${err.message}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const FUNCTIONS = new Set(["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"]);
|
|
18
|
+
/**
|
|
19
|
+
* Where `async` has to go for a function to become async. Method shorthand
|
|
20
|
+
* (`{ foo() {} }`, `class { foo() {} }`) needs it before the key, not before
|
|
21
|
+
* the value's parameter list.
|
|
22
|
+
*/
|
|
23
|
+
function asyncInsertPoint(fn, parent) {
|
|
24
|
+
const p = parent;
|
|
25
|
+
if (p && (p.type === "Property" || p.type === "MethodDefinition") && p.value === fn && p.key) {
|
|
26
|
+
return p.key.start;
|
|
27
|
+
}
|
|
28
|
+
return fn.start;
|
|
29
|
+
}
|
|
30
|
+
const calleeName = (node) => node.callee.type === "Identifier" ? node.callee.name : null;
|
|
31
|
+
/**
|
|
32
|
+
* Insert `await` before every bare call to a known step name, and mark the
|
|
33
|
+
* enclosing function `async` when it isn't already — otherwise an awaited step
|
|
34
|
+
* inside a callback like `step('name', () => { … })` is a syntax error.
|
|
35
|
+
*/
|
|
36
|
+
export function addAwaits(code, stepNames) {
|
|
37
|
+
const ast = parseScript(code);
|
|
38
|
+
const names = new Set(stepNames);
|
|
39
|
+
const inserts = new Map();
|
|
40
|
+
ancestor(ast, {
|
|
41
|
+
CallExpression(node, _state, ancestors) {
|
|
42
|
+
const name = calleeName(node);
|
|
43
|
+
if (!name || !names.has(name))
|
|
44
|
+
return;
|
|
45
|
+
const parent = ancestors[ancestors.length - 2];
|
|
46
|
+
if (parent?.type === "AwaitExpression")
|
|
47
|
+
return;
|
|
48
|
+
inserts.set(node.start, "await ");
|
|
49
|
+
// The nearest enclosing function must be async for that await to be legal.
|
|
50
|
+
for (let i = ancestors.length - 2; i >= 0; i--) {
|
|
51
|
+
const fn = ancestors[i];
|
|
52
|
+
if (!FUNCTIONS.has(fn.type))
|
|
53
|
+
continue;
|
|
54
|
+
if (!fn.async) {
|
|
55
|
+
inserts.set(asyncInsertPoint(fn, ancestors[i - 1]), "async ");
|
|
56
|
+
}
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
let out = code;
|
|
62
|
+
for (const at of [...inserts.keys()].sort((a, b) => b - a)) {
|
|
63
|
+
out = out.slice(0, at) + inserts.get(at) + out.slice(at);
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
/** True when the step list produced by collect mode can only be approximate. */
|
|
68
|
+
export function hasBranching(code) {
|
|
69
|
+
const ast = parseScript(code);
|
|
70
|
+
let found = false;
|
|
71
|
+
const mark = () => {
|
|
72
|
+
found = true;
|
|
73
|
+
};
|
|
74
|
+
simple(ast, {
|
|
75
|
+
IfStatement: mark,
|
|
76
|
+
ForStatement: mark,
|
|
77
|
+
ForOfStatement: mark,
|
|
78
|
+
ForInStatement: mark,
|
|
79
|
+
WhileStatement: mark,
|
|
80
|
+
DoWhileStatement: mark,
|
|
81
|
+
SwitchStatement: mark,
|
|
82
|
+
ConditionalExpression: mark,
|
|
83
|
+
LogicalExpression: mark,
|
|
84
|
+
});
|
|
85
|
+
return found;
|
|
86
|
+
}
|
|
87
|
+
/** Bare calls that are not steps, not locally defined, and not allowed globals. */
|
|
88
|
+
export function unknownCalls(code, stepNames, allowedGlobals) {
|
|
89
|
+
const ast = parseScript(code);
|
|
90
|
+
const known = new Set([...stepNames, ...allowedGlobals]);
|
|
91
|
+
const declared = new Set();
|
|
92
|
+
const called = new Set();
|
|
93
|
+
const declare = (node) => {
|
|
94
|
+
if (node.type === "FunctionDeclaration" && node.id)
|
|
95
|
+
declared.add(node.id.name);
|
|
96
|
+
if (node.type === "VariableDeclarator" && node.id.type === "Identifier")
|
|
97
|
+
declared.add(node.id.name);
|
|
98
|
+
};
|
|
99
|
+
simple(ast, {
|
|
100
|
+
FunctionDeclaration: declare,
|
|
101
|
+
VariableDeclarator: declare,
|
|
102
|
+
CallExpression(node) {
|
|
103
|
+
const name = calleeName(node);
|
|
104
|
+
if (name)
|
|
105
|
+
called.add(name);
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
return [...called].filter((name) => !known.has(name) && !declared.has(name));
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=transform.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transform.js","sourceRoot":"","sources":["../src/transform.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** An error with a kind the plugin and the report can branch on. */
|
|
2
|
+
export class AgentError extends Error {
|
|
3
|
+
kind;
|
|
4
|
+
detail;
|
|
5
|
+
constructor(kind, message, detail) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.kind = kind;
|
|
8
|
+
this.detail = detail;
|
|
9
|
+
this.name = "AgentError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA,oEAAoE;AACpE,MAAM,OAAO,UAAW,SAAQ,KAAK;IAExB;IAEA;IAHX,YACW,IAAe,EACxB,OAAe,EACN,MAA+C;QAExD,KAAK,CAAC,OAAO,CAAC,CAAC;QAJN,SAAI,GAAJ,IAAI,CAAW;QAEf,WAAM,GAAN,MAAM,CAAyC;QAGxD,IAAI,CAAC,IAAI,GAAG,YAAY,CAAC;IAC3B,CAAC;CACF"}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,4CAA4C;AAC5C,MAAM,CAAC,MAAM,aAAa,GAAG,OAAO,CAAC;AACrC,8EAA8E;AAC9E,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { AgentError } from "./types.js";
|
|
4
|
+
/** Resolve `rel` under `root`, refusing anything that escapes it. */
|
|
5
|
+
export function resolveInWorkspace(root, rel) {
|
|
6
|
+
const normRoot = path.resolve(root);
|
|
7
|
+
const abs = path.resolve(normRoot, rel);
|
|
8
|
+
if (abs !== normRoot && !abs.startsWith(normRoot + path.sep)) {
|
|
9
|
+
throw new AgentError("script", `path escapes workspace: ${rel}`);
|
|
10
|
+
}
|
|
11
|
+
return abs;
|
|
12
|
+
}
|
|
13
|
+
export const scriptsDir = (root) => path.join(path.resolve(root), "scripts");
|
|
14
|
+
export const runsDir = (root) => path.join(path.resolve(root), "runs");
|
|
15
|
+
export const runDir = (root, runId) => path.join(runsDir(root), runId);
|
|
16
|
+
export async function ensureWorkspace(root) {
|
|
17
|
+
await fs.mkdir(scriptsDir(root), { recursive: true });
|
|
18
|
+
await fs.mkdir(runsDir(root), { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
export async function readScript(root, rel) {
|
|
21
|
+
return fs.readFile(resolveInWorkspace(root, rel), "utf8");
|
|
22
|
+
}
|
|
23
|
+
export async function writeScript(root, rel, code) {
|
|
24
|
+
const abs = resolveInWorkspace(root, rel);
|
|
25
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
26
|
+
await fs.writeFile(abs, code, "utf8");
|
|
27
|
+
}
|
|
28
|
+
/** Every `.js` under scripts/, as workspace-relative paths. */
|
|
29
|
+
export async function listScripts(root) {
|
|
30
|
+
const out = [];
|
|
31
|
+
const walk = async (dir) => {
|
|
32
|
+
let entries;
|
|
33
|
+
try {
|
|
34
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
for (const e of entries) {
|
|
40
|
+
const abs = path.join(dir, e.name);
|
|
41
|
+
if (e.isDirectory())
|
|
42
|
+
await walk(abs);
|
|
43
|
+
else if (e.name.endsWith(".js"))
|
|
44
|
+
out.push(path.relative(path.resolve(root), abs));
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
await walk(scriptsDir(root));
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=workspace.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace.js","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "trawl-devices-agent",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local sidecar for the Trawl devices plugin — records and replays browser scenarios with Playwright.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"trawl-devices-agent": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"skills"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "legostin",
|
|
18
|
+
"homepage": "https://github.com/legostin/trawl-devices-agent#readme",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/legostin/trawl-devices-agent.git"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/legostin/trawl-devices-agent/issues"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"trawl",
|
|
28
|
+
"playwright",
|
|
29
|
+
"testing",
|
|
30
|
+
"browser-automation",
|
|
31
|
+
"record-and-replay",
|
|
32
|
+
"mcp"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"start": "node dist/index.js",
|
|
38
|
+
"prepublishOnly": "pnpm build"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"acorn": "^8.17.0",
|
|
42
|
+
"acorn-walk": "^8.3.5",
|
|
43
|
+
"playwright": "^1.62.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^22.10.0",
|
|
47
|
+
"typescript": "~5.8.3",
|
|
48
|
+
"vitest": "^3.2.4"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: writing-device-scripts
|
|
3
|
+
description: Use when writing or editing Trawl device scripts — the declarative JS DSL recorded and replayed by trawl-devices-agent.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Writing device scripts
|
|
7
|
+
|
|
8
|
+
A device script is a plain `.js` file made of flat step calls. It is **not**
|
|
9
|
+
Playwright code: do not write `page.` anything, do not write `await`, do not
|
|
10
|
+
import anything.
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
device('chrome-desktop')
|
|
14
|
+
use({ baseUrl: '{{BASE_URL}}', timeout: 15000 })
|
|
15
|
+
|
|
16
|
+
goto('/login')
|
|
17
|
+
fill({ label: 'Email' }, '{{USER}}')
|
|
18
|
+
fill({ label: 'Пароль' }, secret('PWD'))
|
|
19
|
+
click({ role: 'button', name: 'Войти' })
|
|
20
|
+
expectResponse('POST /api/login', { status: 200, jsonPath: { '$.token': /^ey/ } })
|
|
21
|
+
expectUrl('**/dashboard')
|
|
22
|
+
expectText({ testId: 'greeting' }, /Привет/)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Targets
|
|
26
|
+
|
|
27
|
+
Prefer, in order: `{ testId }`, `{ role, name }`, `{ label }`, `{ placeholder }`,
|
|
28
|
+
`{ text }`, `{ css }`. Narrow with `within` and `nth`:
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
click({ role: 'row', name: 'Заказ 42', within: { testId: 'orders' } })
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Steps
|
|
35
|
+
|
|
36
|
+
- Navigation: `goto`, `back`, `forward`, `reload`
|
|
37
|
+
- Actions: `click`, `dblclick`, `fill`, `type`, `press`, `check`, `uncheck`,
|
|
38
|
+
`select`, `hover`, `upload`, `drag`, `scrollTo`
|
|
39
|
+
- Waits: `waitFor(target, 'visible'|'hidden'|'attached')`, `waitForUrl`,
|
|
40
|
+
`waitForResponse`, `sleep`
|
|
41
|
+
- UI assertions: `expectVisible`, `expectHidden`, `expectText`, `expectValue`,
|
|
42
|
+
`expectUrl`, `expectCount`, `expectAttr`
|
|
43
|
+
- HTTP assertions: `expectRequest`, `expectResponse`, `expectNoRequest`
|
|
44
|
+
- Reads: `getText`, `getValue`, `getAttr`, `getUrl`, `count`
|
|
45
|
+
- Misc: `step(name, fn)`, `screenshot(name)`, `note(text)`
|
|
46
|
+
|
|
47
|
+
## Rules
|
|
48
|
+
|
|
49
|
+
1. **No `await`.** The runner inserts it. Reads return real values:
|
|
50
|
+
`const title = getText({ css: 'h1' })`.
|
|
51
|
+
2. **`{{VAR}}`** comes from the Trawl project env; an unknown name fails the run.
|
|
52
|
+
**`secret('NAME')`** reads the Keychain — never paste a literal password.
|
|
53
|
+
3. **Prefer assertions over `sleep`.** Every step already auto-waits.
|
|
54
|
+
4. **Group with `step('name', () => { … })`** — the report shows the group name.
|
|
55
|
+
5. **HTTP matchers are a method plus a URL substring** (`'POST /api/login'`), or
|
|
56
|
+
an object (`{ method, host, path }`). Each `expectResponse` consumes the
|
|
57
|
+
first matching response not yet consumed, waiting up to the timeout.
|
|
58
|
+
6. **A failing step ends the run.** Steps after it never execute, so they do not
|
|
59
|
+
appear in the report.
|