@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,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 };
@@ -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,4 @@
1
+ import "./duration-DUrbfMLK.js";
2
+ import { defineConfig } from "./config-BUEMgFYN.js";
3
+
4
+ export { defineConfig };
@@ -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 };