@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.
Files changed (43) hide show
  1. package/dist/agent-browser-Cxuu-Zz0.js +203 -0
  2. package/dist/assert-BLP5_JwC.js +212 -0
  3. package/dist/browser-DoCXU5Bs.js +736 -0
  4. package/dist/check-Cny-3lkZ.js +41 -0
  5. package/dist/cli.d.ts +1 -0
  6. package/dist/cli.js +30 -0
  7. package/dist/codegen-BH3cUNuf.js +61 -0
  8. package/dist/compose-D5a8qHkg.js +233 -0
  9. package/dist/config-BUEMgFYN.js +89 -0
  10. package/dist/duration-D1ya1zLn.js +3 -0
  11. package/dist/duration-DUrbfMLK.js +30 -0
  12. package/dist/health-B36ufFzJ.js +62 -0
  13. package/dist/http-BZouO1Cj.js +187 -0
  14. package/dist/index.d.ts +119 -0
  15. package/dist/index.js +4 -0
  16. package/dist/init-BjTfn_-A.js +92 -0
  17. package/dist/logs-BCgur07G.js +191 -0
  18. package/dist/output-CHUjdVDf.js +38 -0
  19. package/dist/process-manager-CzexpFO4.js +229 -0
  20. package/dist/process-manager-zzltWvZ0.js +4 -0
  21. package/dist/ps-DuHF7vmE.js +39 -0
  22. package/dist/record-C4SmoPsT.js +140 -0
  23. package/dist/recordings-Cb31alos.js +158 -0
  24. package/dist/replay-Dg9PHNrg.js +171 -0
  25. package/dist/reporter-CqWc26OP.js +25 -0
  26. package/dist/restart-By3Edj5X.js +44 -0
  27. package/dist/snapshot-diff-CqXEVTAZ.js +51 -0
  28. package/dist/start-BClY6oJq.js +79 -0
  29. package/dist/state-DRTSIt_r.js +62 -0
  30. package/dist/stop-QAP6gbDe.js +47 -0
  31. package/package.json +72 -0
  32. package/skills/qprobe/SKILL.md +103 -0
  33. package/skills/qprobe/references/browser.md +201 -0
  34. package/skills/qprobe/references/compose.md +128 -0
  35. package/skills/qprobe/references/http.md +151 -0
  36. package/skills/qprobe/references/process.md +114 -0
  37. package/skills/qprobe/references/recording.md +194 -0
  38. package/skills/qprobe-browser/SKILL.md +87 -0
  39. package/skills/qprobe-compose/SKILL.md +81 -0
  40. package/skills/qprobe-http/SKILL.md +67 -0
  41. package/skills/qprobe-process/SKILL.md +58 -0
  42. package/skills/qprobe-recording/SKILL.md +63 -0
  43. 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,4 @@
1
+ import "./state-DRTSIt_r.js";
2
+ import { getProcessState, listProcesses, startProcess, stopAll, stopProcess } from "./process-manager-CzexpFO4.js";
3
+
4
+ export { startProcess, 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 };