@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,171 @@
|
|
|
1
|
+
import "./duration-DUrbfMLK.js";
|
|
2
|
+
import { loadProbeConfig } from "./config-BUEMgFYN.js";
|
|
3
|
+
import { error, log } from "./output-CHUjdVDf.js";
|
|
4
|
+
import { formatReplayResult } from "./reporter-CqWc26OP.js";
|
|
5
|
+
import { defineCommand } from "citty";
|
|
6
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { x } from "tinyexec";
|
|
9
|
+
|
|
10
|
+
//#region src/testing/replayer.ts
|
|
11
|
+
async function replayRecording(opts) {
|
|
12
|
+
const config = await loadProbeConfig();
|
|
13
|
+
const testsDir = config.tests?.dir ?? "tests/qprobe";
|
|
14
|
+
const specPath = join(testsDir, "recordings", `${opts.name}.spec.ts`);
|
|
15
|
+
await ensurePlaywrightConfig(testsDir, opts);
|
|
16
|
+
const args = [
|
|
17
|
+
"playwright",
|
|
18
|
+
"test",
|
|
19
|
+
specPath
|
|
20
|
+
];
|
|
21
|
+
if (opts.headed) args.push("--headed");
|
|
22
|
+
if (opts.browser) args.push("--project", opts.browser);
|
|
23
|
+
if (opts.parallel) args.push("--workers", "4");
|
|
24
|
+
if (opts.retries) args.push("--retries", String(opts.retries));
|
|
25
|
+
if (opts.report) args.push("--reporter", "html");
|
|
26
|
+
const result = await x("npx", args, {
|
|
27
|
+
timeout: (config.tests?.timeout ?? 3e4) * 10,
|
|
28
|
+
throwOnError: false
|
|
29
|
+
});
|
|
30
|
+
const output = result.stdout + result.stderr;
|
|
31
|
+
const passMatch = output.match(/(\d+) passed/);
|
|
32
|
+
const failMatch = output.match(/(\d+) failed/);
|
|
33
|
+
return {
|
|
34
|
+
exitCode: result.exitCode ?? 0,
|
|
35
|
+
passed: passMatch ? Number(passMatch[1]) : 0,
|
|
36
|
+
failed: failMatch ? Number(failMatch[1]) : 0,
|
|
37
|
+
output
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async function replayAll(opts) {
|
|
41
|
+
const config = await loadProbeConfig();
|
|
42
|
+
const testsDir = config.tests?.dir ?? "tests/qprobe";
|
|
43
|
+
const recordingsDir = join(testsDir, "recordings");
|
|
44
|
+
await ensurePlaywrightConfig(testsDir, opts);
|
|
45
|
+
const args = [
|
|
46
|
+
"playwright",
|
|
47
|
+
"test",
|
|
48
|
+
recordingsDir,
|
|
49
|
+
"--grep",
|
|
50
|
+
"\\.spec\\.ts$"
|
|
51
|
+
];
|
|
52
|
+
if (opts.headed) args.push("--headed");
|
|
53
|
+
if (opts.browser) args.push("--project", opts.browser);
|
|
54
|
+
if (opts.parallel) args.push("--workers", "4");
|
|
55
|
+
if (opts.retries) args.push("--retries", String(opts.retries));
|
|
56
|
+
if (opts.report) args.push("--reporter", "html");
|
|
57
|
+
const result = await x("npx", args, {
|
|
58
|
+
timeout: (config.tests?.timeout ?? 3e4) * 20,
|
|
59
|
+
throwOnError: false
|
|
60
|
+
});
|
|
61
|
+
const output = result.stdout + result.stderr;
|
|
62
|
+
const passMatch = output.match(/(\d+) passed/);
|
|
63
|
+
const failMatch = output.match(/(\d+) failed/);
|
|
64
|
+
return {
|
|
65
|
+
exitCode: result.exitCode ?? 0,
|
|
66
|
+
passed: passMatch ? Number(passMatch[1]) : 0,
|
|
67
|
+
failed: failMatch ? Number(failMatch[1]) : 0,
|
|
68
|
+
output
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
async function ensurePlaywrightConfig(testsDir, opts) {
|
|
72
|
+
const config = await loadProbeConfig();
|
|
73
|
+
const baseUrl = opts.base ?? config.browser?.baseUrl ?? config.http?.baseUrl ?? "http://localhost:3000";
|
|
74
|
+
const content = `import { defineConfig } from '@playwright/test'
|
|
75
|
+
|
|
76
|
+
export default defineConfig({
|
|
77
|
+
testDir: './recordings',
|
|
78
|
+
timeout: ${config.tests?.timeout ?? 3e4},
|
|
79
|
+
use: {
|
|
80
|
+
baseURL: '${baseUrl}',
|
|
81
|
+
trace: 'on-first-retry',
|
|
82
|
+
},
|
|
83
|
+
projects: [
|
|
84
|
+
{ name: 'chromium', use: { browserName: 'chromium' } },
|
|
85
|
+
{ name: 'firefox', use: { browserName: 'firefox' } },
|
|
86
|
+
{ name: 'webkit', use: { browserName: 'webkit' } },
|
|
87
|
+
],
|
|
88
|
+
})
|
|
89
|
+
`;
|
|
90
|
+
await mkdir(testsDir, { recursive: true });
|
|
91
|
+
await writeFile(join(testsDir, "playwright.config.ts"), content, "utf-8");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/commands/replay.ts
|
|
96
|
+
const command = defineCommand({
|
|
97
|
+
meta: {
|
|
98
|
+
name: "replay",
|
|
99
|
+
description: "Run recorded Playwright tests"
|
|
100
|
+
},
|
|
101
|
+
args: {
|
|
102
|
+
name: {
|
|
103
|
+
type: "positional",
|
|
104
|
+
description: "Recording name to replay",
|
|
105
|
+
required: false
|
|
106
|
+
},
|
|
107
|
+
all: {
|
|
108
|
+
type: "boolean",
|
|
109
|
+
description: "Replay all recordings",
|
|
110
|
+
default: false
|
|
111
|
+
},
|
|
112
|
+
headed: {
|
|
113
|
+
type: "boolean",
|
|
114
|
+
description: "Show browser window",
|
|
115
|
+
default: false
|
|
116
|
+
},
|
|
117
|
+
browser: {
|
|
118
|
+
type: "string",
|
|
119
|
+
description: "Browser to use (chromium, firefox, webkit)"
|
|
120
|
+
},
|
|
121
|
+
parallel: {
|
|
122
|
+
type: "boolean",
|
|
123
|
+
description: "Run tests in parallel",
|
|
124
|
+
default: false
|
|
125
|
+
},
|
|
126
|
+
report: {
|
|
127
|
+
type: "boolean",
|
|
128
|
+
description: "Generate HTML report",
|
|
129
|
+
default: false
|
|
130
|
+
},
|
|
131
|
+
retries: {
|
|
132
|
+
type: "string",
|
|
133
|
+
description: "Number of retries for flaky tests"
|
|
134
|
+
},
|
|
135
|
+
base: {
|
|
136
|
+
type: "string",
|
|
137
|
+
description: "Override base URL"
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
async run({ args }) {
|
|
141
|
+
try {
|
|
142
|
+
const opts = {
|
|
143
|
+
headed: args.headed,
|
|
144
|
+
browser: args.browser,
|
|
145
|
+
parallel: args.parallel,
|
|
146
|
+
report: args.report,
|
|
147
|
+
retries: args.retries ? Number(args.retries) : void 0,
|
|
148
|
+
base: args.base
|
|
149
|
+
};
|
|
150
|
+
let result;
|
|
151
|
+
if (args.all) result = await replayAll(opts);
|
|
152
|
+
else if (args.name) result = await replayRecording({
|
|
153
|
+
name: args.name,
|
|
154
|
+
...opts
|
|
155
|
+
});
|
|
156
|
+
else {
|
|
157
|
+
error("Provide a recording name or use --all");
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
log(formatReplayResult(result));
|
|
161
|
+
if (result.exitCode !== 0) process.exit(6);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
error(err instanceof Error ? err.message : String(err));
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
var replay_default = command;
|
|
169
|
+
|
|
170
|
+
//#endregion
|
|
171
|
+
export { replay_default as default };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/testing/reporter.ts
|
|
2
|
+
function formatReplayResult(result) {
|
|
3
|
+
const lines = [];
|
|
4
|
+
if (result.exitCode === 0) lines.push(`\u2705 All tests passed (${result.passed} passed)`);
|
|
5
|
+
else lines.push(`\u274c Tests failed (${result.passed} passed, ${result.failed} failed)`);
|
|
6
|
+
if (result.output.trim()) {
|
|
7
|
+
lines.push("");
|
|
8
|
+
lines.push(result.output.trim());
|
|
9
|
+
}
|
|
10
|
+
return lines.join("\n");
|
|
11
|
+
}
|
|
12
|
+
function formatRecordingsList(recordings) {
|
|
13
|
+
if (recordings.length === 0) return "No recordings found";
|
|
14
|
+
const lines = [];
|
|
15
|
+
lines.push("NAME ACTIONS DATE");
|
|
16
|
+
for (const r of recordings) {
|
|
17
|
+
const name = r.name.padEnd(24);
|
|
18
|
+
const actions = String(r.actions).padEnd(9);
|
|
19
|
+
lines.push(`${name}${actions}${r.date}`);
|
|
20
|
+
}
|
|
21
|
+
return lines.join("\n");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
//#endregion
|
|
25
|
+
export { formatRecordingsList, formatReplayResult };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { error, success } from "./output-CHUjdVDf.js";
|
|
2
|
+
import "./state-DRTSIt_r.js";
|
|
3
|
+
import { getProcessState, startProcess, stopProcess } from "./process-manager-CzexpFO4.js";
|
|
4
|
+
import { defineCommand } from "citty";
|
|
5
|
+
|
|
6
|
+
//#region src/commands/restart.ts
|
|
7
|
+
const command = defineCommand({
|
|
8
|
+
meta: {
|
|
9
|
+
name: "restart",
|
|
10
|
+
description: "Restart a process with its original config"
|
|
11
|
+
},
|
|
12
|
+
args: { name: {
|
|
13
|
+
type: "positional",
|
|
14
|
+
description: "Process name to restart",
|
|
15
|
+
required: true
|
|
16
|
+
} },
|
|
17
|
+
async run({ args }) {
|
|
18
|
+
try {
|
|
19
|
+
const state = await getProcessState(args.name);
|
|
20
|
+
if (!state) {
|
|
21
|
+
error(`No saved state for "${args.name}"`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
await stopProcess(args.name);
|
|
25
|
+
const { pid } = await startProcess({
|
|
26
|
+
name: state.name,
|
|
27
|
+
cmd: state.cmd,
|
|
28
|
+
ready: state.ready,
|
|
29
|
+
timeout: state.timeout,
|
|
30
|
+
port: state.port,
|
|
31
|
+
env: state.env,
|
|
32
|
+
cwd: state.cwd
|
|
33
|
+
});
|
|
34
|
+
success(`Restarted "${args.name}" (PID ${pid})`);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
error(err instanceof Error ? err.message : String(err));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
var restart_default = command;
|
|
42
|
+
|
|
43
|
+
//#endregion
|
|
44
|
+
export { restart_default as default };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
//#region src/browser/snapshot-diff.ts
|
|
2
|
+
function parseSnapshot(text) {
|
|
3
|
+
const elements = [];
|
|
4
|
+
const lines = text.split("\n");
|
|
5
|
+
for (const line of lines) {
|
|
6
|
+
const trimmed = line.trim();
|
|
7
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("URL:")) continue;
|
|
8
|
+
const match = trimmed.match(/^-\s+(\w+)\s+"([^"]*)"\s+\[ref=(@e\d+)\]/);
|
|
9
|
+
if (match) {
|
|
10
|
+
elements.push({
|
|
11
|
+
role: match[1],
|
|
12
|
+
name: match[2],
|
|
13
|
+
ref: match[3]
|
|
14
|
+
});
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const matchNoQuote = trimmed.match(/^-\s+(\w+)\s+(.+?)\s+\[ref=(@e\d+)\]/);
|
|
18
|
+
if (matchNoQuote) elements.push({
|
|
19
|
+
role: matchNoQuote[1],
|
|
20
|
+
name: matchNoQuote[2],
|
|
21
|
+
ref: matchNoQuote[3]
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return elements;
|
|
25
|
+
}
|
|
26
|
+
function extractUrl(text) {
|
|
27
|
+
const match = text.match(/^URL:\s*(.+)$/m);
|
|
28
|
+
return match ? match[1].trim() : null;
|
|
29
|
+
}
|
|
30
|
+
function elementKey(el) {
|
|
31
|
+
return `${el.role}:${el.name}`;
|
|
32
|
+
}
|
|
33
|
+
function diffSnapshots(previous, current) {
|
|
34
|
+
const prevElements = parseSnapshot(previous);
|
|
35
|
+
const currElements = parseSnapshot(current);
|
|
36
|
+
const prevUrl = extractUrl(previous);
|
|
37
|
+
const currUrl = extractUrl(current);
|
|
38
|
+
const prevKeys = new Map();
|
|
39
|
+
for (const el of prevElements) prevKeys.set(elementKey(el), el);
|
|
40
|
+
const currKeys = new Map();
|
|
41
|
+
for (const el of currElements) currKeys.set(elementKey(el), el);
|
|
42
|
+
const lines = [];
|
|
43
|
+
if (prevUrl && currUrl && prevUrl !== currUrl) lines.push(`URL: ${prevUrl} \u2192 ${currUrl}`);
|
|
44
|
+
for (const [key, el] of prevKeys) if (!currKeys.has(key)) lines.push(`REMOVED: ${el.role} "${el.name}" [${el.ref}]`);
|
|
45
|
+
for (const [key, el] of currKeys) if (!prevKeys.has(key)) lines.push(`ADDED: ${el.role} "${el.name}" [${el.ref}]`);
|
|
46
|
+
if (lines.length === 0) return "No changes detected";
|
|
47
|
+
return lines.join("\n");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
//#endregion
|
|
51
|
+
export { diffSnapshots };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { parseDuration } from "./duration-DUrbfMLK.js";
|
|
2
|
+
import { error, success } from "./output-CHUjdVDf.js";
|
|
3
|
+
import "./state-DRTSIt_r.js";
|
|
4
|
+
import { startProcess } from "./process-manager-CzexpFO4.js";
|
|
5
|
+
import { defineCommand } from "citty";
|
|
6
|
+
|
|
7
|
+
//#region src/commands/start.ts
|
|
8
|
+
const command = defineCommand({
|
|
9
|
+
meta: {
|
|
10
|
+
name: "start",
|
|
11
|
+
description: "Start a background process with ready detection"
|
|
12
|
+
},
|
|
13
|
+
args: {
|
|
14
|
+
name: {
|
|
15
|
+
type: "positional",
|
|
16
|
+
description: "Process name",
|
|
17
|
+
required: true
|
|
18
|
+
},
|
|
19
|
+
cmd: {
|
|
20
|
+
type: "positional",
|
|
21
|
+
description: "Command to run",
|
|
22
|
+
required: true
|
|
23
|
+
},
|
|
24
|
+
ready: {
|
|
25
|
+
type: "string",
|
|
26
|
+
description: "Ready pattern (regex/string in stdout)"
|
|
27
|
+
},
|
|
28
|
+
timeout: {
|
|
29
|
+
type: "string",
|
|
30
|
+
description: "Max wait for ready (e.g. 60s, 5m)",
|
|
31
|
+
default: "60s"
|
|
32
|
+
},
|
|
33
|
+
port: {
|
|
34
|
+
type: "string",
|
|
35
|
+
description: "Port for health checks"
|
|
36
|
+
},
|
|
37
|
+
env: {
|
|
38
|
+
type: "string",
|
|
39
|
+
description: "Environment variables as comma-separated KEY=VAL pairs (e.g. \"PORT=3000,DB_URL=postgres://...\")"
|
|
40
|
+
},
|
|
41
|
+
cwd: {
|
|
42
|
+
type: "string",
|
|
43
|
+
description: "Working directory"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
async run({ args }) {
|
|
47
|
+
const envVars = {};
|
|
48
|
+
if (args.env) {
|
|
49
|
+
const pairs = args.env.split(/,(?=[A-Za-z_][A-Za-z0-9_]*=)/);
|
|
50
|
+
for (const pair of pairs) {
|
|
51
|
+
const eqIndex = pair.indexOf("=");
|
|
52
|
+
if (eqIndex > 0) {
|
|
53
|
+
const key = pair.slice(0, eqIndex);
|
|
54
|
+
const value = pair.slice(eqIndex + 1);
|
|
55
|
+
envVars[key] = value;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const { pid } = await startProcess({
|
|
61
|
+
name: args.name,
|
|
62
|
+
cmd: args.cmd,
|
|
63
|
+
ready: args.ready,
|
|
64
|
+
timeout: parseDuration(args.timeout),
|
|
65
|
+
port: args.port ? Number(args.port) : void 0,
|
|
66
|
+
env: Object.keys(envVars).length > 0 ? envVars : void 0,
|
|
67
|
+
cwd: args.cwd
|
|
68
|
+
});
|
|
69
|
+
success(`Started "${args.name}" (PID ${pid})`);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
error(err instanceof Error ? err.message : String(err));
|
|
72
|
+
process.exit(err instanceof Error && err.message.includes("Timeout") ? 2 : 1);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
var start_default = command;
|
|
77
|
+
|
|
78
|
+
//#endregion
|
|
79
|
+
export { start_default as default };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
//#region src/core/state.ts
|
|
5
|
+
const BASE_DIR = "tmp/qprobe";
|
|
6
|
+
const PIDS_DIR = join(BASE_DIR, "pids");
|
|
7
|
+
const STATE_DIR = join(BASE_DIR, "state");
|
|
8
|
+
const LOGS_DIR = join(BASE_DIR, "logs");
|
|
9
|
+
async function ensureDir(dir) {
|
|
10
|
+
await mkdir(dir, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
function getLogPath(name) {
|
|
13
|
+
return join(LOGS_DIR, `${name}.log`);
|
|
14
|
+
}
|
|
15
|
+
async function savePid(name, pid) {
|
|
16
|
+
await ensureDir(PIDS_DIR);
|
|
17
|
+
await writeFile(join(PIDS_DIR, `${name}.pid`), String(pid), "utf-8");
|
|
18
|
+
}
|
|
19
|
+
async function readPid(name) {
|
|
20
|
+
try {
|
|
21
|
+
const content = await readFile(join(PIDS_DIR, `${name}.pid`), "utf-8");
|
|
22
|
+
return Number.parseInt(content.trim(), 10);
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function removePid(name) {
|
|
28
|
+
try {
|
|
29
|
+
await rm(join(PIDS_DIR, `${name}.pid`));
|
|
30
|
+
} catch {}
|
|
31
|
+
}
|
|
32
|
+
async function saveState(name, state) {
|
|
33
|
+
await ensureDir(STATE_DIR);
|
|
34
|
+
await writeFile(join(STATE_DIR, `${name}.json`), JSON.stringify(state, null, 2), "utf-8");
|
|
35
|
+
}
|
|
36
|
+
async function readState(name) {
|
|
37
|
+
try {
|
|
38
|
+
const content = await readFile(join(STATE_DIR, `${name}.json`), "utf-8");
|
|
39
|
+
return JSON.parse(content);
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function removeState(name) {
|
|
45
|
+
try {
|
|
46
|
+
await rm(join(STATE_DIR, `${name}.json`));
|
|
47
|
+
} catch {}
|
|
48
|
+
}
|
|
49
|
+
async function listProcessNames() {
|
|
50
|
+
try {
|
|
51
|
+
const files = await readdir(PIDS_DIR);
|
|
52
|
+
return files.filter((f) => f.endsWith(".pid")).map((f) => f.replace(".pid", ""));
|
|
53
|
+
} catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function ensureLogsDir() {
|
|
58
|
+
await ensureDir(LOGS_DIR);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
//#endregion
|
|
62
|
+
export { ensureLogsDir, getLogPath, listProcessNames, readPid, readState, removePid, removeState, savePid, saveState };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { error, success } from "./output-CHUjdVDf.js";
|
|
2
|
+
import "./state-DRTSIt_r.js";
|
|
3
|
+
import { stopAll, stopProcess } from "./process-manager-CzexpFO4.js";
|
|
4
|
+
import { defineCommand } from "citty";
|
|
5
|
+
|
|
6
|
+
//#region src/commands/stop.ts
|
|
7
|
+
const command = defineCommand({
|
|
8
|
+
meta: {
|
|
9
|
+
name: "stop",
|
|
10
|
+
description: "Stop a running process (SIGTERM → SIGKILL)"
|
|
11
|
+
},
|
|
12
|
+
args: {
|
|
13
|
+
name: {
|
|
14
|
+
type: "positional",
|
|
15
|
+
description: "Process name to stop",
|
|
16
|
+
required: false
|
|
17
|
+
},
|
|
18
|
+
all: {
|
|
19
|
+
type: "boolean",
|
|
20
|
+
description: "Stop all processes",
|
|
21
|
+
default: false
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
async run({ args }) {
|
|
25
|
+
try {
|
|
26
|
+
if (args.all) {
|
|
27
|
+
const stopped = await stopAll();
|
|
28
|
+
if (stopped.length === 0) success("No processes running");
|
|
29
|
+
else success(`Stopped ${stopped.length} process(es): ${stopped.join(", ")}`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (!args.name) {
|
|
33
|
+
error("Provide a process name or use --all");
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
await stopProcess(args.name);
|
|
37
|
+
success(`Stopped "${args.name}"`);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
error(err instanceof Error ? err.message : String(err));
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
var stop_default = command;
|
|
45
|
+
|
|
46
|
+
//#endregion
|
|
47
|
+
export { stop_default as default };
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@questpie/probe",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Dev testing CLI for AI coding agents",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"qprobe": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"skills",
|
|
19
|
+
"templates"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"dev": "tsdown --watch",
|
|
23
|
+
"build": "tsdown",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"test": "bun test",
|
|
26
|
+
"lint": "biome check .",
|
|
27
|
+
"prepublishOnly": "bun run build"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"c12": "^2.0.0",
|
|
31
|
+
"chokidar": "^4.0.0",
|
|
32
|
+
"citty": "^0.1.0",
|
|
33
|
+
"consola": "^3.0.0",
|
|
34
|
+
"defu": "^6.0.0",
|
|
35
|
+
"ofetch": "^1.0.0",
|
|
36
|
+
"tinyexec": "^0.3.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@biomejs/biome": "^1.9.0",
|
|
40
|
+
"@types/bun": "latest",
|
|
41
|
+
"tsdown": "^0.9.0",
|
|
42
|
+
"typescript": "^5.7.0"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"agent-browser": ">=0.1.0",
|
|
46
|
+
"@playwright/test": ">=1.45.0"
|
|
47
|
+
},
|
|
48
|
+
"peerDependenciesMeta": {
|
|
49
|
+
"agent-browser": {
|
|
50
|
+
"optional": false
|
|
51
|
+
},
|
|
52
|
+
"@playwright/test": {
|
|
53
|
+
"optional": true
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"keywords": [
|
|
57
|
+
"testing",
|
|
58
|
+
"ai",
|
|
59
|
+
"agent",
|
|
60
|
+
"browser-automation",
|
|
61
|
+
"cli",
|
|
62
|
+
"dev-tools",
|
|
63
|
+
"playwright",
|
|
64
|
+
"questpie"
|
|
65
|
+
],
|
|
66
|
+
"repository": {
|
|
67
|
+
"type": "git",
|
|
68
|
+
"url": "https://github.com/questpie/probe"
|
|
69
|
+
},
|
|
70
|
+
"author": "QUESTPIE <hello@questpie.com>",
|
|
71
|
+
"homepage": "https://probe.questpie.com"
|
|
72
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: qprobe
|
|
3
|
+
description: "QUESTPIE Probe — dev testing CLI for AI coding agents. Orchestrates dev servers, aggregates logs, controls browsers, sends HTTP requests, records and replays tests. Use when: testing web apps, starting dev servers, checking logs, debugging runtime errors, recording test flows, making API calls against running servers, checking browser console or network tab, composing multi-service stacks. Triggers: 'test this app', 'start the server', 'check if it works', 'what are the logs', 'any errors?', 'record a test', 'run the tests', 'call the API', 'check the network tab', 'start the database', 'compose up', 'is the server running', 'replay tests', 'check for regressions'. Even if the user doesn't mention qprobe by name, use this skill when they want to test, debug, or verify a running web application."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# QUESTPIE Probe
|
|
7
|
+
|
|
8
|
+
Dev testing CLI. Manages dev servers, reads logs, controls browsers, sends HTTP requests, records and replays tests.
|
|
9
|
+
|
|
10
|
+
**Install:** `npm install -g @questpie/probe`
|
|
11
|
+
|
|
12
|
+
## Core Principle
|
|
13
|
+
|
|
14
|
+
> Read logs before opening a browser. 90% of debugging needs zero visual context.
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
qprobe compose up # start all services from config
|
|
20
|
+
qprobe check http://localhost:3000 # health + console + network summary
|
|
21
|
+
qprobe logs server --grep "ERROR" # check process logs
|
|
22
|
+
qprobe http GET /api/health # test API endpoint
|
|
23
|
+
qprobe browser open http://localhost:3000 # open browser (only if needed)
|
|
24
|
+
qprobe browser snapshot -i # see interactive elements
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Command Groups
|
|
28
|
+
|
|
29
|
+
| Command | What it does | When to use |
|
|
30
|
+
|---------|-------------|-------------|
|
|
31
|
+
| `qprobe start` | Start a process with ready detection | Need to run dev server, DB, worker |
|
|
32
|
+
| `qprobe stop` | Stop a running process | Cleanup, restart |
|
|
33
|
+
| `qprobe ps` | List running processes | Check what's running |
|
|
34
|
+
| `qprobe health` | Poll URL until healthy | Wait for server to be ready |
|
|
35
|
+
| `qprobe compose` | Multi-service orchestration | Start full stack (DB+server+admin) |
|
|
36
|
+
| `qprobe logs` | Read/filter/follow process logs | Debug server errors |
|
|
37
|
+
| `qprobe http` | Send HTTP requests | Test API endpoints |
|
|
38
|
+
| `qprobe check` | All-in-one health summary | Quick status overview |
|
|
39
|
+
| `qprobe browser` | Browser automation | Visual testing, form filling |
|
|
40
|
+
| `qprobe record` | Record a test session | Capture a flow for regression |
|
|
41
|
+
| `qprobe replay` | Replay recorded tests | Run regression (zero AI tokens) |
|
|
42
|
+
| `qprobe assert` | Assert conditions | Verify state in CI or scripts |
|
|
43
|
+
|
|
44
|
+
## Recommended Workflow
|
|
45
|
+
|
|
46
|
+
1. **Start stack** → `qprobe compose up`
|
|
47
|
+
2. **Read logs** → `qprobe logs --all --grep ERROR` (no browser yet!)
|
|
48
|
+
3. **Test API** → `qprobe http GET /api/health` (still no browser!)
|
|
49
|
+
4. **Quick check** → `qprobe check` (summarizes everything)
|
|
50
|
+
5. **Open browser** → `qprobe browser open /login` (only if needed)
|
|
51
|
+
6. **Record test** → `qprobe record start "login-flow"`
|
|
52
|
+
7. **Replay free** → `qprobe replay --all` (pure Playwright, zero tokens)
|
|
53
|
+
|
|
54
|
+
## Sub-Skills
|
|
55
|
+
|
|
56
|
+
For detailed reference on each command group, read the corresponding reference file:
|
|
57
|
+
|
|
58
|
+
- **Process management** (start, stop, ps, health, restart) → read `references/process.md`
|
|
59
|
+
- **Compose** (multi-service orchestration, config file) → read `references/compose.md`
|
|
60
|
+
- **HTTP requests** (API testing, auth, assertions) → read `references/http.md`
|
|
61
|
+
- **Browser control** (snapshot, click, fill, console, network) → read `references/browser.md`
|
|
62
|
+
- **Recording & replay** (test capture, Playwright codegen) → read `references/recording.md`
|
|
63
|
+
|
|
64
|
+
## Config File
|
|
65
|
+
|
|
66
|
+
Create `qprobe.config.ts` in project root (optional — CLI works without it):
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { defineConfig } from '@questpie/probe'
|
|
70
|
+
|
|
71
|
+
export default defineConfig({
|
|
72
|
+
services: {
|
|
73
|
+
db: {
|
|
74
|
+
cmd: 'docker compose up postgres',
|
|
75
|
+
ready: 'ready to accept connections',
|
|
76
|
+
},
|
|
77
|
+
server: {
|
|
78
|
+
cmd: 'bun dev',
|
|
79
|
+
ready: 'ready on http://localhost:3000',
|
|
80
|
+
port: 3000,
|
|
81
|
+
health: '/api/health',
|
|
82
|
+
depends: ['db'],
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
browser: {
|
|
86
|
+
driver: 'agent-browser',
|
|
87
|
+
baseUrl: 'http://localhost:3000',
|
|
88
|
+
},
|
|
89
|
+
http: {
|
|
90
|
+
baseUrl: 'http://localhost:3000',
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Tips for Agents
|
|
96
|
+
|
|
97
|
+
- **Always check logs before opening browser** — most bugs are visible in `qprobe logs`
|
|
98
|
+
- **Use `qprobe check`** for a one-command overview of server health
|
|
99
|
+
- **Use `qprobe http`** instead of browser for API testing — much cheaper on tokens
|
|
100
|
+
- **Snapshot with `-i`** (interactive only) to save tokens — skip structural/decorative elements
|
|
101
|
+
- **Snapshot with `--diff`** after actions to see only what changed
|
|
102
|
+
- **Record flows you want to verify later** — replay costs zero AI tokens
|
|
103
|
+
- **Grep is your friend** — `qprobe logs server --grep "ERROR"` is faster than reading everything
|