@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,41 @@
|
|
|
1
|
+
import "./duration-DUrbfMLK.js";
|
|
2
|
+
import { loadProbeConfig, resolveBaseUrl } from "./config-BUEMgFYN.js";
|
|
3
|
+
import { error, info, success, warn } from "./output-CHUjdVDf.js";
|
|
4
|
+
import "./state-DRTSIt_r.js";
|
|
5
|
+
import { listProcesses } from "./process-manager-CzexpFO4.js";
|
|
6
|
+
import { defineCommand } from "citty";
|
|
7
|
+
import { ofetch } from "ofetch";
|
|
8
|
+
|
|
9
|
+
//#region src/commands/check.ts
|
|
10
|
+
const command = defineCommand({
|
|
11
|
+
meta: {
|
|
12
|
+
name: "check",
|
|
13
|
+
description: "Quick health check of a running application"
|
|
14
|
+
},
|
|
15
|
+
args: { url: {
|
|
16
|
+
type: "positional",
|
|
17
|
+
description: "URL to check (defaults to baseUrl from config)",
|
|
18
|
+
required: false
|
|
19
|
+
} },
|
|
20
|
+
async run({ args }) {
|
|
21
|
+
const config = await loadProbeConfig();
|
|
22
|
+
const url = args.url ?? resolveBaseUrl(config);
|
|
23
|
+
info(`Checking ${url}...`);
|
|
24
|
+
try {
|
|
25
|
+
const start = performance.now();
|
|
26
|
+
const response = await ofetch.raw(url, { ignoreResponseError: true });
|
|
27
|
+
const duration = Math.round(performance.now() - start);
|
|
28
|
+
if (response.status >= 200 && response.status < 400) success(`HTTP ${response.status} ${response.statusText} (${duration}ms)`);
|
|
29
|
+
else error(`HTTP ${response.status} ${response.statusText} (${duration}ms)`);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
error(`Connection failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
32
|
+
}
|
|
33
|
+
const processes = await listProcesses();
|
|
34
|
+
if (processes.length > 0) info(`${processes.length} service(s) running (${processes.map((p) => p.name).join(", ")})`);
|
|
35
|
+
else warn("No managed services running");
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
var check_default = command;
|
|
39
|
+
|
|
40
|
+
//#endregion
|
|
41
|
+
export { check_default as default };
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { defineCommand, runMain } from "citty";
|
|
2
|
+
|
|
3
|
+
//#region src/cli.ts
|
|
4
|
+
const main = defineCommand({
|
|
5
|
+
meta: {
|
|
6
|
+
name: "qprobe",
|
|
7
|
+
version: "0.1.0",
|
|
8
|
+
description: "Dev testing CLI for AI coding agents"
|
|
9
|
+
},
|
|
10
|
+
subCommands: {
|
|
11
|
+
start: () => import("./start-BClY6oJq.js").then((m) => m.default),
|
|
12
|
+
stop: () => import("./stop-QAP6gbDe.js").then((m) => m.default),
|
|
13
|
+
restart: () => import("./restart-By3Edj5X.js").then((m) => m.default),
|
|
14
|
+
ps: () => import("./ps-DuHF7vmE.js").then((m) => m.default),
|
|
15
|
+
health: () => import("./health-B36ufFzJ.js").then((m) => m.default),
|
|
16
|
+
compose: () => import("./compose-D5a8qHkg.js").then((m) => m.default),
|
|
17
|
+
logs: () => import("./logs-BCgur07G.js").then((m) => m.default),
|
|
18
|
+
http: () => import("./http-BZouO1Cj.js").then((m) => m.default),
|
|
19
|
+
check: () => import("./check-Cny-3lkZ.js").then((m) => m.default),
|
|
20
|
+
browser: () => import("./browser-DoCXU5Bs.js").then((m) => m.default),
|
|
21
|
+
record: () => import("./record-C4SmoPsT.js").then((m) => m.default),
|
|
22
|
+
replay: () => import("./replay-Dg9PHNrg.js").then((m) => m.default),
|
|
23
|
+
recordings: () => import("./recordings-Cb31alos.js").then((m) => m.default),
|
|
24
|
+
assert: () => import("./assert-BLP5_JwC.js").then((m) => m.default),
|
|
25
|
+
init: () => import("./init-BjTfn_-A.js").then((m) => m.default)
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
runMain(main);
|
|
29
|
+
|
|
30
|
+
//#endregion
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
//#region src/testing/codegen.ts
|
|
2
|
+
function generatePlaywrightTest(recording) {
|
|
3
|
+
const lines = [];
|
|
4
|
+
lines.push("import { test, expect } from '@playwright/test'");
|
|
5
|
+
lines.push("");
|
|
6
|
+
lines.push(`test('${escapeString(recording.name)}', async ({ page }) => {`);
|
|
7
|
+
for (const action of recording.actions) {
|
|
8
|
+
const code = actionToPlaywright(action.command, action.args);
|
|
9
|
+
if (code) lines.push(` ${code}`);
|
|
10
|
+
}
|
|
11
|
+
lines.push("})");
|
|
12
|
+
lines.push("");
|
|
13
|
+
return lines.join("\n");
|
|
14
|
+
}
|
|
15
|
+
function actionToPlaywright(command, args) {
|
|
16
|
+
switch (command) {
|
|
17
|
+
case "browser open": return `await page.goto('${escapeString(args[0] ?? "")}')`;
|
|
18
|
+
case "browser click": return `await page.locator('${escapeSelector(args[0] ?? "")}').click()`;
|
|
19
|
+
case "browser dblclick": return `await page.locator('${escapeSelector(args[0] ?? "")}').dblclick()`;
|
|
20
|
+
case "browser fill": return `await page.locator('${escapeSelector(args[0] ?? "")}').fill('${escapeString(args[1] ?? "")}')`;
|
|
21
|
+
case "browser select": return `await page.locator('${escapeSelector(args[0] ?? "")}').selectOption('${escapeString(args[1] ?? "")}')`;
|
|
22
|
+
case "browser check": return `await page.locator('${escapeSelector(args[0] ?? "")}').check()`;
|
|
23
|
+
case "browser uncheck": return `await page.locator('${escapeSelector(args[0] ?? "")}').uncheck()`;
|
|
24
|
+
case "browser press": return `await page.keyboard.press('${escapeString(args[0] ?? "")}')`;
|
|
25
|
+
case "browser type": return `await page.keyboard.type('${escapeString(args[0] ?? "")}')`;
|
|
26
|
+
case "browser hover": return `await page.locator('${escapeSelector(args[0] ?? "")}').hover()`;
|
|
27
|
+
case "browser screenshot": return `await page.screenshot({ path: '${escapeString(args[0] ?? "screenshot.png")}' })`;
|
|
28
|
+
case "browser wait":
|
|
29
|
+
if (args[0]) return `await page.locator('${escapeSelector(args[0])}').waitFor()`;
|
|
30
|
+
return `await page.waitForLoadState('networkidle')`;
|
|
31
|
+
case "browser eval": return `await page.evaluate(() => { ${args[0] ?? ""} })`;
|
|
32
|
+
case "browser back": return "await page.goBack()";
|
|
33
|
+
case "browser forward": return "await page.goForward()";
|
|
34
|
+
case "browser reload": return "await page.reload()";
|
|
35
|
+
case "http GET":
|
|
36
|
+
case "http POST":
|
|
37
|
+
case "http PUT":
|
|
38
|
+
case "http DELETE":
|
|
39
|
+
case "http PATCH": {
|
|
40
|
+
const method = command.split(" ")[1];
|
|
41
|
+
const url = args[0] ?? "/";
|
|
42
|
+
return `await page.request.${method.toLowerCase()}('${escapeString(url)}')`;
|
|
43
|
+
}
|
|
44
|
+
case "assert text": return `await expect(page.locator('body')).toContainText('${escapeString(args[0] ?? "")}')`;
|
|
45
|
+
case "assert no-text": return `await expect(page.locator('body')).not.toContainText('${escapeString(args[0] ?? "")}')`;
|
|
46
|
+
case "assert element": return `await expect(page.locator('${escapeSelector(args[0] ?? "")}')).toBeVisible()`;
|
|
47
|
+
case "assert url": return `await expect(page).toHaveURL(/${escapeString(args[0] ?? "")}/)`;
|
|
48
|
+
case "assert title": return `await expect(page).toHaveTitle(/${escapeString(args[0] ?? "")}/)`;
|
|
49
|
+
default: return `// ${command} ${args.join(" ")}`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function escapeString(s) {
|
|
53
|
+
return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
54
|
+
}
|
|
55
|
+
function escapeSelector(s) {
|
|
56
|
+
if (s.startsWith("@e")) return s;
|
|
57
|
+
return escapeString(s);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
//#endregion
|
|
61
|
+
export { generatePlaywrightTest };
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import "./duration-DUrbfMLK.js";
|
|
2
|
+
import { loadProbeConfig } from "./config-BUEMgFYN.js";
|
|
3
|
+
import { error, info, json, success, table } from "./output-CHUjdVDf.js";
|
|
4
|
+
import "./state-DRTSIt_r.js";
|
|
5
|
+
import { listProcesses, startProcess, stopProcess } from "./process-manager-CzexpFO4.js";
|
|
6
|
+
import { defineCommand } from "citty";
|
|
7
|
+
import { ofetch } from "ofetch";
|
|
8
|
+
|
|
9
|
+
//#region src/core/compose-engine.ts
|
|
10
|
+
function resolveDependencyOrder(services, only, skip) {
|
|
11
|
+
const filtered = new Set();
|
|
12
|
+
if (only && only.length > 0) {
|
|
13
|
+
const queue = [...only];
|
|
14
|
+
while (queue.length > 0) {
|
|
15
|
+
const name = queue.pop();
|
|
16
|
+
if (filtered.has(name)) continue;
|
|
17
|
+
const svc = services[name];
|
|
18
|
+
if (!svc) throw new Error(`Unknown service: "${name}"`);
|
|
19
|
+
filtered.add(name);
|
|
20
|
+
if (svc.depends) for (const dep of svc.depends) queue.push(dep);
|
|
21
|
+
}
|
|
22
|
+
} else for (const name of Object.keys(services)) filtered.add(name);
|
|
23
|
+
if (skip) for (const name of skip) filtered.delete(name);
|
|
24
|
+
const sorted = [];
|
|
25
|
+
const visited = new Set();
|
|
26
|
+
const visiting = new Set();
|
|
27
|
+
function visit(name) {
|
|
28
|
+
if (visited.has(name)) return;
|
|
29
|
+
if (visiting.has(name)) throw new Error(`Circular dependency detected: ${name}`);
|
|
30
|
+
if (!filtered.has(name)) return;
|
|
31
|
+
visiting.add(name);
|
|
32
|
+
const svc = services[name];
|
|
33
|
+
if (svc?.depends) for (const dep of svc.depends) visit(dep);
|
|
34
|
+
visiting.delete(name);
|
|
35
|
+
visited.add(name);
|
|
36
|
+
sorted.push(name);
|
|
37
|
+
}
|
|
38
|
+
for (const name of filtered) visit(name);
|
|
39
|
+
return sorted;
|
|
40
|
+
}
|
|
41
|
+
async function composeUp(services, opts) {
|
|
42
|
+
const order = resolveDependencyOrder(services, opts.only, opts.skip);
|
|
43
|
+
const started = [];
|
|
44
|
+
for (const name of order) {
|
|
45
|
+
const svc = services[name];
|
|
46
|
+
await startProcess({
|
|
47
|
+
name,
|
|
48
|
+
cmd: svc.cmd,
|
|
49
|
+
ready: svc.ready,
|
|
50
|
+
timeout: svc.timeout ? svc.timeout : 6e4,
|
|
51
|
+
port: svc.port,
|
|
52
|
+
env: svc.env,
|
|
53
|
+
cwd: svc.cwd
|
|
54
|
+
});
|
|
55
|
+
started.push(name);
|
|
56
|
+
if (!opts.noHealth && svc.health) {
|
|
57
|
+
const healthUrl = svc.health.startsWith("http") ? svc.health : `http://localhost:${svc.port ?? 3e3}${svc.health}`;
|
|
58
|
+
await waitForHealth(healthUrl, svc.timeout ?? 3e4);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return started;
|
|
62
|
+
}
|
|
63
|
+
async function composeDown(services) {
|
|
64
|
+
const order = resolveDependencyOrder(services);
|
|
65
|
+
const reversed = [...order].reverse();
|
|
66
|
+
const stopped = [];
|
|
67
|
+
for (const name of reversed) try {
|
|
68
|
+
await stopProcess(name);
|
|
69
|
+
stopped.push(name);
|
|
70
|
+
} catch {}
|
|
71
|
+
return stopped;
|
|
72
|
+
}
|
|
73
|
+
async function waitForHealth(url, timeoutMs) {
|
|
74
|
+
const deadline = Date.now() + timeoutMs;
|
|
75
|
+
while (Date.now() < deadline) {
|
|
76
|
+
try {
|
|
77
|
+
const response = await ofetch.raw(url, { ignoreResponseError: true });
|
|
78
|
+
if (response.status >= 200 && response.status < 400) return;
|
|
79
|
+
} catch {}
|
|
80
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
81
|
+
}
|
|
82
|
+
throw new Error(`Health check failed for ${url} after ${timeoutMs}ms`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/commands/compose.ts
|
|
87
|
+
const up = defineCommand({
|
|
88
|
+
meta: {
|
|
89
|
+
name: "up",
|
|
90
|
+
description: "Start all services from config"
|
|
91
|
+
},
|
|
92
|
+
args: {
|
|
93
|
+
only: {
|
|
94
|
+
type: "string",
|
|
95
|
+
description: "Only start these services (comma-separated)"
|
|
96
|
+
},
|
|
97
|
+
skip: {
|
|
98
|
+
type: "string",
|
|
99
|
+
description: "Skip these services (comma-separated)"
|
|
100
|
+
},
|
|
101
|
+
"no-health": {
|
|
102
|
+
type: "boolean",
|
|
103
|
+
description: "Skip health checks",
|
|
104
|
+
default: false
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
async run({ args }) {
|
|
108
|
+
const config = await loadProbeConfig();
|
|
109
|
+
if (!config.services || Object.keys(config.services).length === 0) {
|
|
110
|
+
error("No services defined in config");
|
|
111
|
+
process.exit(4);
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const started = await composeUp(config.services, {
|
|
115
|
+
only: args.only ? args.only.split(",") : void 0,
|
|
116
|
+
skip: args.skip ? args.skip.split(",") : void 0,
|
|
117
|
+
noHealth: args["no-health"]
|
|
118
|
+
});
|
|
119
|
+
success(`Started ${started.length} service(s): ${started.join(", ")}`);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
error(err instanceof Error ? err.message : String(err));
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
const down = defineCommand({
|
|
127
|
+
meta: {
|
|
128
|
+
name: "down",
|
|
129
|
+
description: "Stop all services"
|
|
130
|
+
},
|
|
131
|
+
args: {},
|
|
132
|
+
async run() {
|
|
133
|
+
const config = await loadProbeConfig();
|
|
134
|
+
if (!config.services) {
|
|
135
|
+
info("No services defined");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const stopped = await composeDown(config.services);
|
|
139
|
+
if (stopped.length === 0) info("No services were running");
|
|
140
|
+
else success(`Stopped ${stopped.length} service(s): ${stopped.join(", ")}`);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
const restart = defineCommand({
|
|
144
|
+
meta: {
|
|
145
|
+
name: "restart",
|
|
146
|
+
description: "Restart a service or all services"
|
|
147
|
+
},
|
|
148
|
+
args: { name: {
|
|
149
|
+
type: "positional",
|
|
150
|
+
description: "Service name (optional)",
|
|
151
|
+
required: false
|
|
152
|
+
} },
|
|
153
|
+
async run({ args }) {
|
|
154
|
+
const config = await loadProbeConfig();
|
|
155
|
+
if (!config.services) {
|
|
156
|
+
error("No services defined");
|
|
157
|
+
process.exit(4);
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
if (args.name) {
|
|
161
|
+
const svc = config.services[args.name];
|
|
162
|
+
if (!svc) {
|
|
163
|
+
error(`Unknown service: "${args.name}"`);
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
const { stopProcess: stopProcess$1, startProcess: startProcess$1 } = await import("./process-manager-zzltWvZ0.js");
|
|
167
|
+
try {
|
|
168
|
+
await stopProcess$1(args.name);
|
|
169
|
+
} catch {}
|
|
170
|
+
await startProcess$1({
|
|
171
|
+
name: args.name,
|
|
172
|
+
cmd: svc.cmd,
|
|
173
|
+
ready: svc.ready,
|
|
174
|
+
port: svc.port,
|
|
175
|
+
env: svc.env,
|
|
176
|
+
cwd: svc.cwd
|
|
177
|
+
});
|
|
178
|
+
success(`Restarted "${args.name}"`);
|
|
179
|
+
} else {
|
|
180
|
+
await composeDown(config.services);
|
|
181
|
+
const started = await composeUp(config.services, {});
|
|
182
|
+
success(`Restarted ${started.length} service(s)`);
|
|
183
|
+
}
|
|
184
|
+
} catch (err) {
|
|
185
|
+
error(err instanceof Error ? err.message : String(err));
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
const status = defineCommand({
|
|
191
|
+
meta: {
|
|
192
|
+
name: "status",
|
|
193
|
+
description: "Show status of all services"
|
|
194
|
+
},
|
|
195
|
+
args: { json: {
|
|
196
|
+
type: "boolean",
|
|
197
|
+
default: false
|
|
198
|
+
} },
|
|
199
|
+
async run({ args }) {
|
|
200
|
+
const processes = await listProcesses();
|
|
201
|
+
if (args.json) {
|
|
202
|
+
json(processes);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (processes.length === 0) {
|
|
206
|
+
info("No services running");
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
table(processes.map((p) => ({
|
|
210
|
+
name: p.name,
|
|
211
|
+
pid: p.pid,
|
|
212
|
+
port: p.port ?? "—",
|
|
213
|
+
status: p.status,
|
|
214
|
+
uptime: p.uptime
|
|
215
|
+
})));
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
const command = defineCommand({
|
|
219
|
+
meta: {
|
|
220
|
+
name: "compose",
|
|
221
|
+
description: "Manage service stack from config"
|
|
222
|
+
},
|
|
223
|
+
subCommands: {
|
|
224
|
+
up,
|
|
225
|
+
down,
|
|
226
|
+
restart,
|
|
227
|
+
status
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
var compose_default = command;
|
|
231
|
+
|
|
232
|
+
//#endregion
|
|
233
|
+
export { compose_default as default };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { parseDuration } from "./duration-DUrbfMLK.js";
|
|
2
|
+
import { loadConfig } from "c12";
|
|
3
|
+
import { defu } from "defu";
|
|
4
|
+
|
|
5
|
+
//#region src/core/config.ts
|
|
6
|
+
const defaults = {
|
|
7
|
+
browser: {
|
|
8
|
+
driver: "agent-browser",
|
|
9
|
+
headless: true,
|
|
10
|
+
session: "qprobe"
|
|
11
|
+
},
|
|
12
|
+
logs: {
|
|
13
|
+
dir: "tmp/qprobe/logs",
|
|
14
|
+
maxSize: "10mb",
|
|
15
|
+
browserConsole: true
|
|
16
|
+
},
|
|
17
|
+
tests: {
|
|
18
|
+
dir: "tests/qprobe",
|
|
19
|
+
timeout: 3e4
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
function defineConfig(config) {
|
|
23
|
+
return config;
|
|
24
|
+
}
|
|
25
|
+
let _config = null;
|
|
26
|
+
async function loadProbeConfig() {
|
|
27
|
+
if (_config) return _config;
|
|
28
|
+
try {
|
|
29
|
+
const configFile = process.env.QPROBE_CONFIG;
|
|
30
|
+
const { config } = await loadConfig({
|
|
31
|
+
name: "qprobe",
|
|
32
|
+
defaults,
|
|
33
|
+
...configFile ? { configFile } : {}
|
|
34
|
+
});
|
|
35
|
+
_config = defu(config ?? {}, defaults);
|
|
36
|
+
applyEnvOverrides(_config);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
39
|
+
throw new ConfigError(msg);
|
|
40
|
+
}
|
|
41
|
+
return _config;
|
|
42
|
+
}
|
|
43
|
+
var ConfigError = class extends Error {
|
|
44
|
+
name = "ConfigError";
|
|
45
|
+
constructor(message) {
|
|
46
|
+
super(message);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
function applyEnvOverrides(config) {
|
|
50
|
+
const baseUrl = process.env.QPROBE_BASE_URL;
|
|
51
|
+
if (baseUrl) {
|
|
52
|
+
config.http = config.http ?? {};
|
|
53
|
+
config.http.baseUrl = baseUrl;
|
|
54
|
+
config.browser = config.browser ?? {};
|
|
55
|
+
config.browser.baseUrl = baseUrl;
|
|
56
|
+
}
|
|
57
|
+
const browserDriver = process.env.QPROBE_BROWSER_DRIVER;
|
|
58
|
+
if (browserDriver === "agent-browser" || browserDriver === "playwright") {
|
|
59
|
+
config.browser = config.browser ?? {};
|
|
60
|
+
config.browser.driver = browserDriver;
|
|
61
|
+
}
|
|
62
|
+
const logDir = process.env.QPROBE_LOG_DIR;
|
|
63
|
+
if (logDir) {
|
|
64
|
+
config.logs = config.logs ?? {};
|
|
65
|
+
config.logs.dir = logDir;
|
|
66
|
+
}
|
|
67
|
+
const headless = process.env.QPROBE_HEADLESS;
|
|
68
|
+
if (headless === "true" || headless === "false") {
|
|
69
|
+
config.browser = config.browser ?? {};
|
|
70
|
+
config.browser.headless = headless === "true";
|
|
71
|
+
}
|
|
72
|
+
const timeout = process.env.QPROBE_TIMEOUT;
|
|
73
|
+
if (timeout) {
|
|
74
|
+
config.tests = config.tests ?? {};
|
|
75
|
+
config.tests.timeout = parseDuration(timeout);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function resolveBaseUrl(config) {
|
|
79
|
+
const envBaseUrl = process.env.QPROBE_BASE_URL;
|
|
80
|
+
if (envBaseUrl) return envBaseUrl;
|
|
81
|
+
if (config.http?.baseUrl) return config.http.baseUrl;
|
|
82
|
+
if (config.browser?.baseUrl) return config.browser.baseUrl;
|
|
83
|
+
const firstServiceWithPort = Object.values(config.services ?? {}).find((s) => s.port);
|
|
84
|
+
if (firstServiceWithPort?.port) return `http://localhost:${firstServiceWithPort.port}`;
|
|
85
|
+
return "http://localhost:3000";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
//#endregion
|
|
89
|
+
export { ConfigError, defineConfig, loadProbeConfig, resolveBaseUrl };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region src/utils/duration.ts
|
|
2
|
+
const UNITS = {
|
|
3
|
+
ms: 1,
|
|
4
|
+
s: 1e3,
|
|
5
|
+
m: 6e4,
|
|
6
|
+
h: 36e5,
|
|
7
|
+
d: 864e5
|
|
8
|
+
};
|
|
9
|
+
function parseDuration(input) {
|
|
10
|
+
const match = input.match(/^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/);
|
|
11
|
+
if (!match) throw new Error(`Invalid duration: "${input}"`);
|
|
12
|
+
const value = Number(match[1]);
|
|
13
|
+
const unit = UNITS[match[2]];
|
|
14
|
+
return Math.round(value * unit);
|
|
15
|
+
}
|
|
16
|
+
function formatDuration(ms) {
|
|
17
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
18
|
+
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
|
|
19
|
+
if (ms < 36e5) {
|
|
20
|
+
const m$1 = Math.floor(ms / 6e4);
|
|
21
|
+
const s = Math.floor(ms % 6e4 / 1e3);
|
|
22
|
+
return s > 0 ? `${m$1}m ${s}s` : `${m$1}m`;
|
|
23
|
+
}
|
|
24
|
+
const h = Math.floor(ms / 36e5);
|
|
25
|
+
const m = Math.floor(ms % 36e5 / 6e4);
|
|
26
|
+
return m > 0 ? `${h}h ${m}m` : `${h}h`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
30
|
+
export { formatDuration, parseDuration };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { parseDuration } from "./duration-DUrbfMLK.js";
|
|
2
|
+
import { error, success } from "./output-CHUjdVDf.js";
|
|
3
|
+
import { defineCommand } from "citty";
|
|
4
|
+
import { ofetch } from "ofetch";
|
|
5
|
+
|
|
6
|
+
//#region src/commands/health.ts
|
|
7
|
+
const command = defineCommand({
|
|
8
|
+
meta: {
|
|
9
|
+
name: "health",
|
|
10
|
+
description: "Poll a URL until it responds with expected status"
|
|
11
|
+
},
|
|
12
|
+
args: {
|
|
13
|
+
url: {
|
|
14
|
+
type: "positional",
|
|
15
|
+
description: "URL to check",
|
|
16
|
+
required: true
|
|
17
|
+
},
|
|
18
|
+
interval: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "Interval between attempts",
|
|
21
|
+
default: "1s"
|
|
22
|
+
},
|
|
23
|
+
timeout: {
|
|
24
|
+
type: "string",
|
|
25
|
+
description: "Max total wait time",
|
|
26
|
+
default: "30s"
|
|
27
|
+
},
|
|
28
|
+
status: {
|
|
29
|
+
type: "string",
|
|
30
|
+
description: "Expected status code",
|
|
31
|
+
default: "200"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
async run({ args }) {
|
|
35
|
+
const intervalMs = parseDuration(args.interval);
|
|
36
|
+
const timeoutMs = parseDuration(args.timeout);
|
|
37
|
+
const expectedStatus = Number(args.status);
|
|
38
|
+
const startTime = Date.now();
|
|
39
|
+
const deadline = startTime + timeoutMs;
|
|
40
|
+
while (Date.now() < deadline) {
|
|
41
|
+
try {
|
|
42
|
+
const reqStart = performance.now();
|
|
43
|
+
const response = await ofetch.raw(args.url, { ignoreResponseError: true });
|
|
44
|
+
const duration = Math.round(performance.now() - reqStart);
|
|
45
|
+
if (response.status === expectedStatus) {
|
|
46
|
+
const elapsed$1 = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
47
|
+
success(`${args.url} responding (${response.status} ${response.statusText}, ${duration}ms) after ${elapsed$1}s`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
} catch {}
|
|
51
|
+
if (Date.now() + intervalMs > deadline) break;
|
|
52
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
53
|
+
}
|
|
54
|
+
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
55
|
+
error(`${args.url} did not respond with ${expectedStatus} within ${elapsed}s`);
|
|
56
|
+
process.exit(3);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
var health_default = command;
|
|
60
|
+
|
|
61
|
+
//#endregion
|
|
62
|
+
export { health_default as default };
|