@questpie/probe 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/dist/agent-browser-Cxuu-Zz0.js +203 -0
- package/dist/assert-BLP5_JwC.js +212 -0
- package/dist/browser-DoCXU5Bs.js +736 -0
- package/dist/check-Cny-3lkZ.js +41 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +30 -0
- package/dist/codegen-BH3cUNuf.js +61 -0
- package/dist/compose-D5a8qHkg.js +233 -0
- package/dist/config-BUEMgFYN.js +89 -0
- package/dist/duration-D1ya1zLn.js +3 -0
- package/dist/duration-DUrbfMLK.js +30 -0
- package/dist/health-B36ufFzJ.js +62 -0
- package/dist/http-BZouO1Cj.js +187 -0
- package/dist/index.d.ts +119 -0
- package/dist/index.js +4 -0
- package/dist/init-BjTfn_-A.js +92 -0
- package/dist/logs-BCgur07G.js +191 -0
- package/dist/output-CHUjdVDf.js +38 -0
- package/dist/process-manager-CzexpFO4.js +229 -0
- package/dist/process-manager-zzltWvZ0.js +4 -0
- package/dist/ps-DuHF7vmE.js +39 -0
- package/dist/record-C4SmoPsT.js +140 -0
- package/dist/recordings-Cb31alos.js +158 -0
- package/dist/replay-Dg9PHNrg.js +171 -0
- package/dist/reporter-CqWc26OP.js +25 -0
- package/dist/restart-By3Edj5X.js +44 -0
- package/dist/snapshot-diff-CqXEVTAZ.js +51 -0
- package/dist/start-BClY6oJq.js +79 -0
- package/dist/state-DRTSIt_r.js +62 -0
- package/dist/stop-QAP6gbDe.js +47 -0
- package/package.json +72 -0
- package/skills/qprobe/SKILL.md +103 -0
- package/skills/qprobe/references/browser.md +201 -0
- package/skills/qprobe/references/compose.md +128 -0
- package/skills/qprobe/references/http.md +151 -0
- package/skills/qprobe/references/process.md +114 -0
- package/skills/qprobe/references/recording.md +194 -0
- package/skills/qprobe-browser/SKILL.md +87 -0
- package/skills/qprobe-compose/SKILL.md +81 -0
- package/skills/qprobe-http/SKILL.md +67 -0
- package/skills/qprobe-process/SKILL.md +58 -0
- package/skills/qprobe-recording/SKILL.md +63 -0
- package/skills/qprobe-ux/SKILL.md +250 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { ensureLogsDir, getLogPath, listProcessNames, readPid, readState, removePid, removeState, savePid, saveState } from "./state-DRTSIt_r.js";
|
|
2
|
+
import { appendFile } from "node:fs/promises";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { openSync } from "node:fs";
|
|
5
|
+
|
|
6
|
+
//#region src/core/log-writer.ts
|
|
7
|
+
function timestamp() {
|
|
8
|
+
return new Date().toISOString();
|
|
9
|
+
}
|
|
10
|
+
function detectLevel(line) {
|
|
11
|
+
const lower = line.toLowerCase();
|
|
12
|
+
if (lower.includes("error") || lower.includes("err ")) return "ERROR";
|
|
13
|
+
if (lower.includes("warn")) return "WARN";
|
|
14
|
+
if (lower.includes("debug")) return "DEBUG";
|
|
15
|
+
return "INFO";
|
|
16
|
+
}
|
|
17
|
+
async function writeLog(name, data, level) {
|
|
18
|
+
await ensureLogsDir();
|
|
19
|
+
const logPath = getLogPath(name);
|
|
20
|
+
const lines = data.split("\n").filter((l) => l.length > 0);
|
|
21
|
+
const formatted = lines.map((line) => {
|
|
22
|
+
const lvl = level ?? detectLevel(line);
|
|
23
|
+
return `${timestamp()} ${lvl.padEnd(5)} ${line}`;
|
|
24
|
+
}).join("\n");
|
|
25
|
+
if (formatted.length > 0) await appendFile(logPath, `${formatted}\n`, "utf-8");
|
|
26
|
+
}
|
|
27
|
+
function createLogWriter(name) {
|
|
28
|
+
return {
|
|
29
|
+
stdout(chunk) {
|
|
30
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
31
|
+
writeLog(name, text);
|
|
32
|
+
},
|
|
33
|
+
stderr(chunk) {
|
|
34
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
35
|
+
writeLog(name, text, "ERROR");
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/core/process-manager.ts
|
|
42
|
+
function isProcessAlive(pid) {
|
|
43
|
+
try {
|
|
44
|
+
process.kill(pid, 0);
|
|
45
|
+
return true;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function startProcess(opts) {
|
|
51
|
+
const existingPid = await readPid(opts.name);
|
|
52
|
+
if (existingPid && isProcessAlive(existingPid)) throw new Error(`Process "${opts.name}" is already running (PID ${existingPid})`);
|
|
53
|
+
await ensureLogsDir();
|
|
54
|
+
const [command, ...args] = opts.cmd.split(/\s+/);
|
|
55
|
+
if (!command) throw new Error("Empty command");
|
|
56
|
+
if (opts.ready) {
|
|
57
|
+
const logWriter = createLogWriter(opts.name);
|
|
58
|
+
const child$1 = spawn(command, args, {
|
|
59
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
60
|
+
env: {
|
|
61
|
+
...process.env,
|
|
62
|
+
...opts.env
|
|
63
|
+
},
|
|
64
|
+
stdio: [
|
|
65
|
+
"ignore",
|
|
66
|
+
"pipe",
|
|
67
|
+
"pipe"
|
|
68
|
+
],
|
|
69
|
+
detached: true
|
|
70
|
+
});
|
|
71
|
+
const pid$1 = child$1.pid;
|
|
72
|
+
if (!pid$1) throw new Error("Failed to spawn process");
|
|
73
|
+
child$1.stdout?.on("data", logWriter.stdout);
|
|
74
|
+
child$1.stderr?.on("data", logWriter.stderr);
|
|
75
|
+
await savePid(opts.name, pid$1);
|
|
76
|
+
await saveState(opts.name, makeState(opts, pid$1));
|
|
77
|
+
const timeoutMs = opts.timeout ?? 6e4;
|
|
78
|
+
await waitForReady(child$1, opts.ready, timeoutMs);
|
|
79
|
+
child$1.stdout?.removeAllListeners();
|
|
80
|
+
child$1.stderr?.removeAllListeners();
|
|
81
|
+
child$1.stdout?.destroy();
|
|
82
|
+
child$1.stderr?.destroy();
|
|
83
|
+
child$1.unref();
|
|
84
|
+
return { pid: pid$1 };
|
|
85
|
+
}
|
|
86
|
+
const logPath = getLogPath(opts.name);
|
|
87
|
+
const logFd = openSync(logPath, "a");
|
|
88
|
+
const child = spawn(command, args, {
|
|
89
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
90
|
+
env: {
|
|
91
|
+
...process.env,
|
|
92
|
+
...opts.env
|
|
93
|
+
},
|
|
94
|
+
stdio: [
|
|
95
|
+
"ignore",
|
|
96
|
+
logFd,
|
|
97
|
+
logFd
|
|
98
|
+
],
|
|
99
|
+
detached: true
|
|
100
|
+
});
|
|
101
|
+
const pid = child.pid;
|
|
102
|
+
if (!pid) throw new Error("Failed to spawn process");
|
|
103
|
+
child.unref();
|
|
104
|
+
await savePid(opts.name, pid);
|
|
105
|
+
await saveState(opts.name, makeState(opts, pid));
|
|
106
|
+
return { pid };
|
|
107
|
+
}
|
|
108
|
+
function makeState(opts, pid) {
|
|
109
|
+
return {
|
|
110
|
+
name: opts.name,
|
|
111
|
+
cmd: opts.cmd,
|
|
112
|
+
pid,
|
|
113
|
+
port: opts.port,
|
|
114
|
+
ready: opts.ready,
|
|
115
|
+
cwd: opts.cwd,
|
|
116
|
+
env: opts.env,
|
|
117
|
+
timeout: opts.timeout,
|
|
118
|
+
startedAt: new Date().toISOString()
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function waitForReady(child, pattern, timeoutMs) {
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
const regex = new RegExp(pattern);
|
|
124
|
+
let resolved = false;
|
|
125
|
+
const timer = setTimeout(() => {
|
|
126
|
+
if (!resolved) {
|
|
127
|
+
resolved = true;
|
|
128
|
+
reject(new Error(`Timeout waiting for ready pattern "${pattern}" after ${timeoutMs}ms`));
|
|
129
|
+
}
|
|
130
|
+
}, timeoutMs);
|
|
131
|
+
const onData = (chunk) => {
|
|
132
|
+
if (resolved) return;
|
|
133
|
+
const text = chunk.toString("utf-8");
|
|
134
|
+
if (regex.test(text)) {
|
|
135
|
+
resolved = true;
|
|
136
|
+
clearTimeout(timer);
|
|
137
|
+
child.stdout?.off("data", onData);
|
|
138
|
+
child.stderr?.off("data", onStderrData);
|
|
139
|
+
resolve();
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const onStderrData = (chunk) => {
|
|
143
|
+
if (resolved) return;
|
|
144
|
+
const text = chunk.toString("utf-8");
|
|
145
|
+
if (regex.test(text)) {
|
|
146
|
+
resolved = true;
|
|
147
|
+
clearTimeout(timer);
|
|
148
|
+
child.stdout?.off("data", onData);
|
|
149
|
+
child.stderr?.off("data", onStderrData);
|
|
150
|
+
resolve();
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
child.stdout?.on("data", onData);
|
|
154
|
+
child.stderr?.on("data", onStderrData);
|
|
155
|
+
child.on("exit", (code) => {
|
|
156
|
+
if (!resolved) {
|
|
157
|
+
resolved = true;
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
reject(new Error(`Process exited with code ${code} before ready`));
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
async function stopProcess(name) {
|
|
165
|
+
const pid = await readPid(name);
|
|
166
|
+
if (!pid) throw new Error(`No PID file for "${name}"`);
|
|
167
|
+
if (!isProcessAlive(pid)) {
|
|
168
|
+
await removePid(name);
|
|
169
|
+
await removeState(name);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
process.kill(-pid, "SIGTERM");
|
|
174
|
+
} catch {
|
|
175
|
+
try {
|
|
176
|
+
process.kill(pid, "SIGTERM");
|
|
177
|
+
} catch {}
|
|
178
|
+
}
|
|
179
|
+
const deadline = Date.now() + 5e3;
|
|
180
|
+
while (Date.now() < deadline) {
|
|
181
|
+
if (!isProcessAlive(pid)) break;
|
|
182
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
183
|
+
}
|
|
184
|
+
if (isProcessAlive(pid)) try {
|
|
185
|
+
process.kill(-pid, "SIGKILL");
|
|
186
|
+
} catch {
|
|
187
|
+
try {
|
|
188
|
+
process.kill(pid, "SIGKILL");
|
|
189
|
+
} catch {}
|
|
190
|
+
}
|
|
191
|
+
await removePid(name);
|
|
192
|
+
await removeState(name);
|
|
193
|
+
}
|
|
194
|
+
async function stopAll() {
|
|
195
|
+
const names = await listProcessNames();
|
|
196
|
+
for (const name of names) await stopProcess(name);
|
|
197
|
+
return names;
|
|
198
|
+
}
|
|
199
|
+
async function listProcesses() {
|
|
200
|
+
const names = await listProcessNames();
|
|
201
|
+
const result = [];
|
|
202
|
+
for (const name of names) {
|
|
203
|
+
const state = await readState(name);
|
|
204
|
+
const pid = await readPid(name);
|
|
205
|
+
if (!state || !pid) continue;
|
|
206
|
+
const alive = isProcessAlive(pid);
|
|
207
|
+
if (!alive) {
|
|
208
|
+
await removePid(name);
|
|
209
|
+
await removeState(name);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const uptimeMs = Date.now() - new Date(state.startedAt).getTime();
|
|
213
|
+
const { formatDuration } = await import("./duration-D1ya1zLn.js");
|
|
214
|
+
result.push({
|
|
215
|
+
name: state.name,
|
|
216
|
+
pid,
|
|
217
|
+
port: state.port,
|
|
218
|
+
status: "running",
|
|
219
|
+
uptime: formatDuration(uptimeMs)
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
async function getProcessState(name) {
|
|
225
|
+
return readState(name);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
//#endregion
|
|
229
|
+
export { getProcessState, listProcesses, startProcess, stopAll, stopProcess };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { info, json, table } from "./output-CHUjdVDf.js";
|
|
2
|
+
import "./state-DRTSIt_r.js";
|
|
3
|
+
import { listProcesses } from "./process-manager-CzexpFO4.js";
|
|
4
|
+
import { defineCommand } from "citty";
|
|
5
|
+
|
|
6
|
+
//#region src/commands/ps.ts
|
|
7
|
+
const command = defineCommand({
|
|
8
|
+
meta: {
|
|
9
|
+
name: "ps",
|
|
10
|
+
description: "List running processes"
|
|
11
|
+
},
|
|
12
|
+
args: { json: {
|
|
13
|
+
type: "boolean",
|
|
14
|
+
description: "JSON output",
|
|
15
|
+
default: false
|
|
16
|
+
} },
|
|
17
|
+
async run({ args }) {
|
|
18
|
+
const processes = await listProcesses();
|
|
19
|
+
if (processes.length === 0) {
|
|
20
|
+
info("No processes running");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (args.json) {
|
|
24
|
+
json(processes);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
table(processes.map((p) => ({
|
|
28
|
+
name: p.name,
|
|
29
|
+
pid: p.pid,
|
|
30
|
+
port: p.port ?? "—",
|
|
31
|
+
status: p.status,
|
|
32
|
+
uptime: p.uptime
|
|
33
|
+
})));
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
var ps_default = command;
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
export { ps_default as default };
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import "./duration-DUrbfMLK.js";
|
|
2
|
+
import { loadProbeConfig } from "./config-BUEMgFYN.js";
|
|
3
|
+
import { error, info, success } from "./output-CHUjdVDf.js";
|
|
4
|
+
import { generatePlaywrightTest } from "./codegen-BH3cUNuf.js";
|
|
5
|
+
import { defineCommand } from "citty";
|
|
6
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
//#region src/testing/recorder.ts
|
|
10
|
+
const STATE_FILE = "tmp/qprobe/state/recording.json";
|
|
11
|
+
let activeRecording = null;
|
|
12
|
+
async function startRecording(name) {
|
|
13
|
+
if (activeRecording) throw new Error(`Already recording "${activeRecording.name}". Stop it first.`);
|
|
14
|
+
const config = await loadProbeConfig();
|
|
15
|
+
activeRecording = {
|
|
16
|
+
name,
|
|
17
|
+
startedAt: new Date().toISOString(),
|
|
18
|
+
baseUrl: config.http?.baseUrl ?? config.browser?.baseUrl,
|
|
19
|
+
actions: []
|
|
20
|
+
};
|
|
21
|
+
await mkdir("tmp/qprobe/state", { recursive: true });
|
|
22
|
+
await writeFile(STATE_FILE, JSON.stringify(activeRecording, null, 2), "utf-8");
|
|
23
|
+
}
|
|
24
|
+
async function stopRecording() {
|
|
25
|
+
const recording = activeRecording ?? await loadActiveRecording();
|
|
26
|
+
if (!recording) throw new Error("No active recording");
|
|
27
|
+
recording.finishedAt = new Date().toISOString();
|
|
28
|
+
const config = await loadProbeConfig();
|
|
29
|
+
const testsDir = config.tests?.dir ?? "tests/qprobe";
|
|
30
|
+
const recordingsDir = join(testsDir, "recordings");
|
|
31
|
+
await mkdir(recordingsDir, { recursive: true });
|
|
32
|
+
const jsonPath = join(recordingsDir, `${recording.name}.json`);
|
|
33
|
+
await writeFile(jsonPath, JSON.stringify(recording, null, 2), "utf-8");
|
|
34
|
+
activeRecording = null;
|
|
35
|
+
try {
|
|
36
|
+
const { rm: rm$1 } = await import("node:fs/promises");
|
|
37
|
+
await rm$1(STATE_FILE);
|
|
38
|
+
} catch {}
|
|
39
|
+
return recording;
|
|
40
|
+
}
|
|
41
|
+
async function cancelRecording() {
|
|
42
|
+
activeRecording = null;
|
|
43
|
+
try {
|
|
44
|
+
const { rm: rm$1 } = await import("node:fs/promises");
|
|
45
|
+
await rm$1(STATE_FILE);
|
|
46
|
+
} catch {}
|
|
47
|
+
}
|
|
48
|
+
async function loadActiveRecording() {
|
|
49
|
+
try {
|
|
50
|
+
const content = await readFile(STATE_FILE, "utf-8");
|
|
51
|
+
activeRecording = JSON.parse(content);
|
|
52
|
+
return activeRecording;
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function getActiveRecording() {
|
|
58
|
+
if (activeRecording) return activeRecording;
|
|
59
|
+
return loadActiveRecording();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/commands/record.ts
|
|
64
|
+
const start = defineCommand({
|
|
65
|
+
meta: {
|
|
66
|
+
name: "start",
|
|
67
|
+
description: "Start recording browser actions"
|
|
68
|
+
},
|
|
69
|
+
args: { name: {
|
|
70
|
+
type: "positional",
|
|
71
|
+
description: "Recording name",
|
|
72
|
+
required: true
|
|
73
|
+
} },
|
|
74
|
+
async run({ args }) {
|
|
75
|
+
try {
|
|
76
|
+
await startRecording(args.name);
|
|
77
|
+
success(`Recording "${args.name}" started`);
|
|
78
|
+
info("Browser and HTTP commands will be recorded. Run \"qprobe record stop\" when done.");
|
|
79
|
+
} catch (err) {
|
|
80
|
+
error(err instanceof Error ? err.message : String(err));
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
const stop = defineCommand({
|
|
86
|
+
meta: {
|
|
87
|
+
name: "stop",
|
|
88
|
+
description: "Stop recording and generate Playwright test"
|
|
89
|
+
},
|
|
90
|
+
args: {},
|
|
91
|
+
async run() {
|
|
92
|
+
try {
|
|
93
|
+
const recording = await stopRecording();
|
|
94
|
+
const specCode = generatePlaywrightTest(recording);
|
|
95
|
+
const config = await loadProbeConfig();
|
|
96
|
+
const testsDir = config.tests?.dir ?? "tests/qprobe";
|
|
97
|
+
const recordingsDir = join(testsDir, "recordings");
|
|
98
|
+
await mkdir(recordingsDir, { recursive: true });
|
|
99
|
+
const specPath = join(recordingsDir, `${recording.name}.spec.ts`);
|
|
100
|
+
await writeFile(specPath, specCode, "utf-8");
|
|
101
|
+
success(`Recording "${recording.name}" saved (${recording.actions.length} actions)`);
|
|
102
|
+
info(`JSON: ${join(recordingsDir, `${recording.name}.json`)}`);
|
|
103
|
+
info(`Spec: ${specPath}`);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
error(err instanceof Error ? err.message : String(err));
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
const cancel = defineCommand({
|
|
111
|
+
meta: {
|
|
112
|
+
name: "cancel",
|
|
113
|
+
description: "Cancel recording without saving"
|
|
114
|
+
},
|
|
115
|
+
args: {},
|
|
116
|
+
async run() {
|
|
117
|
+
const active = await getActiveRecording();
|
|
118
|
+
if (!active) {
|
|
119
|
+
info("No active recording");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
await cancelRecording();
|
|
123
|
+
success(`Recording "${active.name}" cancelled`);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
const command = defineCommand({
|
|
127
|
+
meta: {
|
|
128
|
+
name: "record",
|
|
129
|
+
description: "Record browser actions for replay"
|
|
130
|
+
},
|
|
131
|
+
subCommands: {
|
|
132
|
+
start,
|
|
133
|
+
stop,
|
|
134
|
+
cancel
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
var record_default = command;
|
|
138
|
+
|
|
139
|
+
//#endregion
|
|
140
|
+
export { record_default as default };
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import "./duration-DUrbfMLK.js";
|
|
2
|
+
import { loadProbeConfig } from "./config-BUEMgFYN.js";
|
|
3
|
+
import { error, info, json, log, success } from "./output-CHUjdVDf.js";
|
|
4
|
+
import { generatePlaywrightTest } from "./codegen-BH3cUNuf.js";
|
|
5
|
+
import { formatRecordingsList } from "./reporter-CqWc26OP.js";
|
|
6
|
+
import { defineCommand } from "citty";
|
|
7
|
+
import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
//#region src/commands/recordings.ts
|
|
11
|
+
async function getRecordingsDir() {
|
|
12
|
+
const config = await loadProbeConfig();
|
|
13
|
+
return join(config.tests?.dir ?? "tests/qprobe", "recordings");
|
|
14
|
+
}
|
|
15
|
+
async function loadRecording(name) {
|
|
16
|
+
const dir = await getRecordingsDir();
|
|
17
|
+
const content = await readFile(join(dir, `${name}.json`), "utf-8");
|
|
18
|
+
return JSON.parse(content);
|
|
19
|
+
}
|
|
20
|
+
const list = defineCommand({
|
|
21
|
+
meta: {
|
|
22
|
+
name: "list",
|
|
23
|
+
description: "List all recordings"
|
|
24
|
+
},
|
|
25
|
+
args: { json: {
|
|
26
|
+
type: "boolean",
|
|
27
|
+
description: "JSON output",
|
|
28
|
+
default: false
|
|
29
|
+
} },
|
|
30
|
+
async run({ args }) {
|
|
31
|
+
const dir = await getRecordingsDir();
|
|
32
|
+
let files;
|
|
33
|
+
try {
|
|
34
|
+
files = (await readdir(dir)).filter((f) => f.endsWith(".json"));
|
|
35
|
+
} catch {
|
|
36
|
+
info("No recordings found");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const recordings = [];
|
|
40
|
+
for (const file of files) try {
|
|
41
|
+
const content = await readFile(join(dir, file), "utf-8");
|
|
42
|
+
const rec = JSON.parse(content);
|
|
43
|
+
recordings.push({
|
|
44
|
+
name: rec.name,
|
|
45
|
+
actions: rec.actions.length,
|
|
46
|
+
date: rec.startedAt.split("T")[0] ?? ""
|
|
47
|
+
});
|
|
48
|
+
} catch {}
|
|
49
|
+
if (args.json) json(recordings);
|
|
50
|
+
else log(formatRecordingsList(recordings));
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
const show = defineCommand({
|
|
54
|
+
meta: {
|
|
55
|
+
name: "show",
|
|
56
|
+
description: "Show recording details"
|
|
57
|
+
},
|
|
58
|
+
args: {
|
|
59
|
+
name: {
|
|
60
|
+
type: "positional",
|
|
61
|
+
description: "Recording name",
|
|
62
|
+
required: true
|
|
63
|
+
},
|
|
64
|
+
json: {
|
|
65
|
+
type: "boolean",
|
|
66
|
+
default: false
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
async run({ args }) {
|
|
70
|
+
try {
|
|
71
|
+
const rec = await loadRecording(args.name);
|
|
72
|
+
if (args.json) json(rec);
|
|
73
|
+
else {
|
|
74
|
+
info(`Recording: ${rec.name}`);
|
|
75
|
+
info(`Started: ${rec.startedAt}`);
|
|
76
|
+
if (rec.finishedAt) info(`Finished: ${rec.finishedAt}`);
|
|
77
|
+
info(`Actions: ${rec.actions.length}`);
|
|
78
|
+
log("");
|
|
79
|
+
for (const action of rec.actions) log(` ${action.command} ${action.args.join(" ")}`);
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
error(`Recording "${args.name}" not found`);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
const deleteCmd = defineCommand({
|
|
88
|
+
meta: {
|
|
89
|
+
name: "delete",
|
|
90
|
+
description: "Delete a recording"
|
|
91
|
+
},
|
|
92
|
+
args: { name: {
|
|
93
|
+
type: "positional",
|
|
94
|
+
description: "Recording name",
|
|
95
|
+
required: true
|
|
96
|
+
} },
|
|
97
|
+
async run({ args }) {
|
|
98
|
+
const dir = await getRecordingsDir();
|
|
99
|
+
try {
|
|
100
|
+
await rm(join(dir, `${args.name}.json`));
|
|
101
|
+
} catch {}
|
|
102
|
+
try {
|
|
103
|
+
await rm(join(dir, `${args.name}.spec.ts`));
|
|
104
|
+
} catch {}
|
|
105
|
+
success(`Deleted recording "${args.name}"`);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
const exportCmd = defineCommand({
|
|
109
|
+
meta: {
|
|
110
|
+
name: "export",
|
|
111
|
+
description: "Export as standalone Playwright project"
|
|
112
|
+
},
|
|
113
|
+
args: { name: {
|
|
114
|
+
type: "positional",
|
|
115
|
+
description: "Recording name",
|
|
116
|
+
required: true
|
|
117
|
+
} },
|
|
118
|
+
async run({ args }) {
|
|
119
|
+
try {
|
|
120
|
+
const rec = await loadRecording(args.name);
|
|
121
|
+
const specCode = generatePlaywrightTest(rec);
|
|
122
|
+
const exportDir = `${args.name}-playwright`;
|
|
123
|
+
await mkdir(exportDir, { recursive: true });
|
|
124
|
+
await writeFile(join(exportDir, `${args.name}.spec.ts`), specCode, "utf-8");
|
|
125
|
+
await writeFile(join(exportDir, "package.json"), JSON.stringify({
|
|
126
|
+
name: `${args.name}-tests`,
|
|
127
|
+
scripts: { test: "playwright test" },
|
|
128
|
+
devDependencies: { "@playwright/test": ">=1.45.0" }
|
|
129
|
+
}, null, 2), "utf-8");
|
|
130
|
+
await writeFile(join(exportDir, "playwright.config.ts"), `import { defineConfig } from '@playwright/test'
|
|
131
|
+
export default defineConfig({
|
|
132
|
+
use: { baseURL: '${rec.baseUrl ?? "http://localhost:3000"}' },
|
|
133
|
+
})
|
|
134
|
+
`, "utf-8");
|
|
135
|
+
success(`Exported to ./${exportDir}/`);
|
|
136
|
+
info("Run: cd " + exportDir + " && bun install && bunx playwright test");
|
|
137
|
+
} catch {
|
|
138
|
+
error(`Recording "${args.name}" not found`);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
const command = defineCommand({
|
|
144
|
+
meta: {
|
|
145
|
+
name: "recordings",
|
|
146
|
+
description: "Manage test recordings"
|
|
147
|
+
},
|
|
148
|
+
subCommands: {
|
|
149
|
+
list,
|
|
150
|
+
show,
|
|
151
|
+
delete: deleteCmd,
|
|
152
|
+
export: exportCmd
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
var recordings_default = command;
|
|
156
|
+
|
|
157
|
+
//#endregion
|
|
158
|
+
export { recordings_default as default };
|