ptywright 0.2.0 → 0.3.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/README.md +38 -7
- package/dist/agent.mjs +2 -2
- package/dist/bin/ptywright.mjs +1 -1
- package/dist/{cli-DIUx2w6X.mjs → cli-CfvlbRoZ.mjs} +7 -9
- package/dist/cli.mjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/mcp.mjs +1 -1
- package/dist/{runner-DzZlFrt1.mjs → runner-zi0nItvB.mjs} +153 -176
- package/dist/{server-VHuEWWj_.mjs → server-BC3yo-dq.mjs} +1 -1
- package/package.json +1 -1
- package/schemas/ptywright-agent.schema.json +3 -19
package/README.md
CHANGED
|
@@ -107,7 +107,7 @@ bunx ptywright@latest pty validate tests/cassettes/codex.pty.json
|
|
|
107
107
|
bunx ptywright@latest pty inspect tests/cassettes/codex.pty.json
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
External projects do not need
|
|
110
|
+
External projects do not need a ptywright-specific PTY wrapper. Use the structural
|
|
111
111
|
`wrapPtyLike` API for `node-pty`/`bun-pty` style objects:
|
|
112
112
|
|
|
113
113
|
```ts
|
|
@@ -308,9 +308,10 @@ Recording artifacts are best for failure diagnosis or manual review; prefer `sna
|
|
|
308
308
|
|
|
309
309
|
## Browser Agent Regression
|
|
310
310
|
|
|
311
|
-
The
|
|
312
|
-
|
|
313
|
-
replayable run artifact plus terminal/DOM snapshots.
|
|
311
|
+
The browser-first path is integration-agnostic: ptywright launches any command
|
|
312
|
+
that prints a browser URL, drives the terminal DOM with Playwright, and persists
|
|
313
|
+
a replayable run artifact plus terminal/DOM snapshots. The browser page must
|
|
314
|
+
expose the terminal root as `[data-terminal-root]`.
|
|
314
315
|
|
|
315
316
|
```bash
|
|
316
317
|
# First run records snapshots, screenshots, replay metadata, and report.
|
|
@@ -379,9 +380,39 @@ Artifacts are split intentionally:
|
|
|
379
380
|
- `tests/agent-snapshots/<name>/` contains stable terminal/DOM baselines.
|
|
380
381
|
- `--update-snapshots` is the explicit update path for intentional UI changes.
|
|
381
382
|
|
|
382
|
-
`launch.mode=
|
|
383
|
-
ptywright
|
|
384
|
-
`
|
|
383
|
+
`launch.mode=command` is the recommended integration contract. `command` and
|
|
384
|
+
`args` are spawned directly, and ptywright reads the first URL printed to stdout
|
|
385
|
+
or stderr. Use `waitForUrlMs` to tune startup timeouts and `urlRegex` when the
|
|
386
|
+
URL is embedded in structured output. Set `launch.agentFlavor` explicitly when
|
|
387
|
+
the command is a wrapper, so mask presets still match the underlying agent.
|
|
388
|
+
|
|
389
|
+
`launch.mode=url` skips process launch and points ptywright at an already
|
|
390
|
+
running browser terminal.
|
|
391
|
+
|
|
392
|
+
A wrapper integration is just a normal command that prints its browser URL:
|
|
393
|
+
|
|
394
|
+
```json
|
|
395
|
+
{
|
|
396
|
+
"name": "codex_browser_replay",
|
|
397
|
+
"launch": {
|
|
398
|
+
"mode": "command",
|
|
399
|
+
"agentFlavor": "codex",
|
|
400
|
+
"command": "node_modules/.bin/browser-terminal-launcher",
|
|
401
|
+
"args": [
|
|
402
|
+
"--replay",
|
|
403
|
+
"test/recordings/codex-yolo.pty.json",
|
|
404
|
+
"--speed",
|
|
405
|
+
"0",
|
|
406
|
+
"--print-url"
|
|
407
|
+
],
|
|
408
|
+
"waitForUrlMs": 15000
|
|
409
|
+
},
|
|
410
|
+
"steps": [
|
|
411
|
+
{ "type": "waitForStableDom" },
|
|
412
|
+
{ "type": "snapshot", "name": "codex", "targets": ["terminal", "dom"] }
|
|
413
|
+
]
|
|
414
|
+
}
|
|
415
|
+
```
|
|
385
416
|
|
|
386
417
|
Set `launch.agentFlavor` to `codex`, `claude`, `droid`, or `generic` to opt
|
|
387
418
|
into built-in mask presets for timestamps, generated ids, model names, token
|
package/dist/agent.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as runAgentSpecPath, i as runAgentSpec, n as
|
|
2
|
-
export { defaultSpecNameForPath,
|
|
1
|
+
import { a as runAgentSpecPath, i as runAgentSpec, n as printAgentLaunchPlan, r as replayAgentRecordPath, t as defaultSpecNameForPath } from "./runner-zi0nItvB.mjs";
|
|
2
|
+
export { defaultSpecNameForPath, printAgentLaunchPlan, replayAgentRecordPath, runAgentSpec, runAgentSpecPath };
|
package/dist/bin/ptywright.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { c as createDefaultPtyAdapter, l as resolvePtyBackend } from "./runner-zApMYWZx.mjs";
|
|
2
|
-
import { a as readScriptManifestPath, c as resolveScriptManifestPath, d as resolveScriptRunSummaryPath, f as runScriptPath, i as findScriptSummaryManifest, l as validateScriptManifest, n as runAllScripts, o as relocateScriptManifestCommands, s as resolveManifestPrimaryPath$1, t as createPtywrightServer, u as readScriptRunSummaryPath } from "./server-
|
|
3
|
-
import { C as isAgentManifestLike, E as writeAgentManifestPath, S as agentManifestPath, T as validateAgentManifestFiles, _ as
|
|
2
|
+
import { a as readScriptManifestPath, c as resolveScriptManifestPath, d as resolveScriptRunSummaryPath, f as runScriptPath, i as findScriptSummaryManifest, l as validateScriptManifest, n as runAllScripts, o as relocateScriptManifestCommands, s as resolveManifestPrimaryPath$1, t as createPtywrightServer, u as readScriptRunSummaryPath } from "./server-BC3yo-dq.mjs";
|
|
3
|
+
import { C as isAgentManifestLike, E as writeAgentManifestPath, S as agentManifestPath, T as validateAgentManifestFiles, _ as readAgentCassettePath, a as runAgentSpecPath, b as launchAgentBrowser, c as agentRunModeSchema, d as readAgentRunRecordPath, f as writeAgentRunRecordPath, g as isAgentCassetteLike, h as resolveAgentLaunchTarget, l as formatAgentArgv, m as createAgentTemplateSpec, o as loadAgentSpec, p as formatArgv, r as replayAgentRecordPath, s as AGENT_RUN_RECORD_SCHEMA_URL, u as isAgentRunRecordLike, v as normalizeAgentFlowSpec, w as readAgentManifestPath, x as AGENT_MANIFEST_FILE_NAME, y as sanitizeArtifactName } from "./runner-zi0nItvB.mjs";
|
|
4
4
|
import { c as createPtyCassetteReplay, i as formatPtyCassetteInspectLines, l as readPtyCassettePath, o as inspectPtyCassettePath, r as createPtyCassetteRecorder, t as wrapPtyLike, v as validatePtyCassette } from "./pty_like-Cpkh_O9B.mjs";
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
6
|
import { z } from "zod";
|
|
@@ -2135,13 +2135,11 @@ async function recordAgentSpecPath(specPath, options) {
|
|
|
2135
2135
|
async function recordAgentSpec(input, options) {
|
|
2136
2136
|
const spec = normalizeAgentFlowSpec(input);
|
|
2137
2137
|
const rootDir = options.rootDir ? resolve(process.cwd(), options.rootDir) : process.cwd();
|
|
2138
|
-
const launchMode = spec.launch.mode ?? (spec.launch.url ? "url" : "aitty");
|
|
2139
2138
|
const outPath = isAbsolute(options.outPath) ? options.outPath : resolve(process.cwd(), options.outPath);
|
|
2140
2139
|
const durationMs = options.durationMs ?? 3e4;
|
|
2141
2140
|
const steps = [];
|
|
2142
2141
|
let browser = null;
|
|
2143
|
-
const
|
|
2144
|
-
const url = launchMode === "url" ? spec.launch.url : session.url;
|
|
2142
|
+
const launchTarget = await resolveAgentLaunchTarget(spec.launch, { rootDir });
|
|
2145
2143
|
try {
|
|
2146
2144
|
browser = await launchAgentBrowser({ headless: options.headless ?? false });
|
|
2147
2145
|
const viewport = spec.viewports?.[0] ?? {
|
|
@@ -2160,7 +2158,7 @@ async function recordAgentSpec(input, options) {
|
|
|
2160
2158
|
});
|
|
2161
2159
|
const page = await context.newPage();
|
|
2162
2160
|
await installRecorderHooks(page);
|
|
2163
|
-
await page.goto(url, {
|
|
2161
|
+
await page.goto(launchTarget.url, {
|
|
2164
2162
|
waitUntil: "domcontentloaded",
|
|
2165
2163
|
timeout: spec.defaults?.timeoutMs ?? 3e4
|
|
2166
2164
|
});
|
|
@@ -2198,19 +2196,19 @@ async function recordAgentSpec(input, options) {
|
|
|
2198
2196
|
ok: true,
|
|
2199
2197
|
outPath,
|
|
2200
2198
|
stepCount: recorded.steps.length,
|
|
2201
|
-
url
|
|
2199
|
+
url: launchTarget.url
|
|
2202
2200
|
};
|
|
2203
2201
|
} catch (error) {
|
|
2204
2202
|
return {
|
|
2205
2203
|
ok: false,
|
|
2206
2204
|
outPath,
|
|
2207
2205
|
stepCount: steps.length,
|
|
2208
|
-
url,
|
|
2206
|
+
url: launchTarget.url,
|
|
2209
2207
|
error: error instanceof Error ? error.message : String(error)
|
|
2210
2208
|
};
|
|
2211
2209
|
} finally {
|
|
2212
2210
|
await browser?.close();
|
|
2213
|
-
await session?.close();
|
|
2211
|
+
await launchTarget.session?.close();
|
|
2214
2212
|
}
|
|
2215
2213
|
}
|
|
2216
2214
|
async function installRecorderHooks(page) {
|
package/dist/cli.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as main } from "./cli-
|
|
1
|
+
import { t as main } from "./cli-CfvlbRoZ.mjs";
|
|
2
2
|
export { main };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as createPtywrightServer } from "./server-
|
|
1
|
+
import { t as createPtywrightServer } from "./server-BC3yo-dq.mjs";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
3
|
//#region src/index.ts
|
|
4
4
|
const { server, sessions } = createPtywrightServer();
|
package/dist/mcp.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as createPtywrightServer } from "./server-
|
|
1
|
+
import { t as createPtywrightServer } from "./server-BC3yo-dq.mjs";
|
|
2
2
|
export { createPtywrightServer };
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
|
-
import {
|
|
4
|
+
import { mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
|
-
import { spawn } from "node:child_process";
|
|
7
6
|
import { chromium } from "playwright";
|
|
8
7
|
import { createServer } from "node:http";
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
9
|
//#region src/agent/manifest.ts
|
|
10
10
|
const AGENT_MANIFEST_SCHEMA_URL = "https://ptywright.local/schemas/ptywright-agent-manifest.schema.json";
|
|
11
11
|
const AGENT_MANIFEST_FILE_NAME = "ptywright-agent.manifest.json";
|
|
@@ -171,134 +171,6 @@ function formatZodIssues$1(error) {
|
|
|
171
171
|
}).join("; ");
|
|
172
172
|
}
|
|
173
173
|
//#endregion
|
|
174
|
-
//#region src/agent/aitty.ts
|
|
175
|
-
function buildAittyExecCommand(launch, options = {}) {
|
|
176
|
-
if (!launch.command) throw new Error("launch.command is required for aitty mode");
|
|
177
|
-
const rootDir = options.rootDir ?? process.cwd();
|
|
178
|
-
const cwd = launch.cwd ? resolve(rootDir, launch.cwd) : rootDir;
|
|
179
|
-
const aitty = launch.aitty ?? {};
|
|
180
|
-
const cli = resolveAittyCliCommand(aitty.command, rootDir, options.env ?? process.env);
|
|
181
|
-
const args = [
|
|
182
|
-
...cli.args,
|
|
183
|
-
"exec",
|
|
184
|
-
"--launch",
|
|
185
|
-
"print"
|
|
186
|
-
];
|
|
187
|
-
pushOption(args, "--cwd", cwd);
|
|
188
|
-
pushOption(args, "--host", aitty.host);
|
|
189
|
-
pushOption(args, "--port", aitty.port);
|
|
190
|
-
pushOption(args, "--project", aitty.project);
|
|
191
|
-
pushOption(args, "--label", aitty.label);
|
|
192
|
-
pushOption(args, "--title", aitty.title);
|
|
193
|
-
pushOption(args, "--subtitle", aitty.subtitle);
|
|
194
|
-
pushOption(args, "--theme", aitty.theme && aitty.theme !== "auto" ? aitty.theme : void 0);
|
|
195
|
-
pushOption(args, "--font-size", aitty.fontSize);
|
|
196
|
-
pushOption(args, "--experimental-screen-mode", aitty.screenMode);
|
|
197
|
-
args.push("--", launch.command, ...launch.args ?? []);
|
|
198
|
-
return {
|
|
199
|
-
file: cli.file,
|
|
200
|
-
args,
|
|
201
|
-
cwd,
|
|
202
|
-
env: {
|
|
203
|
-
...options.env ?? process.env,
|
|
204
|
-
...launch.env
|
|
205
|
-
}
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
async function launchAittyBrowserSession(launch, options = {}) {
|
|
209
|
-
const command = buildAittyExecCommand(launch, options);
|
|
210
|
-
const timeoutMs = launch.aitty?.waitForUrlMs ?? 15e3;
|
|
211
|
-
const child = spawn(command.file, command.args, {
|
|
212
|
-
cwd: command.cwd,
|
|
213
|
-
env: command.env,
|
|
214
|
-
stdio: [
|
|
215
|
-
"ignore",
|
|
216
|
-
"pipe",
|
|
217
|
-
"pipe"
|
|
218
|
-
]
|
|
219
|
-
});
|
|
220
|
-
const chunks = [];
|
|
221
|
-
const stderrChunks = [];
|
|
222
|
-
return {
|
|
223
|
-
url: await new Promise((resolveUrl, reject) => {
|
|
224
|
-
let settled = false;
|
|
225
|
-
const timer = setTimeout(() => {
|
|
226
|
-
finish(/* @__PURE__ */ new Error(`timed out after ${timeoutMs}ms waiting for aitty session URL\nstderr=${stderrChunks.join("").trim()}`));
|
|
227
|
-
}, timeoutMs);
|
|
228
|
-
const finish = (result) => {
|
|
229
|
-
if (settled) return;
|
|
230
|
-
settled = true;
|
|
231
|
-
clearTimeout(timer);
|
|
232
|
-
child.stdout.off("data", onStdout);
|
|
233
|
-
child.stderr.off("data", onStderr);
|
|
234
|
-
child.off("error", onError);
|
|
235
|
-
child.off("exit", onExit);
|
|
236
|
-
if (result instanceof Error) reject(result);
|
|
237
|
-
else resolveUrl(result);
|
|
238
|
-
};
|
|
239
|
-
const onStdout = (chunk) => {
|
|
240
|
-
const text = chunk.toString("utf8");
|
|
241
|
-
chunks.push(text);
|
|
242
|
-
const found = extractAittyUrlFromOutput(chunks.join(""));
|
|
243
|
-
if (found) finish(found);
|
|
244
|
-
};
|
|
245
|
-
const onStderr = (chunk) => {
|
|
246
|
-
stderrChunks.push(chunk.toString("utf8"));
|
|
247
|
-
};
|
|
248
|
-
const onError = (error) => finish(error);
|
|
249
|
-
const onExit = (code, signal) => {
|
|
250
|
-
finish(/* @__PURE__ */ new Error(`aitty exited before printing a session URL (code=${code ?? "null"} signal=${signal ?? "null"})\nstdout=${chunks.join("").trim()}\nstderr=${stderrChunks.join("").trim()}`));
|
|
251
|
-
};
|
|
252
|
-
child.stdout.on("data", onStdout);
|
|
253
|
-
child.stderr.on("data", onStderr);
|
|
254
|
-
child.once("error", onError);
|
|
255
|
-
child.once("exit", onExit);
|
|
256
|
-
}),
|
|
257
|
-
process: child,
|
|
258
|
-
close: () => closeChild(child)
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
function extractAittyUrlFromOutput(output) {
|
|
262
|
-
return output.match(/https?:\/\/[^\s"'<>]+/)?.[0] ?? null;
|
|
263
|
-
}
|
|
264
|
-
function resolveAittyCliCommand(explicitCommand, rootDir, env) {
|
|
265
|
-
if (explicitCommand) return {
|
|
266
|
-
file: explicitCommand,
|
|
267
|
-
args: []
|
|
268
|
-
};
|
|
269
|
-
if (env.PTYWRIGHT_AITTY_CLI) return {
|
|
270
|
-
file: env.PTYWRIGHT_AITTY_CLI,
|
|
271
|
-
args: []
|
|
272
|
-
};
|
|
273
|
-
const siblingDist = resolve(rootDir, "../aitty/packages/cli/dist/cli.js");
|
|
274
|
-
if (existsSync(siblingDist)) return {
|
|
275
|
-
file: "node",
|
|
276
|
-
args: [siblingDist]
|
|
277
|
-
};
|
|
278
|
-
return {
|
|
279
|
-
file: "aitty",
|
|
280
|
-
args: []
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
function pushOption(args, name, value) {
|
|
284
|
-
if (value === void 0 || value === "") return;
|
|
285
|
-
args.push(name, String(value));
|
|
286
|
-
}
|
|
287
|
-
async function closeChild(child) {
|
|
288
|
-
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
289
|
-
await new Promise((resolveClose) => {
|
|
290
|
-
const timer = setTimeout(() => {
|
|
291
|
-
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
|
292
|
-
resolveClose();
|
|
293
|
-
}, 2e3);
|
|
294
|
-
child.once("exit", () => {
|
|
295
|
-
clearTimeout(timer);
|
|
296
|
-
resolveClose();
|
|
297
|
-
});
|
|
298
|
-
child.kill("SIGTERM");
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
//#endregion
|
|
302
174
|
//#region src/agent/browser.ts
|
|
303
175
|
async function launchAgentBrowser(args) {
|
|
304
176
|
let lastError;
|
|
@@ -391,8 +263,14 @@ const agentViewportSchema = z.object({
|
|
|
391
263
|
isMobile: z.boolean().optional(),
|
|
392
264
|
hasTouch: z.boolean().optional()
|
|
393
265
|
});
|
|
266
|
+
const agentLaunchModeSchema = z.enum(["command", "url"]);
|
|
267
|
+
function inferAgentLaunchMode(value) {
|
|
268
|
+
if (value.mode) return value.mode;
|
|
269
|
+
if (value.url) return "url";
|
|
270
|
+
return "command";
|
|
271
|
+
}
|
|
394
272
|
const agentLaunchSchema = z.object({
|
|
395
|
-
mode:
|
|
273
|
+
mode: agentLaunchModeSchema.optional(),
|
|
396
274
|
agentFlavor: z.enum([
|
|
397
275
|
"codex",
|
|
398
276
|
"claude",
|
|
@@ -404,33 +282,17 @@ const agentLaunchSchema = z.object({
|
|
|
404
282
|
cwd: z.string().optional(),
|
|
405
283
|
env: z.record(z.string()).optional(),
|
|
406
284
|
url: z.string().url().optional(),
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
label: z.string().min(1).optional(),
|
|
412
|
-
title: z.string().min(1).optional(),
|
|
413
|
-
subtitle: z.string().min(1).optional(),
|
|
414
|
-
theme: z.enum([
|
|
415
|
-
"dark",
|
|
416
|
-
"light",
|
|
417
|
-
"auto"
|
|
418
|
-
]).optional(),
|
|
419
|
-
fontSize: z.number().int().min(11).max(24).optional(),
|
|
420
|
-
screenMode: z.enum(["termvision"]).optional(),
|
|
421
|
-
port: z.number().int().min(0).max(65535).optional(),
|
|
422
|
-
host: z.string().min(1).optional(),
|
|
423
|
-
waitForUrlMs: z.number().int().positive().optional()
|
|
424
|
-
}).optional()
|
|
425
|
-
}).superRefine((value, ctx) => {
|
|
426
|
-
const mode = value.mode ?? (value.url ? "url" : "aitty");
|
|
285
|
+
urlRegex: z.string().min(1).optional(),
|
|
286
|
+
waitForUrlMs: z.number().int().positive().optional()
|
|
287
|
+
}).strict().superRefine((value, ctx) => {
|
|
288
|
+
const mode = inferAgentLaunchMode(value);
|
|
427
289
|
if (mode === "url" && !value.url) ctx.addIssue({
|
|
428
290
|
code: z.ZodIssueCode.custom,
|
|
429
291
|
message: "launch.url is required when launch.mode is 'url'"
|
|
430
292
|
});
|
|
431
|
-
if (mode === "
|
|
293
|
+
if (mode === "command" && !value.command) ctx.addIssue({
|
|
432
294
|
code: z.ZodIssueCode.custom,
|
|
433
|
-
message: "launch.command is required when launch.mode is '
|
|
295
|
+
message: "launch.command is required when launch.mode is 'command'"
|
|
434
296
|
});
|
|
435
297
|
});
|
|
436
298
|
const waitForTextStepSchema = z.object({
|
|
@@ -528,6 +390,9 @@ function normalizeAgentFlowSpec(input) {
|
|
|
528
390
|
viewports: spec.viewports?.length ? spec.viewports : [...DEFAULT_AGENT_VIEWPORTS]
|
|
529
391
|
};
|
|
530
392
|
}
|
|
393
|
+
function resolveAgentLaunchMode(launch) {
|
|
394
|
+
return inferAgentLaunchMode(launch);
|
|
395
|
+
}
|
|
531
396
|
//#endregion
|
|
532
397
|
//#region src/agent/cassette.ts
|
|
533
398
|
const AGENT_CASSETTE_SCHEMA_URL = "https://ptywright.local/schemas/ptywright-agent-cassette.schema.json";
|
|
@@ -759,6 +624,127 @@ function escapeHtml$1(input) {
|
|
|
759
624
|
return input.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
760
625
|
}
|
|
761
626
|
//#endregion
|
|
627
|
+
//#region src/agent/command_launch.ts
|
|
628
|
+
const DEFAULT_URL_REGEX = /https?:\/\/[^\s"'<>]+/;
|
|
629
|
+
function buildCommandLaunchCommand(launch, options = {}) {
|
|
630
|
+
if (!launch.command) throw new Error("launch.command is required when launch.mode is 'command'");
|
|
631
|
+
const rootDir = options.rootDir ?? process.cwd();
|
|
632
|
+
const cwd = launch.cwd ? resolve(rootDir, launch.cwd) : rootDir;
|
|
633
|
+
return {
|
|
634
|
+
file: launch.command,
|
|
635
|
+
args: launch.args ?? [],
|
|
636
|
+
cwd,
|
|
637
|
+
env: {
|
|
638
|
+
...options.env ?? process.env,
|
|
639
|
+
...launch.env
|
|
640
|
+
},
|
|
641
|
+
label: launch.command,
|
|
642
|
+
urlRegex: launch.urlRegex,
|
|
643
|
+
waitForUrlMs: launch.waitForUrlMs
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
async function launchBrowserSessionFromCommand(command) {
|
|
647
|
+
const timeoutMs = command.waitForUrlMs ?? 15e3;
|
|
648
|
+
const child = spawn(command.file, command.args, {
|
|
649
|
+
cwd: command.cwd,
|
|
650
|
+
env: command.env,
|
|
651
|
+
stdio: [
|
|
652
|
+
"ignore",
|
|
653
|
+
"pipe",
|
|
654
|
+
"pipe"
|
|
655
|
+
]
|
|
656
|
+
});
|
|
657
|
+
const stdoutChunks = [];
|
|
658
|
+
const stderrChunks = [];
|
|
659
|
+
return {
|
|
660
|
+
url: await new Promise((resolveUrl, reject) => {
|
|
661
|
+
let settled = false;
|
|
662
|
+
const timer = setTimeout(() => {
|
|
663
|
+
finish(/* @__PURE__ */ new Error(`timed out after ${timeoutMs}ms waiting for ${command.label ?? command.file} session URL\nstdout=${stdoutChunks.join("").trim()}\nstderr=${stderrChunks.join("").trim()}`));
|
|
664
|
+
}, timeoutMs);
|
|
665
|
+
const finish = (result) => {
|
|
666
|
+
if (settled) return;
|
|
667
|
+
settled = true;
|
|
668
|
+
clearTimeout(timer);
|
|
669
|
+
child.stdout.off("data", onStdout);
|
|
670
|
+
child.stderr.off("data", onStderr);
|
|
671
|
+
child.off("error", onError);
|
|
672
|
+
child.off("exit", onExit);
|
|
673
|
+
if (result instanceof Error) reject(result);
|
|
674
|
+
else resolveUrl(result);
|
|
675
|
+
};
|
|
676
|
+
const readUrl = () => {
|
|
677
|
+
const found = extractUrlFromOutput(`${stdoutChunks.join("")}\n${stderrChunks.join("")}`, command.urlRegex);
|
|
678
|
+
if (found) finish(found);
|
|
679
|
+
};
|
|
680
|
+
const onStdout = (chunk) => {
|
|
681
|
+
stdoutChunks.push(chunk.toString("utf8"));
|
|
682
|
+
readUrl();
|
|
683
|
+
};
|
|
684
|
+
const onStderr = (chunk) => {
|
|
685
|
+
stderrChunks.push(chunk.toString("utf8"));
|
|
686
|
+
readUrl();
|
|
687
|
+
};
|
|
688
|
+
const onError = (error) => finish(error);
|
|
689
|
+
const onExit = (code, signal) => {
|
|
690
|
+
finish(/* @__PURE__ */ new Error(`${command.label ?? command.file} exited before printing a session URL (code=${code ?? "null"} signal=${signal ?? "null"})\nstdout=${stdoutChunks.join("").trim()}\nstderr=${stderrChunks.join("").trim()}`));
|
|
691
|
+
};
|
|
692
|
+
child.stdout.on("data", onStdout);
|
|
693
|
+
child.stderr.on("data", onStderr);
|
|
694
|
+
child.once("error", onError);
|
|
695
|
+
child.once("exit", onExit);
|
|
696
|
+
}),
|
|
697
|
+
process: child,
|
|
698
|
+
close: () => closeChild(child)
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
function extractUrlFromOutput(output, regexSource) {
|
|
702
|
+
if (!regexSource) return output.match(DEFAULT_URL_REGEX)?.[0] ?? null;
|
|
703
|
+
const match = output.match(new RegExp(regexSource, "m"));
|
|
704
|
+
return match?.[1] ?? match?.[0] ?? null;
|
|
705
|
+
}
|
|
706
|
+
function formatBrowserLaunchCommand(command) {
|
|
707
|
+
return [command.file, ...command.args].join(" ");
|
|
708
|
+
}
|
|
709
|
+
async function closeChild(child) {
|
|
710
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
711
|
+
await new Promise((resolveClose) => {
|
|
712
|
+
const timer = setTimeout(() => {
|
|
713
|
+
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
|
714
|
+
resolveClose();
|
|
715
|
+
}, 2e3);
|
|
716
|
+
child.once("exit", () => {
|
|
717
|
+
clearTimeout(timer);
|
|
718
|
+
resolveClose();
|
|
719
|
+
});
|
|
720
|
+
child.kill("SIGTERM");
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
//#endregion
|
|
724
|
+
//#region src/agent/launch.ts
|
|
725
|
+
function buildAgentLaunchCommand(launch, options = {}) {
|
|
726
|
+
if (resolveAgentLaunchMode(launch) === "url") return null;
|
|
727
|
+
return buildCommandLaunchCommand(launch, options);
|
|
728
|
+
}
|
|
729
|
+
async function resolveAgentLaunchTarget(launch, options = {}) {
|
|
730
|
+
const mode = resolveAgentLaunchMode(launch);
|
|
731
|
+
if (mode === "url") return {
|
|
732
|
+
mode,
|
|
733
|
+
url: launch.url,
|
|
734
|
+
session: null
|
|
735
|
+
};
|
|
736
|
+
const session = await launchBrowserSessionFromCommand(buildCommandLaunchCommand(launch, options));
|
|
737
|
+
return {
|
|
738
|
+
mode,
|
|
739
|
+
url: session.url,
|
|
740
|
+
session
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
function formatAgentLaunchCommand(launch) {
|
|
744
|
+
const command = buildAgentLaunchCommand(launch);
|
|
745
|
+
return command ? formatBrowserLaunchCommand(command) : "launch.mode=url";
|
|
746
|
+
}
|
|
747
|
+
//#endregion
|
|
762
748
|
//#region src/agent/presets.ts
|
|
763
749
|
const COMMON_AGENT_MASKS = [
|
|
764
750
|
{
|
|
@@ -849,19 +835,15 @@ function createAgentTemplateSpec(flavor) {
|
|
|
849
835
|
artifactsDir: `.tmp/agent/${name}`,
|
|
850
836
|
snapshotDir: `tests/agent-snapshots/${name}`,
|
|
851
837
|
launch: {
|
|
852
|
-
mode: "
|
|
838
|
+
mode: "command",
|
|
853
839
|
agentFlavor: flavor,
|
|
854
|
-
command,
|
|
855
|
-
args: [
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
theme: "light",
|
|
862
|
-
fontSize: 14,
|
|
863
|
-
waitForUrlMs: 15e3
|
|
864
|
-
}
|
|
840
|
+
command: "your-browser-terminal-launcher",
|
|
841
|
+
args: [
|
|
842
|
+
"--agent",
|
|
843
|
+
command,
|
|
844
|
+
"--print-url"
|
|
845
|
+
],
|
|
846
|
+
waitForUrlMs: 15e3
|
|
865
847
|
},
|
|
866
848
|
viewports: DEFAULT_VIEWPORTS.map((viewport) => ({ ...viewport })),
|
|
867
849
|
defaults: {
|
|
@@ -1412,9 +1394,7 @@ async function runAgentSpec(input, options = {}) {
|
|
|
1412
1394
|
}
|
|
1413
1395
|
async function runViewport(args) {
|
|
1414
1396
|
const { browser, spec, viewport, rootDir, artifactsDir, snapshotDir, updateSnapshots, recordCassette, cassette, result } = args;
|
|
1415
|
-
const
|
|
1416
|
-
const session = launchMode === "aitty" ? await launchAittyBrowserSession(spec.launch, { rootDir }) : null;
|
|
1417
|
-
const url = launchMode === "url" ? spec.launch.url : session.url;
|
|
1397
|
+
const launchTarget = await resolveAgentLaunchTarget(spec.launch, { rootDir });
|
|
1418
1398
|
const context = await browser.newContext({
|
|
1419
1399
|
viewport: {
|
|
1420
1400
|
width: viewport.width,
|
|
@@ -1440,7 +1420,7 @@ async function runViewport(args) {
|
|
|
1440
1420
|
nextReplayPhase: 0
|
|
1441
1421
|
};
|
|
1442
1422
|
try {
|
|
1443
|
-
await page.goto(url, {
|
|
1423
|
+
await page.goto(launchTarget.url, {
|
|
1444
1424
|
waitUntil: "domcontentloaded",
|
|
1445
1425
|
timeout: spec.defaults?.timeoutMs ?? 3e4
|
|
1446
1426
|
});
|
|
@@ -1482,7 +1462,7 @@ async function runViewport(args) {
|
|
|
1482
1462
|
}
|
|
1483
1463
|
} finally {
|
|
1484
1464
|
await withTimeout(context.close().catch(() => void 0), 5e3);
|
|
1485
|
-
await session?.close();
|
|
1465
|
+
await launchTarget.session?.close();
|
|
1486
1466
|
}
|
|
1487
1467
|
}
|
|
1488
1468
|
async function replayAgentCassette(cassette, cassettePath, options) {
|
|
@@ -1884,14 +1864,11 @@ function formatStepLabel(step) {
|
|
|
1884
1864
|
function envTruthy(value) {
|
|
1885
1865
|
return value === "1" || value === "true" || value === "yes";
|
|
1886
1866
|
}
|
|
1887
|
-
function
|
|
1888
|
-
|
|
1889
|
-
if ((spec.launch.mode ?? "aitty") !== "aitty") return "launch.mode=url";
|
|
1890
|
-
const command = buildAittyExecCommand(spec.launch);
|
|
1891
|
-
return [command.file, ...command.args].join(" ");
|
|
1867
|
+
function printAgentLaunchPlan(input) {
|
|
1868
|
+
return formatAgentLaunchCommand(normalizeAgentFlowSpec(input).launch);
|
|
1892
1869
|
}
|
|
1893
1870
|
function defaultSpecNameForPath(path) {
|
|
1894
1871
|
return sanitizeArtifactName(basename(path, extname(path)));
|
|
1895
1872
|
}
|
|
1896
1873
|
//#endregion
|
|
1897
|
-
export { isAgentManifestLike as C, writeAgentManifestPath as E, agentManifestPath as S, validateAgentManifestFiles as T,
|
|
1874
|
+
export { isAgentManifestLike as C, writeAgentManifestPath as E, agentManifestPath as S, validateAgentManifestFiles as T, readAgentCassettePath as _, runAgentSpecPath as a, launchAgentBrowser as b, agentRunModeSchema as c, readAgentRunRecordPath as d, writeAgentRunRecordPath as f, isAgentCassetteLike as g, resolveAgentLaunchTarget as h, runAgentSpec as i, formatAgentArgv as l, createAgentTemplateSpec as m, printAgentLaunchPlan as n, loadAgentSpec as o, formatArgv as p, replayAgentRecordPath as r, AGENT_RUN_RECORD_SCHEMA_URL as s, defaultSpecNameForPath as t, isAgentRunRecordLike as u, normalizeAgentFlowSpec as v, readAgentManifestPath as w, AGENT_MANIFEST_FILE_NAME as x, sanitizeArtifactName as y };
|
package/package.json
CHANGED
|
@@ -14,31 +14,15 @@
|
|
|
14
14
|
"type": "object",
|
|
15
15
|
"additionalProperties": false,
|
|
16
16
|
"properties": {
|
|
17
|
-
"mode": { "type": "string", "enum": ["
|
|
17
|
+
"mode": { "type": "string", "enum": ["command", "url"] },
|
|
18
18
|
"agentFlavor": { "type": "string", "enum": ["codex", "claude", "droid", "generic"] },
|
|
19
19
|
"command": { "type": "string", "minLength": 1 },
|
|
20
20
|
"args": { "type": "array", "items": { "type": "string" } },
|
|
21
21
|
"cwd": { "type": "string" },
|
|
22
22
|
"env": { "type": "object", "additionalProperties": { "type": "string" } },
|
|
23
23
|
"url": { "type": "string" },
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
"additionalProperties": false,
|
|
27
|
-
"properties": {
|
|
28
|
-
"command": { "type": "string", "minLength": 1 },
|
|
29
|
-
"args": { "type": "array", "items": { "type": "string" } },
|
|
30
|
-
"project": { "type": "string", "minLength": 1 },
|
|
31
|
-
"label": { "type": "string", "minLength": 1 },
|
|
32
|
-
"title": { "type": "string", "minLength": 1 },
|
|
33
|
-
"subtitle": { "type": "string", "minLength": 1 },
|
|
34
|
-
"theme": { "type": "string", "enum": ["dark", "light", "auto"] },
|
|
35
|
-
"fontSize": { "type": "integer", "minimum": 11, "maximum": 24 },
|
|
36
|
-
"screenMode": { "type": "string", "enum": ["termvision"] },
|
|
37
|
-
"port": { "type": "integer", "minimum": 0, "maximum": 65535 },
|
|
38
|
-
"host": { "type": "string", "minLength": 1 },
|
|
39
|
-
"waitForUrlMs": { "type": "integer", "minimum": 1 }
|
|
40
|
-
}
|
|
41
|
-
}
|
|
24
|
+
"urlRegex": { "type": "string", "minLength": 1 },
|
|
25
|
+
"waitForUrlMs": { "type": "integer", "minimum": 1 }
|
|
42
26
|
}
|
|
43
27
|
},
|
|
44
28
|
"viewports": {
|