@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,187 @@
|
|
|
1
|
+
import "./duration-DUrbfMLK.js";
|
|
2
|
+
import { ConfigError, loadProbeConfig, resolveBaseUrl } from "./config-BUEMgFYN.js";
|
|
3
|
+
import { error, info, log, success } from "./output-CHUjdVDf.js";
|
|
4
|
+
import { defineCommand } from "citty";
|
|
5
|
+
import { ofetch } from "ofetch";
|
|
6
|
+
|
|
7
|
+
//#region src/core/http-client.ts
|
|
8
|
+
async function makeRequest(opts, config) {
|
|
9
|
+
const base = opts.base ?? resolveBaseUrl(config);
|
|
10
|
+
const url = opts.path.startsWith("http") ? opts.path : `${base}${opts.path}`;
|
|
11
|
+
const headers = {
|
|
12
|
+
...config.http?.headers,
|
|
13
|
+
...opts.headers
|
|
14
|
+
};
|
|
15
|
+
if (opts.token) headers["Authorization"] = `Bearer ${opts.token}`;
|
|
16
|
+
let body;
|
|
17
|
+
if (opts.data) {
|
|
18
|
+
body = JSON.parse(opts.data);
|
|
19
|
+
if (!headers["Content-Type"]) headers["Content-Type"] = "application/json";
|
|
20
|
+
}
|
|
21
|
+
const start = performance.now();
|
|
22
|
+
const response = await ofetch.raw(url, {
|
|
23
|
+
method: opts.method,
|
|
24
|
+
headers,
|
|
25
|
+
body,
|
|
26
|
+
ignoreResponseError: true
|
|
27
|
+
});
|
|
28
|
+
const duration = Math.round(performance.now() - start);
|
|
29
|
+
const responseHeaders = {};
|
|
30
|
+
response.headers.forEach((value, key) => {
|
|
31
|
+
responseHeaders[key] = value;
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
status: response.status,
|
|
35
|
+
statusText: response.statusText,
|
|
36
|
+
headers: responseHeaders,
|
|
37
|
+
body: response._data,
|
|
38
|
+
duration
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/commands/http.ts
|
|
44
|
+
const command = defineCommand({
|
|
45
|
+
meta: {
|
|
46
|
+
name: "http",
|
|
47
|
+
description: "Send HTTP requests against a running server"
|
|
48
|
+
},
|
|
49
|
+
args: {
|
|
50
|
+
method: {
|
|
51
|
+
type: "positional",
|
|
52
|
+
description: "HTTP method (GET, POST, PUT, DELETE, PATCH)",
|
|
53
|
+
required: true
|
|
54
|
+
},
|
|
55
|
+
path: {
|
|
56
|
+
type: "positional",
|
|
57
|
+
description: "URL path (e.g. /api/users)",
|
|
58
|
+
required: true
|
|
59
|
+
},
|
|
60
|
+
data: {
|
|
61
|
+
type: "string",
|
|
62
|
+
alias: "d",
|
|
63
|
+
description: "Request body (JSON)"
|
|
64
|
+
},
|
|
65
|
+
header: {
|
|
66
|
+
type: "string",
|
|
67
|
+
alias: "H",
|
|
68
|
+
description: "Header as key:value"
|
|
69
|
+
},
|
|
70
|
+
token: {
|
|
71
|
+
type: "string",
|
|
72
|
+
description: "Bearer token"
|
|
73
|
+
},
|
|
74
|
+
status: {
|
|
75
|
+
type: "string",
|
|
76
|
+
description: "Assert expected status code"
|
|
77
|
+
},
|
|
78
|
+
jq: {
|
|
79
|
+
type: "string",
|
|
80
|
+
description: "JQ-style filter on response"
|
|
81
|
+
},
|
|
82
|
+
raw: {
|
|
83
|
+
type: "boolean",
|
|
84
|
+
description: "Raw output (no pretty-print)",
|
|
85
|
+
default: false
|
|
86
|
+
},
|
|
87
|
+
verbose: {
|
|
88
|
+
type: "boolean",
|
|
89
|
+
alias: "v",
|
|
90
|
+
description: "Show request and response headers",
|
|
91
|
+
default: false
|
|
92
|
+
},
|
|
93
|
+
base: {
|
|
94
|
+
type: "string",
|
|
95
|
+
description: "Base URL override"
|
|
96
|
+
},
|
|
97
|
+
timing: {
|
|
98
|
+
type: "boolean",
|
|
99
|
+
description: "Show request timing breakdown",
|
|
100
|
+
default: false
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
async run({ args }) {
|
|
104
|
+
const config = await loadProbeConfig();
|
|
105
|
+
const headers = {};
|
|
106
|
+
if (args.header) {
|
|
107
|
+
const parts = args.header.split(",");
|
|
108
|
+
for (const part of parts) {
|
|
109
|
+
const idx = part.indexOf(":");
|
|
110
|
+
if (idx > 0) headers[part.slice(0, idx).trim()] = part.slice(idx + 1).trim();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
if (args.verbose) {
|
|
115
|
+
const base = args.base ?? config.http?.baseUrl ?? "http://localhost:3000";
|
|
116
|
+
const url = args.path.startsWith("http") ? args.path : `${base}${args.path}`;
|
|
117
|
+
log(`\u2192 ${args.method} ${url}`);
|
|
118
|
+
for (const [k, v] of Object.entries(headers)) log(`\u2192 ${k}: ${v}`);
|
|
119
|
+
if (args.data) log(`\u2192 Body: ${args.data}`);
|
|
120
|
+
}
|
|
121
|
+
const result = await makeRequest({
|
|
122
|
+
method: args.method,
|
|
123
|
+
path: args.path,
|
|
124
|
+
base: args.base,
|
|
125
|
+
data: args.data,
|
|
126
|
+
headers: Object.keys(headers).length > 0 ? headers : void 0,
|
|
127
|
+
token: args.token,
|
|
128
|
+
verbose: args.verbose,
|
|
129
|
+
raw: args.raw
|
|
130
|
+
}, config);
|
|
131
|
+
if (args.verbose) {
|
|
132
|
+
log(`\u2190 ${result.status} ${result.statusText} (${result.duration}ms)`);
|
|
133
|
+
for (const [k, v] of Object.entries(result.headers)) log(`\u2190 ${k}: ${v}`);
|
|
134
|
+
}
|
|
135
|
+
if (args.timing) info(`Total: ${result.duration}ms`);
|
|
136
|
+
if (args.status) {
|
|
137
|
+
const expected = Number(args.status);
|
|
138
|
+
if (result.status !== expected) {
|
|
139
|
+
error(`Expected ${expected}, got ${result.status} ${result.statusText} (${result.duration}ms)`);
|
|
140
|
+
if (result.body) {
|
|
141
|
+
info("Response body:");
|
|
142
|
+
log(args.raw ? String(result.body) : JSON.stringify(result.body, null, 2));
|
|
143
|
+
}
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
success(`${result.status} ${result.statusText} (${result.duration}ms)`);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (!args.verbose) info(`${result.status} ${result.statusText} (${result.duration}ms)`);
|
|
150
|
+
if (result.body !== void 0 && result.body !== null) {
|
|
151
|
+
let output = result.body;
|
|
152
|
+
if (args.jq) output = applyJqFilter(result.body, args.jq);
|
|
153
|
+
if (args.raw) log(typeof output === "string" ? output : JSON.stringify(output));
|
|
154
|
+
else log(JSON.stringify(output, null, 2));
|
|
155
|
+
}
|
|
156
|
+
} catch (err) {
|
|
157
|
+
error(err instanceof Error ? err.message : String(err));
|
|
158
|
+
process.exit(err instanceof ConfigError ? 4 : 1);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
var http_default = command;
|
|
163
|
+
function applyJqFilter(data, expr) {
|
|
164
|
+
const parts = expr.split(".").filter(Boolean);
|
|
165
|
+
let current = data;
|
|
166
|
+
for (const part of parts) {
|
|
167
|
+
const arrayMatch = part.match(/^\[(\d+)]$/);
|
|
168
|
+
if (arrayMatch && Array.isArray(current)) current = current[Number(arrayMatch[1])];
|
|
169
|
+
else if (current && typeof current === "object" && !Array.isArray(current)) {
|
|
170
|
+
const bracketMatch = part.match(/^(.+?)\[(\d+)]$/);
|
|
171
|
+
if (bracketMatch) {
|
|
172
|
+
const obj = current;
|
|
173
|
+
const arr = obj[bracketMatch[1]];
|
|
174
|
+
if (Array.isArray(arr)) current = arr[Number(bracketMatch[2])];
|
|
175
|
+
else current = void 0;
|
|
176
|
+
} else current = current[part];
|
|
177
|
+
} else if (Array.isArray(current)) current = current.map((item) => {
|
|
178
|
+
if (item && typeof item === "object") return item[part];
|
|
179
|
+
return void 0;
|
|
180
|
+
});
|
|
181
|
+
else current = void 0;
|
|
182
|
+
}
|
|
183
|
+
return current;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
//#endregion
|
|
187
|
+
export { http_default as default };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
//#region src/core/config.d.ts
|
|
2
|
+
interface ServiceConfig {
|
|
3
|
+
cmd: string;
|
|
4
|
+
ready?: string;
|
|
5
|
+
port?: number;
|
|
6
|
+
health?: string;
|
|
7
|
+
depends?: string[];
|
|
8
|
+
env?: Record<string, string>;
|
|
9
|
+
cwd?: string;
|
|
10
|
+
stop?: string;
|
|
11
|
+
timeout?: number;
|
|
12
|
+
}
|
|
13
|
+
interface BrowserConfig {
|
|
14
|
+
driver?: "agent-browser" | "playwright";
|
|
15
|
+
baseUrl?: string;
|
|
16
|
+
headless?: boolean;
|
|
17
|
+
session?: string;
|
|
18
|
+
}
|
|
19
|
+
interface HttpConfig {
|
|
20
|
+
baseUrl?: string;
|
|
21
|
+
headers?: Record<string, string>;
|
|
22
|
+
}
|
|
23
|
+
interface ProbeConfig {
|
|
24
|
+
services?: Record<string, ServiceConfig>;
|
|
25
|
+
browser?: BrowserConfig;
|
|
26
|
+
http?: HttpConfig;
|
|
27
|
+
logs?: {
|
|
28
|
+
dir?: string;
|
|
29
|
+
maxSize?: string;
|
|
30
|
+
browserConsole?: boolean;
|
|
31
|
+
};
|
|
32
|
+
tests?: {
|
|
33
|
+
dir?: string;
|
|
34
|
+
timeout?: number;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
declare function defineConfig(config: ProbeConfig): ProbeConfig; //#endregion
|
|
38
|
+
//#region src/browser/types.d.ts
|
|
39
|
+
interface SnapshotOpts {
|
|
40
|
+
interactive?: boolean;
|
|
41
|
+
compact?: boolean;
|
|
42
|
+
depth?: number;
|
|
43
|
+
selector?: string;
|
|
44
|
+
diff?: boolean;
|
|
45
|
+
}
|
|
46
|
+
interface ScreenshotOpts {
|
|
47
|
+
path?: string;
|
|
48
|
+
annotate?: boolean;
|
|
49
|
+
full?: boolean;
|
|
50
|
+
selector?: string;
|
|
51
|
+
}
|
|
52
|
+
interface ConsoleEntry {
|
|
53
|
+
level: "log" | "warn" | "error" | "info";
|
|
54
|
+
text: string;
|
|
55
|
+
timestamp?: string;
|
|
56
|
+
source?: string;
|
|
57
|
+
}
|
|
58
|
+
interface ErrorEntry {
|
|
59
|
+
message: string;
|
|
60
|
+
stack?: string;
|
|
61
|
+
timestamp?: string;
|
|
62
|
+
}
|
|
63
|
+
interface NetworkEntry {
|
|
64
|
+
method: string;
|
|
65
|
+
url: string;
|
|
66
|
+
status: number;
|
|
67
|
+
duration: number;
|
|
68
|
+
timestamp?: string;
|
|
69
|
+
}
|
|
70
|
+
interface ConsoleOpts {
|
|
71
|
+
level?: "log" | "warn" | "error" | "info";
|
|
72
|
+
clear?: boolean;
|
|
73
|
+
json?: boolean;
|
|
74
|
+
}
|
|
75
|
+
interface NetworkOpts {
|
|
76
|
+
failed?: boolean;
|
|
77
|
+
method?: string;
|
|
78
|
+
grep?: string;
|
|
79
|
+
json?: boolean;
|
|
80
|
+
}
|
|
81
|
+
interface WaitOpts {
|
|
82
|
+
ref?: string;
|
|
83
|
+
selector?: string;
|
|
84
|
+
url?: string;
|
|
85
|
+
text?: string;
|
|
86
|
+
network?: "idle";
|
|
87
|
+
hidden?: boolean;
|
|
88
|
+
timeout?: number;
|
|
89
|
+
}
|
|
90
|
+
interface BrowserDriver {
|
|
91
|
+
open(url: string): Promise<void>;
|
|
92
|
+
back(): Promise<void>;
|
|
93
|
+
forward(): Promise<void>;
|
|
94
|
+
reload(): Promise<void>;
|
|
95
|
+
url(): Promise<string>;
|
|
96
|
+
title(): Promise<string>;
|
|
97
|
+
close(): Promise<void>;
|
|
98
|
+
snapshot(opts?: SnapshotOpts): Promise<string>;
|
|
99
|
+
click(ref: string): Promise<void>;
|
|
100
|
+
dblclick(ref: string): Promise<void>;
|
|
101
|
+
fill(ref: string, value: string): Promise<void>;
|
|
102
|
+
select(ref: string, value: string): Promise<void>;
|
|
103
|
+
check(ref: string): Promise<void>;
|
|
104
|
+
uncheck(ref: string): Promise<void>;
|
|
105
|
+
press(key: string): Promise<void>;
|
|
106
|
+
type(text: string): Promise<void>;
|
|
107
|
+
hover(ref: string): Promise<void>;
|
|
108
|
+
focus(ref: string): Promise<void>;
|
|
109
|
+
scroll(direction: string, px?: number): Promise<void>;
|
|
110
|
+
upload(ref: string, file: string): Promise<void>;
|
|
111
|
+
screenshot(opts?: ScreenshotOpts): Promise<string>;
|
|
112
|
+
eval(js: string): Promise<string>;
|
|
113
|
+
text(selector?: string): Promise<string>;
|
|
114
|
+
console(opts?: ConsoleOpts): Promise<ConsoleEntry[]>;
|
|
115
|
+
errors(): Promise<ErrorEntry[]>;
|
|
116
|
+
network(opts?: NetworkOpts): Promise<NetworkEntry[]>;
|
|
117
|
+
wait(opts: WaitOpts): Promise<void>;
|
|
118
|
+
} //#endregion
|
|
119
|
+
export { BrowserConfig, BrowserDriver, ConsoleEntry, HttpConfig, NetworkEntry, ProbeConfig, ServiceConfig, SnapshotOpts, defineConfig };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { error, info, success } from "./output-CHUjdVDf.js";
|
|
2
|
+
import { defineCommand } from "citty";
|
|
3
|
+
import { access, mkdir, writeFile } from "node:fs/promises";
|
|
4
|
+
|
|
5
|
+
//#region src/commands/init.ts
|
|
6
|
+
const CONFIG_TEMPLATE = `import { defineConfig } from '@questpie/probe'
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
services: {
|
|
10
|
+
// db: {
|
|
11
|
+
// cmd: 'docker compose up postgres',
|
|
12
|
+
// ready: 'ready to accept connections',
|
|
13
|
+
// health: 'http://localhost:5432',
|
|
14
|
+
// stop: 'docker compose down postgres',
|
|
15
|
+
// },
|
|
16
|
+
// server: {
|
|
17
|
+
// cmd: 'bun dev',
|
|
18
|
+
// ready: 'ready on http://localhost:3000',
|
|
19
|
+
// port: 3000,
|
|
20
|
+
// health: '/api/health',
|
|
21
|
+
// depends: ['db'],
|
|
22
|
+
// env: {
|
|
23
|
+
// DATABASE_URL: 'postgresql://postgres:postgres@localhost:5432/dev',
|
|
24
|
+
// },
|
|
25
|
+
// },
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
browser: {
|
|
29
|
+
driver: 'agent-browser',
|
|
30
|
+
baseUrl: 'http://localhost:3000',
|
|
31
|
+
headless: true,
|
|
32
|
+
session: 'qprobe',
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
http: {
|
|
36
|
+
baseUrl: 'http://localhost:3000',
|
|
37
|
+
headers: {
|
|
38
|
+
'Content-Type': 'application/json',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
logs: {
|
|
43
|
+
dir: 'tmp/qprobe/logs',
|
|
44
|
+
maxSize: '10mb',
|
|
45
|
+
browserConsole: true,
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
tests: {
|
|
49
|
+
dir: 'tests/qprobe',
|
|
50
|
+
timeout: 30_000,
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
`;
|
|
54
|
+
const GITIGNORE_ADDITION = "\n# QUESTPIE Probe\ntmp/qprobe/\n";
|
|
55
|
+
const command = defineCommand({
|
|
56
|
+
meta: {
|
|
57
|
+
name: "init",
|
|
58
|
+
description: "Initialize QUESTPIE Probe config in current project"
|
|
59
|
+
},
|
|
60
|
+
args: { force: {
|
|
61
|
+
type: "boolean",
|
|
62
|
+
description: "Overwrite existing config",
|
|
63
|
+
default: false
|
|
64
|
+
} },
|
|
65
|
+
async run({ args }) {
|
|
66
|
+
const configPath = "qprobe.config.ts";
|
|
67
|
+
if (!args.force) try {
|
|
68
|
+
await access(configPath);
|
|
69
|
+
error(`${configPath} already exists. Use --force to overwrite.`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
} catch {}
|
|
72
|
+
await writeFile(configPath, CONFIG_TEMPLATE, "utf-8");
|
|
73
|
+
success(`Created ${configPath}`);
|
|
74
|
+
await mkdir("tmp/qprobe/logs", { recursive: true });
|
|
75
|
+
await mkdir("tmp/qprobe/pids", { recursive: true });
|
|
76
|
+
await mkdir("tests/qprobe/recordings", { recursive: true });
|
|
77
|
+
info("Created tmp/qprobe/ and tests/qprobe/ directories");
|
|
78
|
+
try {
|
|
79
|
+
const { readFile: readFile$1 } = await import("node:fs/promises");
|
|
80
|
+
const gitignore = await readFile$1(".gitignore", "utf-8");
|
|
81
|
+
if (!gitignore.includes("tmp/qprobe")) {
|
|
82
|
+
await writeFile(".gitignore", gitignore + GITIGNORE_ADDITION, "utf-8");
|
|
83
|
+
info("Added tmp/qprobe/ to .gitignore");
|
|
84
|
+
}
|
|
85
|
+
} catch {}
|
|
86
|
+
info("Run \"qprobe compose up\" to start your stack");
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
var init_default = command;
|
|
90
|
+
|
|
91
|
+
//#endregion
|
|
92
|
+
export { init_default as default };
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { parseDuration } from "./duration-DUrbfMLK.js";
|
|
2
|
+
import { error, info, json, log } from "./output-CHUjdVDf.js";
|
|
3
|
+
import { getLogPath, listProcessNames } from "./state-DRTSIt_r.js";
|
|
4
|
+
import { defineCommand } from "citty";
|
|
5
|
+
import { readFile, stat } from "node:fs/promises";
|
|
6
|
+
import { consola } from "consola";
|
|
7
|
+
import { watch } from "chokidar";
|
|
8
|
+
|
|
9
|
+
//#region src/core/log-reader.ts
|
|
10
|
+
function parseLine(line, source) {
|
|
11
|
+
const match = line.match(/^(\d{4}-\d{2}-\d{2}T[\d:.]+Z)\s+(INFO|ERROR|WARN|DEBUG)\s+(.*)$/);
|
|
12
|
+
if (!match) return null;
|
|
13
|
+
return {
|
|
14
|
+
timestamp: match[1],
|
|
15
|
+
level: match[2],
|
|
16
|
+
message: match[3],
|
|
17
|
+
source
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function matchesFilter(entry, filter) {
|
|
21
|
+
if (filter.level && entry.level.toLowerCase() !== filter.level.toLowerCase()) return false;
|
|
22
|
+
if (filter.grep) {
|
|
23
|
+
const regex = new RegExp(filter.grep, "i");
|
|
24
|
+
if (!regex.test(entry.message)) return false;
|
|
25
|
+
}
|
|
26
|
+
if (filter.since) {
|
|
27
|
+
const entryTime = new Date(entry.timestamp).getTime();
|
|
28
|
+
const cutoff = Date.now() - filter.since;
|
|
29
|
+
if (entryTime < cutoff) return false;
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
async function readLogs(name, filter = {}) {
|
|
34
|
+
const logPath = getLogPath(name);
|
|
35
|
+
let content;
|
|
36
|
+
try {
|
|
37
|
+
content = await readFile(logPath, "utf-8");
|
|
38
|
+
} catch {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
const lines = content.trim().split("\n");
|
|
42
|
+
const entries = [];
|
|
43
|
+
for (const line of lines) {
|
|
44
|
+
const entry = parseLine(line, name);
|
|
45
|
+
if (entry && matchesFilter(entry, filter)) entries.push(entry);
|
|
46
|
+
}
|
|
47
|
+
const limit = filter.lines ?? 50;
|
|
48
|
+
return entries.slice(-limit);
|
|
49
|
+
}
|
|
50
|
+
async function readAllLogs(filter = {}) {
|
|
51
|
+
const names = await listProcessNames();
|
|
52
|
+
const allEntries = [];
|
|
53
|
+
for (const name of names) {
|
|
54
|
+
const entries = await readLogs(name, {
|
|
55
|
+
...filter,
|
|
56
|
+
lines: void 0
|
|
57
|
+
});
|
|
58
|
+
allEntries.push(...entries);
|
|
59
|
+
}
|
|
60
|
+
allEntries.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
61
|
+
const limit = filter.lines ?? 50;
|
|
62
|
+
return allEntries.slice(-limit);
|
|
63
|
+
}
|
|
64
|
+
async function followLogs(name, filter, signal) {
|
|
65
|
+
const logPath = getLogPath(name);
|
|
66
|
+
let fileSize = 0;
|
|
67
|
+
try {
|
|
68
|
+
const s = await stat(logPath);
|
|
69
|
+
fileSize = s.size;
|
|
70
|
+
} catch {}
|
|
71
|
+
const existing = await readLogs(name, filter);
|
|
72
|
+
for (const entry of existing) printEntry(entry);
|
|
73
|
+
const watcher = watch(logPath, { persistent: true });
|
|
74
|
+
const onChange = async () => {
|
|
75
|
+
try {
|
|
76
|
+
const content = await readFile(logPath, "utf-8");
|
|
77
|
+
const newContent = content.slice(fileSize);
|
|
78
|
+
fileSize = content.length;
|
|
79
|
+
const lines = newContent.trim().split("\n");
|
|
80
|
+
for (const line of lines) {
|
|
81
|
+
const entry = parseLine(line, name);
|
|
82
|
+
if (entry && matchesFilter(entry, filter)) printEntry(entry);
|
|
83
|
+
}
|
|
84
|
+
} catch {}
|
|
85
|
+
};
|
|
86
|
+
watcher.on("change", onChange);
|
|
87
|
+
watcher.on("add", onChange);
|
|
88
|
+
signal.addEventListener("abort", () => {
|
|
89
|
+
watcher.close();
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function printEntry(entry) {
|
|
93
|
+
const prefix = entry.source ? `[${entry.source}] ` : "";
|
|
94
|
+
const levelColor = entry.level === "ERROR" ? "\x1B[31m" : entry.level === "WARN" ? "\x1B[33m" : "\x1B[0m";
|
|
95
|
+
consola.log(`${prefix}${entry.timestamp} ${levelColor}${entry.level}\x1b[0m ${entry.message}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/commands/logs.ts
|
|
100
|
+
const command = defineCommand({
|
|
101
|
+
meta: {
|
|
102
|
+
name: "logs",
|
|
103
|
+
description: "Read process logs with filtering"
|
|
104
|
+
},
|
|
105
|
+
args: {
|
|
106
|
+
name: {
|
|
107
|
+
type: "positional",
|
|
108
|
+
description: "Process name",
|
|
109
|
+
required: false
|
|
110
|
+
},
|
|
111
|
+
follow: {
|
|
112
|
+
type: "boolean",
|
|
113
|
+
alias: "f",
|
|
114
|
+
description: "Follow mode (tail -f)",
|
|
115
|
+
default: false
|
|
116
|
+
},
|
|
117
|
+
lines: {
|
|
118
|
+
type: "string",
|
|
119
|
+
alias: "n",
|
|
120
|
+
description: "Number of lines",
|
|
121
|
+
default: "50"
|
|
122
|
+
},
|
|
123
|
+
grep: {
|
|
124
|
+
type: "string",
|
|
125
|
+
description: "Filter pattern (regex)"
|
|
126
|
+
},
|
|
127
|
+
level: {
|
|
128
|
+
type: "string",
|
|
129
|
+
description: "Log level filter (error, warn, info, debug)"
|
|
130
|
+
},
|
|
131
|
+
since: {
|
|
132
|
+
type: "string",
|
|
133
|
+
description: "Time filter (e.g. 5m, 1h)"
|
|
134
|
+
},
|
|
135
|
+
all: {
|
|
136
|
+
type: "boolean",
|
|
137
|
+
description: "All processes merged",
|
|
138
|
+
default: false
|
|
139
|
+
},
|
|
140
|
+
unified: {
|
|
141
|
+
type: "boolean",
|
|
142
|
+
description: "All processes + browser unified",
|
|
143
|
+
default: false
|
|
144
|
+
},
|
|
145
|
+
json: {
|
|
146
|
+
type: "boolean",
|
|
147
|
+
description: "JSON output",
|
|
148
|
+
default: false
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
async run({ args }) {
|
|
152
|
+
const filter = {
|
|
153
|
+
grep: args.grep,
|
|
154
|
+
level: args.level,
|
|
155
|
+
lines: Number(args.lines),
|
|
156
|
+
since: args.since ? parseDuration(args.since) : void 0,
|
|
157
|
+
json: args.json
|
|
158
|
+
};
|
|
159
|
+
if (args.all || args.unified) {
|
|
160
|
+
const entries$1 = await readAllLogs(filter);
|
|
161
|
+
if (entries$1.length === 0) {
|
|
162
|
+
info("No log entries found");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (args.json) json(entries$1);
|
|
166
|
+
else for (const entry of entries$1) log(`[${entry.source}] ${entry.timestamp} ${entry.level} ${entry.message}`);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (!args.name) {
|
|
170
|
+
error("Provide a process name, or use --all");
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
if (args.follow) {
|
|
174
|
+
const ac = new AbortController();
|
|
175
|
+
process.on("SIGINT", () => ac.abort());
|
|
176
|
+
await followLogs(args.name, filter, ac.signal);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const entries = await readLogs(args.name, filter);
|
|
180
|
+
if (entries.length === 0) {
|
|
181
|
+
info(`No log entries for "${args.name}"`);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (args.json) json(entries);
|
|
185
|
+
else for (const entry of entries) log(`${entry.timestamp} ${entry.level} ${entry.message}`);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
var logs_default = command;
|
|
189
|
+
|
|
190
|
+
//#endregion
|
|
191
|
+
export { logs_default as default };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { consola } from "consola";
|
|
2
|
+
|
|
3
|
+
//#region src/utils/output.ts
|
|
4
|
+
function success(msg) {
|
|
5
|
+
consola.success(msg);
|
|
6
|
+
}
|
|
7
|
+
function error(msg) {
|
|
8
|
+
consola.error(msg);
|
|
9
|
+
}
|
|
10
|
+
function warn(msg) {
|
|
11
|
+
consola.warn(msg);
|
|
12
|
+
}
|
|
13
|
+
function info(msg) {
|
|
14
|
+
consola.info(msg);
|
|
15
|
+
}
|
|
16
|
+
function log(msg) {
|
|
17
|
+
consola.log(msg);
|
|
18
|
+
}
|
|
19
|
+
function table(rows) {
|
|
20
|
+
if (rows.length === 0) return;
|
|
21
|
+
const keys = Object.keys(rows[0]);
|
|
22
|
+
const widths = keys.map((k) => {
|
|
23
|
+
const vals = rows.map((r) => String(r[k] ?? ""));
|
|
24
|
+
return Math.max(k.length, ...vals.map((v) => v.length));
|
|
25
|
+
});
|
|
26
|
+
const header = keys.map((k, i) => k.toUpperCase().padEnd(widths[i])).join(" ");
|
|
27
|
+
consola.log(header);
|
|
28
|
+
for (const row of rows) {
|
|
29
|
+
const line = keys.map((k, i) => String(row[k] ?? "").padEnd(widths[i])).join(" ");
|
|
30
|
+
consola.log(line);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function json(data) {
|
|
34
|
+
consola.log(JSON.stringify(data, null, 2));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
//#endregion
|
|
38
|
+
export { error, info, json, log, success, table, warn };
|