ptywright 0.1.0 → 0.2.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.
Files changed (68) hide show
  1. package/README.md +459 -116
  2. package/dist/agent.mjs +2 -0
  3. package/dist/bin/ptywright.mjs +6 -0
  4. package/dist/cli-DIUx2w6X.mjs +3587 -0
  5. package/dist/cli.mjs +2 -0
  6. package/{src/index.ts → dist/index.mjs} +7 -9
  7. package/dist/mcp.mjs +2 -0
  8. package/dist/pty-cassette.mjs +24 -0
  9. package/dist/pty_like-Cpkh_O9B.mjs +404 -0
  10. package/dist/runner-DzZlFrt1.mjs +1897 -0
  11. package/dist/runner-zApMYWZx.mjs +3257 -0
  12. package/dist/script.mjs +2 -0
  13. package/dist/server-VHuEWWj_.mjs +3068 -0
  14. package/dist/session.mjs +2 -0
  15. package/dist/terminal_session-DopC7Xg6.mjs +893 -0
  16. package/package.json +28 -21
  17. package/schemas/ptywright-agent-cassette.schema.json +57 -0
  18. package/schemas/ptywright-agent-check.schema.json +122 -0
  19. package/schemas/ptywright-agent-manifest.schema.json +107 -0
  20. package/schemas/ptywright-agent-promote.schema.json +146 -0
  21. package/schemas/ptywright-agent-replay-summary.schema.json +140 -0
  22. package/schemas/ptywright-agent-run.schema.json +126 -0
  23. package/schemas/ptywright-agent.schema.json +182 -0
  24. package/schemas/ptywright-pty-cassette.schema.json +86 -0
  25. package/schemas/ptywright-script-manifest.schema.json +75 -0
  26. package/schemas/ptywright-script-run-summary.schema.json +114 -0
  27. package/schemas/ptywright-script.schema.json +55 -3
  28. package/skills/ptywright-testing/SKILL.md +53 -33
  29. package/bin/ptywright +0 -4
  30. package/src/cli.ts +0 -414
  31. package/src/generator/doc_parser.ts +0 -341
  32. package/src/generator/generate.ts +0 -161
  33. package/src/generator/index.ts +0 -10
  34. package/src/generator/script_generator.ts +0 -209
  35. package/src/generator/step_extractor.ts +0 -397
  36. package/src/mcp/http_server.ts +0 -174
  37. package/src/mcp/script_recording.ts +0 -238
  38. package/src/mcp/server.ts +0 -1348
  39. package/src/pty/bun_pty_adapter.ts +0 -34
  40. package/src/pty/bun_terminal_adapter.ts +0 -149
  41. package/src/pty/pty_adapter.ts +0 -31
  42. package/src/script/dsl.ts +0 -188
  43. package/src/script/module.ts +0 -43
  44. package/src/script/path.ts +0 -151
  45. package/src/script/run.ts +0 -108
  46. package/src/script/run_all.ts +0 -229
  47. package/src/script/runner.ts +0 -983
  48. package/src/script/schema.ts +0 -237
  49. package/src/script/steps/assert_snapshot_equals.ts +0 -21
  50. package/src/script/steps/index.ts +0 -2
  51. package/src/script/suite_report.ts +0 -626
  52. package/src/session/session_manager.ts +0 -145
  53. package/src/session/terminal_session.ts +0 -473
  54. package/src/terminal/ansi.ts +0 -142
  55. package/src/terminal/keys.ts +0 -180
  56. package/src/terminal/mask.ts +0 -70
  57. package/src/terminal/mouse.ts +0 -75
  58. package/src/terminal/snapshot.ts +0 -196
  59. package/src/terminal/style.ts +0 -121
  60. package/src/terminal/view.ts +0 -49
  61. package/src/trace/asciicast.ts +0 -20
  62. package/src/trace/asciinema_player_assets.ts +0 -44
  63. package/src/trace/cast_to_txt.ts +0 -116
  64. package/src/trace/recorder.ts +0 -110
  65. package/src/trace/report.ts +0 -2092
  66. package/src/types.ts +0 -86
  67. package/src/util/hash.ts +0 -8
  68. package/src/util/sleep.ts +0 -5
@@ -1,121 +0,0 @@
1
- import type { Terminal } from "@xterm/headless";
2
-
3
- export type Color =
4
- | { mode: "default" }
5
- | { mode: "palette"; value: number }
6
- | { mode: "rgb"; value: number };
7
-
8
- export type CellStyle = {
9
- fg: Color;
10
- bg: Color;
11
- bold: boolean;
12
- dim: boolean;
13
- italic: boolean;
14
- underline: boolean;
15
- inverse: boolean;
16
- strikethrough: boolean;
17
- };
18
-
19
- export const DEFAULT_STYLE: CellStyle = {
20
- fg: { mode: "default" },
21
- bg: { mode: "default" },
22
- bold: false,
23
- dim: false,
24
- italic: false,
25
- underline: false,
26
- inverse: false,
27
- strikethrough: false,
28
- };
29
-
30
- export function extractStyle(
31
- cell: ReturnType<Terminal["buffer"]["active"]["getNullCell"]>,
32
- ): CellStyle {
33
- const fg = extractColor(
34
- cell.isFgDefault(),
35
- cell.isFgPalette(),
36
- cell.isFgRGB(),
37
- cell.getFgColor(),
38
- );
39
- const bg = extractColor(
40
- cell.isBgDefault(),
41
- cell.isBgPalette(),
42
- cell.isBgRGB(),
43
- cell.getBgColor(),
44
- );
45
-
46
- const style: CellStyle = {
47
- fg,
48
- bg,
49
- bold: cell.isBold() !== 0,
50
- dim: cell.isDim() !== 0,
51
- italic: cell.isItalic() !== 0,
52
- underline: cell.isUnderline() !== 0,
53
- inverse: cell.isInverse() !== 0,
54
- strikethrough: cell.isStrikethrough() !== 0,
55
- };
56
-
57
- return isDefaultStyle(style) ? DEFAULT_STYLE : style;
58
- }
59
-
60
- export function isDefaultStyle(style: CellStyle): boolean {
61
- return (
62
- style.fg.mode === "default" &&
63
- style.bg.mode === "default" &&
64
- !style.bold &&
65
- !style.dim &&
66
- !style.italic &&
67
- !style.underline &&
68
- !style.inverse &&
69
- !style.strikethrough
70
- );
71
- }
72
-
73
- export function styleKey(style: CellStyle): string {
74
- const fg = style.fg.mode === "default" ? "d" : `${style.fg.mode}:${style.fg.value}`;
75
- const bg = style.bg.mode === "default" ? "d" : `${style.bg.mode}:${style.bg.value}`;
76
- return [
77
- fg,
78
- bg,
79
- style.bold ? "b" : "",
80
- style.dim ? "d" : "",
81
- style.italic ? "i" : "",
82
- style.underline ? "u" : "",
83
- style.inverse ? "r" : "",
84
- style.strikethrough ? "s" : "",
85
- ].join("|");
86
- }
87
-
88
- export function findMeaningfulEndCol(
89
- line: ReturnType<Terminal["buffer"]["active"]["getLine"]>,
90
- cols: number,
91
- nullCell: ReturnType<Terminal["buffer"]["active"]["getNullCell"]>,
92
- ): number {
93
- if (!line) return 0;
94
-
95
- for (let x = cols - 1; x >= 0; x -= 1) {
96
- const cell = line.getCell(x, nullCell);
97
- if (!cell) continue;
98
- if (cell.getWidth() === 0) continue;
99
-
100
- const chars = cell.getChars();
101
- const style = extractStyle(cell);
102
- const meaningful = (chars !== "" && chars !== " ") || !isDefaultStyle(style);
103
- if (meaningful) {
104
- return x + 1;
105
- }
106
- }
107
-
108
- return 0;
109
- }
110
-
111
- function extractColor(
112
- isDefault: boolean,
113
- isPalette: boolean,
114
- isRgb: boolean,
115
- value: number,
116
- ): Color {
117
- if (isDefault) return { mode: "default" };
118
- if (isRgb) return { mode: "rgb", value };
119
- if (isPalette) return { mode: "palette", value };
120
- return { mode: "default" };
121
- }
@@ -1,49 +0,0 @@
1
- import type { SnapshotScope } from "./snapshot";
2
-
3
- export type TerminalMeta = {
4
- cols: number;
5
- rows: number;
6
- bufferType: "normal" | "alternate";
7
- viewportY: number;
8
- baseY: number;
9
- length: number;
10
- cursorX: number;
11
- cursorY: number;
12
- };
13
-
14
- export type SnapshotViewOptions = {
15
- sessionId: string;
16
- scope: SnapshotScope;
17
- hash: string;
18
- lines: string[];
19
- meta: TerminalMeta;
20
- lineNumbers?: boolean;
21
- };
22
-
23
- export function formatSnapshotView(options: SnapshotViewOptions): string {
24
- const lineNumbers = options.lineNumbers ?? true;
25
-
26
- const cursorAbsY = options.meta.baseY + options.meta.cursorY;
27
- const cursorViewportRow = cursorAbsY - options.meta.viewportY;
28
- const cursorViewportCol = options.meta.cursorX;
29
-
30
- const header = [
31
- `session=${options.sessionId}`,
32
- `scope=${options.scope}`,
33
- `size=${options.meta.cols}x${options.meta.rows}`,
34
- `buffer=${options.meta.bufferType}`,
35
- `cursor=${cursorViewportCol + 1},${cursorViewportRow + 1}`,
36
- `hash=${options.hash}`,
37
- ].join(" ");
38
-
39
- const digits = Math.max(2, String(options.lines.length).length);
40
- const out: string[] = [header];
41
-
42
- for (let i = 0; i < options.lines.length; i += 1) {
43
- const n = i + 1;
44
- const prefix = lineNumbers ? `${String(n).padStart(digits, "0")}│ ` : "";
45
- out.push(`${prefix}${options.lines[i] ?? ""}`);
46
- }
47
-
48
- return out.join("\n");
49
- }
@@ -1,20 +0,0 @@
1
- export type AsciicastHeader = {
2
- version: 2;
3
- width: number;
4
- height: number;
5
- timestamp?: number;
6
- env?: Record<string, string>;
7
- title?: string;
8
- command?: string;
9
- term?: string;
10
- };
11
-
12
- export type AsciicastEvent = [timeSeconds: number, type: "o" | "i" | "m" | "r", data: string];
13
-
14
- export function encodeAsciicast(header: AsciicastHeader, events: AsciicastEvent[]): string {
15
- const lines: string[] = [JSON.stringify(header)];
16
- for (const event of events) {
17
- lines.push(JSON.stringify(event));
18
- }
19
- return `${lines.join("\n")}\n`;
20
- }
@@ -1,44 +0,0 @@
1
- import { copyFileSync, existsSync, mkdirSync } from "node:fs";
2
- import { dirname, join } from "node:path";
3
- import { createRequire } from "node:module";
4
-
5
- export type EnsureAsciinemaPlayerAssetsResult = {
6
- ok: boolean;
7
- copied: boolean;
8
- cssPath: string;
9
- jsPath: string;
10
- error?: string;
11
- };
12
-
13
- export function ensureAsciinemaPlayerAssets(reportPath: string): EnsureAsciinemaPlayerAssetsResult {
14
- const dir = dirname(reportPath);
15
- const cssPath = join(dir, "asciinema-player.css");
16
- const jsPath = join(dir, "asciinema-player.min.js");
17
-
18
- const cssExists = existsSync(cssPath);
19
- const jsExists = existsSync(jsPath);
20
- if (cssExists && jsExists) {
21
- return { ok: true, copied: false, cssPath, jsPath };
22
- }
23
-
24
- try {
25
- mkdirSync(dir, { recursive: true });
26
-
27
- const require = createRequire(import.meta.url);
28
- const resolvedCss = require.resolve("asciinema-player/dist/bundle/asciinema-player.css");
29
- const resolvedJs = require.resolve("asciinema-player/dist/bundle/asciinema-player.min.js");
30
-
31
- if (!cssExists) copyFileSync(resolvedCss, cssPath);
32
- if (!jsExists) copyFileSync(resolvedJs, jsPath);
33
-
34
- return { ok: true, copied: true, cssPath, jsPath };
35
- } catch (error) {
36
- return {
37
- ok: false,
38
- copied: false,
39
- cssPath,
40
- jsPath,
41
- error: (error as Error).message,
42
- };
43
- }
44
- }
@@ -1,116 +0,0 @@
1
- function parseArgs(argv: string[]): { inPath: string; outPath?: string; stripAnsi: boolean } {
2
- const out: Partial<{ inPath: string; outPath?: string; stripAnsi: boolean }> = {
3
- stripAnsi: true,
4
- };
5
-
6
- for (let i = 0; i < argv.length; i += 1) {
7
- const arg = argv[i];
8
- const next = argv[i + 1];
9
-
10
- if (!out.inPath && arg && !arg.startsWith("-")) {
11
- out.inPath = arg;
12
- continue;
13
- }
14
-
15
- if (arg === "--in" && next) {
16
- out.inPath = next;
17
- i += 1;
18
- continue;
19
- }
20
-
21
- if (arg === "--out" && next) {
22
- out.outPath = next;
23
- i += 1;
24
- continue;
25
- }
26
-
27
- if (arg === "--strip-ansi") {
28
- out.stripAnsi = true;
29
- continue;
30
- }
31
-
32
- if (arg === "--keep-ansi") {
33
- out.stripAnsi = false;
34
- continue;
35
- }
36
-
37
- throw new Error(`unknown arg: ${arg ?? ""}`);
38
- }
39
-
40
- if (!out.inPath) throw new Error("missing <castPath> (or --in <path>)");
41
- return out as { inPath: string; outPath?: string; stripAnsi: boolean };
42
- }
43
-
44
- type AsciicastHeader = {
45
- version?: number;
46
- };
47
-
48
- type AsciicastEvent = [timeSeconds: number, type: "o" | "i" | "m" | "r", data: string];
49
-
50
- function stripAnsi(text: string): string {
51
- // CSI: ESC [ ... @-~ (includes SGR, cursor, erase, etc)
52
- // eslint-disable-next-line no-control-regex
53
- const csi = /\u001b\[[0-?]*[ -/]*[@-~]/g;
54
- // OSC: ESC ] ... BEL or ST (ESC \)
55
- // eslint-disable-next-line no-control-regex
56
- const osc = /\u001b\][\s\S]*?(?:\u0007|\u001b\\)/g;
57
- // Two-byte escapes: SS3 (ESC O) + final
58
- // eslint-disable-next-line no-control-regex
59
- const ss3 = /\u001bO[@-~]/g;
60
-
61
- return text.replace(osc, "").replace(csi, "").replace(ss3, "");
62
- }
63
-
64
- async function run(args: { inPath: string; outPath?: string; stripAnsi: boolean }): Promise<void> {
65
- const input = await Bun.file(args.inPath).text();
66
- const lines = input.split("\n").filter((l) => l.length > 0);
67
- if (lines.length === 0) throw new Error("empty cast");
68
-
69
- let header: AsciicastHeader | null = null;
70
- try {
71
- header = JSON.parse(lines[0] ?? "") as AsciicastHeader;
72
- } catch {
73
- throw new Error("invalid cast header JSON");
74
- }
75
-
76
- if (header?.version !== 2) {
77
- throw new Error(`unsupported asciicast version: ${header?.version ?? "unknown"}`);
78
- }
79
-
80
- let out = "";
81
- for (const line of lines.slice(1)) {
82
- let parsed: unknown;
83
- try {
84
- parsed = JSON.parse(line);
85
- } catch {
86
- continue;
87
- }
88
-
89
- if (!Array.isArray(parsed) || parsed.length < 3) continue;
90
- const event = parsed as Partial<AsciicastEvent>;
91
- if (event[1] !== "o") continue;
92
- if (typeof event[2] !== "string") continue;
93
-
94
- out += event[2];
95
- }
96
-
97
- const rendered = args.stripAnsi ? stripAnsi(out) : out;
98
-
99
- if (args.outPath) {
100
- await Bun.write(args.outPath, rendered);
101
- } else {
102
- process.stdout.write(rendered);
103
- if (!rendered.endsWith("\n")) process.stdout.write("\n");
104
- }
105
- }
106
-
107
- if (import.meta.main) {
108
- try {
109
- const args = parseArgs(process.argv.slice(2));
110
- await run(args);
111
- } catch (error) {
112
- // eslint-disable-next-line no-console
113
- console.error((error as Error).message);
114
- process.exitCode = 1;
115
- }
116
- }
@@ -1,110 +0,0 @@
1
- import type { AsciicastEvent, AsciicastHeader } from "./asciicast";
2
- import { encodeAsciicast } from "./asciicast";
3
-
4
- export type TraceRecorderOptions = {
5
- maxEvents?: number;
6
- maxDataChars?: number;
7
- mergeOutput?: boolean;
8
- timePrecisionMs?: number;
9
- };
10
-
11
- export type TraceSnapshot = {
12
- header: AsciicastHeader;
13
- events: AsciicastEvent[];
14
- cast: string;
15
- droppedEvents: number;
16
- droppedDataChars: number;
17
- };
18
-
19
- const DEFAULT_MAX_EVENTS = 50_000;
20
- const DEFAULT_MAX_DATA_CHARS = 5_000_000;
21
- const DEFAULT_TIME_PRECISION_MS = 1;
22
-
23
- export class TraceRecorder {
24
- private readonly header: AsciicastHeader;
25
- private readonly startedAtMs: number;
26
- private readonly maxEvents: number;
27
- private readonly maxDataChars: number;
28
- private readonly mergeOutput: boolean;
29
- private readonly timePrecisionMs: number;
30
-
31
- private readonly events: AsciicastEvent[] = [];
32
- private dataChars = 0;
33
- private droppedEvents = 0;
34
- private droppedDataChars = 0;
35
-
36
- constructor(header: AsciicastHeader, options?: TraceRecorderOptions) {
37
- this.header = header;
38
- this.startedAtMs = performance.now();
39
- this.maxEvents = Math.max(1, Math.trunc(options?.maxEvents ?? DEFAULT_MAX_EVENTS));
40
- this.maxDataChars = Math.max(1, Math.trunc(options?.maxDataChars ?? DEFAULT_MAX_DATA_CHARS));
41
- this.mergeOutput = options?.mergeOutput ?? true;
42
- this.timePrecisionMs = Math.max(
43
- 1,
44
- Math.trunc(options?.timePrecisionMs ?? DEFAULT_TIME_PRECISION_MS),
45
- );
46
- }
47
-
48
- recordOutput(data: string): void {
49
- this.addEvent("o", data);
50
- }
51
-
52
- recordInput(data: string): void {
53
- this.addEvent("i", data);
54
- }
55
-
56
- recordResize(cols: number, rows: number): void {
57
- this.addEvent("r", `${cols}x${rows}`);
58
- }
59
-
60
- mark(label?: string): void {
61
- this.addEvent("m", label ?? "");
62
- }
63
-
64
- snapshot(options?: { tailEvents?: number }): TraceSnapshot {
65
- const tailEvents = options?.tailEvents;
66
- const events = tailEvents
67
- ? this.events.slice(-Math.max(0, Math.trunc(tailEvents)))
68
- : [...this.events];
69
- return {
70
- header: this.header,
71
- events,
72
- cast: encodeAsciicast(this.header, events),
73
- droppedEvents: this.droppedEvents,
74
- droppedDataChars: this.droppedDataChars,
75
- };
76
- }
77
-
78
- private addEvent(type: AsciicastEvent[1], data: string): void {
79
- const timeSeconds = this.nowSeconds();
80
-
81
- const last = this.events.at(-1);
82
- if (this.mergeOutput && type === "o" && last && last[1] === "o" && last[0] === timeSeconds) {
83
- last[2] += data;
84
- this.dataChars += data.length;
85
- this.trim();
86
- return;
87
- }
88
-
89
- this.events.push([timeSeconds, type, data]);
90
- this.dataChars += data.length;
91
- this.trim();
92
- }
93
-
94
- private nowSeconds(): number {
95
- const elapsedMs = performance.now() - this.startedAtMs;
96
- const quantized = Math.round(elapsedMs / this.timePrecisionMs) * this.timePrecisionMs;
97
- return quantized / 1000;
98
- }
99
-
100
- private trim(): void {
101
- while (this.events.length > this.maxEvents || this.dataChars > this.maxDataChars) {
102
- const removed = this.events.shift();
103
- if (!removed) break;
104
- const chars = removed[2].length;
105
- this.dataChars -= chars;
106
- this.droppedEvents += 1;
107
- this.droppedDataChars += chars;
108
- }
109
- }
110
- }