dripfeed 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dripfeed contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,355 @@
1
+ # dripfeed
2
+
3
+ Soak test your API. One request every few seconds, for hours. Logs every failure.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/dripfeed)](https://www.npmjs.com/package/dripfeed)
6
+ [![npm downloads](https://img.shields.io/npm/dm/dripfeed)](https://www.npmjs.com/package/dripfeed)
7
+ [![CI](https://img.shields.io/github/actions/workflow/status/ph33nx/dripfeed/ci.yml?branch=main&label=CI)](https://github.com/ph33nx/dripfeed/actions/workflows/ci.yml)
8
+ [![license](https://img.shields.io/npm/l/dripfeed)](https://github.com/ph33nx/dripfeed/blob/main/LICENSE)
9
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org)
10
+ [![Bun](https://img.shields.io/badge/bun-%3E%3D1.0-black)](https://bun.sh)
11
+
12
+ **dripfeed** hits your API endpoints at regular intervals (every 1 to 30 seconds) and logs every response to a local SQLite database. Run it for hours or days to catch intermittent failures, latency degradation, and silent outages that load tests and uptime pings miss. This is soak testing: sustained, low-volume traffic over long periods to surface problems that only appear under real-world conditions.
13
+
14
+ ## When to use dripfeed
15
+
16
+ | Scenario | Tool |
17
+ |----------|------|
18
+ | "Can my server handle 10,000 concurrent users?" | k6, artillery, vegeta |
19
+ | "Is my API up right now?" | Uptime Kuma, Better Stack, Pingdom |
20
+ | "Did my API silently degrade overnight?" | **dripfeed** |
21
+ | "Does my API return errors under sustained real-world usage?" | **dripfeed** |
22
+ | "I need a queryable history of every API response for the last 24 hours" | **dripfeed** |
23
+
24
+ ## Why dripfeed?
25
+
26
+ - **Zero infrastructure.** No Docker, no Grafana, no InfluxDB. One CLI command, one SQLite file
27
+ - **SQLite-first.** Every request logged to a queryable database. `SELECT * FROM results WHERE status >= 500`
28
+ - **Multi-endpoint rotation.** Weighted random or round-robin across your full API surface
29
+ - **POST bodies + headers.** Not just GET pings. Test real API payloads with auth tokens
30
+ - **Error body capture.** Logs the full response body on non-2xx so you know *why* it failed
31
+ - **CI/CD ready.** Threshold assertions with non-zero exit codes. Fail the pipeline if p95 > 500ms
32
+ - **Runtime-agnostic.** Works on Node.js (20+), Bun, and Deno. Bun gets zero-dep SQLite via `bun:sqlite`
33
+
34
+ ## Quick Start
35
+
36
+ ```bash
37
+ # Generate a starter config
38
+ npx dripfeed init
39
+
40
+ # Edit dripfeed.config.ts with your endpoints
41
+
42
+ # Run indefinitely (Ctrl+C to stop)
43
+ npx dripfeed run
44
+
45
+ # Run for a fixed duration
46
+ npx dripfeed run --duration 2h
47
+
48
+ # Run in CI with thresholds
49
+ npx dripfeed run --duration 10m --quiet
50
+ ```
51
+
52
+ ## Install
53
+
54
+ ```bash
55
+ # Global
56
+ npm install -g dripfeed
57
+
58
+ # Project dependency
59
+ npm install dripfeed
60
+
61
+ # Or with other package managers
62
+ pnpm add dripfeed
63
+ yarn add dripfeed
64
+ bun add dripfeed
65
+ ```
66
+
67
+ ## Configuration
68
+
69
+ Create a `dripfeed.config.ts` (or `.json`, `.yaml`, `.toml`):
70
+
71
+ ```typescript
72
+ import type { DripfeedConfig } from 'dripfeed';
73
+
74
+ const config: DripfeedConfig = {
75
+ interval: '3s',
76
+ timeout: '30s',
77
+ storage: 'sqlite',
78
+ rotation: 'weighted-random',
79
+ headers: {
80
+ Authorization: 'Bearer ${API_TOKEN}',
81
+ },
82
+ endpoints: [
83
+ {
84
+ name: 'get-users',
85
+ url: 'https://api.example.com/v1/users',
86
+ weight: 2,
87
+ },
88
+ {
89
+ name: 'create-order',
90
+ url: 'https://api.example.com/v1/orders',
91
+ method: 'POST',
92
+ headers: { 'Content-Type': 'application/json' },
93
+ body: { product_id: 'sku-123', quantity: 1 },
94
+ weight: 1,
95
+ },
96
+ ],
97
+ thresholds: {
98
+ error_rate: '< 1%',
99
+ p95: '< 500ms',
100
+ },
101
+ };
102
+
103
+ export default config;
104
+ ```
105
+
106
+ Environment variables are interpolated via `${VAR}` syntax from `process.env`.
107
+
108
+ ## CLI
109
+
110
+ ```
111
+ Usage: dripfeed <command> [options]
112
+
113
+ Commands:
114
+ run Start a soak test
115
+ init Generate a starter config file
116
+ report Generate a report from an existing SQLite database
117
+ export Export results to CSV or JSON
118
+
119
+ Run options:
120
+ --duration, -d Run duration (e.g. 30s, 10m, 2h). Omit for indefinite
121
+ --interval, -i Override config interval
122
+ --db SQLite database path (default: dripfeed-results.db)
123
+ --report, -r Report format: console, json, markdown
124
+ --output, -o Report output file path
125
+ --quiet, -q Suppress live console output
126
+ ```
127
+
128
+ > **Note:** Using `--report json` or `--report markdown` automatically suppresses live console output so the report output is clean and parseable.
129
+
130
+ ## Live Output
131
+
132
+ ```
133
+ dripfeed v0.1.0 — every 3s | Ctrl+C to stop
134
+
135
+ ✓ #1 get-users 200 142ms | ok:1 fail:0 (100.0%)
136
+ ✓ #2 create-order 201 387ms | ok:2 fail:0 (100.0%)
137
+ ✗ #3 get-users 500 891ms | ok:2 fail:1 (66.7%) | Internal Server Error
138
+ ✓ #4 create-order 201 245ms | ok:3 fail:1 (75.0%)
139
+ ```
140
+
141
+ On Ctrl+C, prints a summary with per-endpoint latency percentiles, error counts, and threshold pass/fail results.
142
+
143
+ ## Library API
144
+
145
+ Use dripfeed programmatically in any Node.js/Bun application:
146
+
147
+ ```typescript
148
+ import { createSoakTest, parseConfig, createConsoleReporter } from 'dripfeed';
149
+
150
+ // parseConfig validates and applies defaults to a raw config object
151
+ const config = parseConfig({
152
+ interval: '3s',
153
+ endpoints: [
154
+ { name: 'health', url: 'https://api.example.com/health' },
155
+ ],
156
+ });
157
+
158
+ const test = createSoakTest(config, [createConsoleReporter()]);
159
+
160
+ // Run for a fixed duration
161
+ const stats = await test.run({ duration: '10m' });
162
+ console.log(`Uptime: ${stats.uptime_pct}%`);
163
+
164
+ // Or start/stop manually
165
+ await test.start();
166
+ // ... later
167
+ const stats = await test.stop();
168
+ ```
169
+
170
+ ### Express.js
171
+
172
+ ```typescript
173
+ import express from 'express';
174
+ import { createSoakTest, createMemoryStorage } from 'dripfeed';
175
+
176
+ const app = express();
177
+
178
+ // Start soak test alongside your server
179
+ const test = createSoakTest({
180
+ interval: '10s',
181
+ storage: 'memory',
182
+ endpoints: [
183
+ { name: 'self-health', url: 'http://localhost:3000/health' },
184
+ ],
185
+ }, []);
186
+
187
+ test.start();
188
+
189
+ app.get('/health', (req, res) => res.json({ ok: true }));
190
+ app.get('/soak-status', async (req, res) => {
191
+ const stats = await test.stop();
192
+ res.json(stats);
193
+ });
194
+ ```
195
+
196
+ ### Next.js (API Route)
197
+
198
+ ```typescript
199
+ // app/api/soak/route.ts
200
+ import { createSoakTest } from 'dripfeed';
201
+
202
+ export async function POST() {
203
+ const test = createSoakTest({
204
+ interval: '1s',
205
+ storage: 'memory',
206
+ endpoints: [
207
+ { name: 'api', url: 'https://api.example.com/health' },
208
+ ],
209
+ }, []);
210
+
211
+ const stats = await test.run({ duration: '30s' });
212
+ return Response.json(stats);
213
+ }
214
+ ```
215
+
216
+ ### Hono
217
+
218
+ ```typescript
219
+ import { Hono } from 'hono';
220
+ import { createSoakTest } from 'dripfeed';
221
+
222
+ const app = new Hono();
223
+
224
+ app.post('/soak', async (c) => {
225
+ const test = createSoakTest({
226
+ interval: '2s',
227
+ storage: 'memory',
228
+ endpoints: [
229
+ { name: 'health', url: 'https://api.example.com/health' },
230
+ ],
231
+ }, []);
232
+
233
+ const stats = await test.run({ duration: '1m' });
234
+ return c.json(stats);
235
+ });
236
+ ```
237
+
238
+ ## Query Results
239
+
240
+ The SQLite database is the primary artifact. Query it after a run:
241
+
242
+ ```bash
243
+ # Error count by endpoint
244
+ sqlite3 dripfeed-results.db "
245
+ SELECT endpoint, COUNT(*) as errors
246
+ FROM results WHERE status >= 400 OR status IS NULL
247
+ GROUP BY endpoint ORDER BY errors DESC"
248
+
249
+ # Latency over time (1-minute buckets)
250
+ sqlite3 dripfeed-results.db "
251
+ SELECT strftime('%H:%M', timestamp) as minute,
252
+ ROUND(AVG(duration_ms)) as avg_ms, MAX(duration_ms) as max_ms
253
+ FROM results GROUP BY minute ORDER BY minute"
254
+
255
+ # All error response bodies
256
+ sqlite3 dripfeed-results.db "
257
+ SELECT timestamp, endpoint, status, response_body
258
+ FROM results WHERE status >= 400 OR status IS NULL"
259
+ ```
260
+
261
+ ## Reports
262
+
263
+ ```bash
264
+ # Generate from existing database
265
+ npx dripfeed report --db dripfeed-results.db --format json --output report.json
266
+ npx dripfeed report --format markdown --output report.md
267
+
268
+ # Export raw data
269
+ npx dripfeed export --format csv --output results.csv
270
+ ```
271
+
272
+ ## CI/CD Integration
273
+
274
+ ### GitHub Actions
275
+
276
+ ```yaml
277
+ - name: Soak test staging
278
+ run: npx dripfeed run --duration 10m --quiet
279
+ env:
280
+ API_TOKEN: ${{ secrets.API_TOKEN }}
281
+ ```
282
+
283
+ Threshold failures produce a non-zero exit code, failing the pipeline automatically.
284
+
285
+ ## Thresholds
286
+
287
+ Define pass/fail criteria in your config:
288
+
289
+ ```typescript
290
+ thresholds: {
291
+ error_rate: '< 1%', // fail if error rate exceeds 1%
292
+ p95: '< 500ms', // fail if p95 latency exceeds 500ms
293
+ p99: '< 2000ms', // fail if p99 exceeds 2 seconds
294
+ }
295
+ ```
296
+
297
+ ## Storage Adapters
298
+
299
+ | Adapter | Runtime | Dependencies | When to use |
300
+ |---------|---------|-------------|-------------|
301
+ | **SQLite** | Bun | Zero (`bun:sqlite` built-in) | Default on Bun |
302
+ | **SQLite** | Node.js | `better-sqlite3` (optional peer dep) | Default on Node |
303
+ | **JSON** | Any | Zero | Fallback if no SQLite available |
304
+ | **Memory** | Any | Zero | Tests and programmatic use |
305
+
306
+ Storage is auto-detected based on your runtime. Override with `storage: 'json'` in config.
307
+
308
+ ## Configuration Reference
309
+
310
+ | Option | Type | Default | Description |
311
+ |--------|------|---------|-------------|
312
+ | `interval` | `string` | `"3s"` | Time between requests (`500ms`, `1s`, `3s`, `5s`, `30s`, `1m`) |
313
+ | `timeout` | `string` | `"30s"` | Request timeout |
314
+ | `storage` | `string` | `"sqlite"` | Storage adapter: `sqlite`, `json`, `memory` |
315
+ | `db` | `string` | `"dripfeed-results.db"` | SQLite database path |
316
+ | `rotation` | `string` | `"weighted-random"` | Endpoint selection: `weighted-random`, `round-robin`, `sequential` |
317
+ | `headers` | `object` | `{}` | Global headers for all requests |
318
+ | `endpoints` | `array` | required | Endpoint definitions (see below) |
319
+ | `thresholds` | `object` | none | Pass/fail criteria |
320
+
321
+ ### Endpoint options
322
+
323
+ | Option | Type | Default | Description |
324
+ |--------|------|---------|-------------|
325
+ | `name` | `string` | required | Human-readable label |
326
+ | `url` | `string` | required | Full URL |
327
+ | `method` | `string` | `"GET"` | HTTP method |
328
+ | `headers` | `object` | `{}` | Per-endpoint headers (merged with global) |
329
+ | `body` | `any` | none | JSON request body |
330
+ | `timeout` | `string` | global timeout | Per-endpoint timeout override |
331
+ | `weight` | `number` | `1` | Selection probability (higher = more frequent) |
332
+
333
+ ## Good to Know
334
+
335
+ - **SQLite database location:** Created in the current working directory (default: `dripfeed-results.db`). Override with `db` config option or `--db` flag.
336
+ - **Multiple runs append:** Subsequent runs append to the same SQLite file. Delete the `.db` file between runs for fresh results, or use a unique `--db` path per run.
337
+ - **Minimum interval:** 100ms enforced floor to prevent accidental DoS. The tool is designed for 1-60 second intervals.
338
+ - **Serverless:** Use `storage: 'memory'` in serverless environments (Vercel, Lambda) where the filesystem is ephemeral. Pass a short `duration` to stay within function timeout limits.
339
+ - **HTML/PDF reports:** Not yet supported. Use the HTML print-to-PDF workflow: generate markdown, render in a browser, print to PDF.
340
+ - **Library API:** Use `parseConfig()` to validate raw config objects before passing to `createSoakTest()`. This applies Zod defaults (`interval`, `timeout`, `rotation`, etc.) that the runner requires.
341
+
342
+ ## Contributing
343
+
344
+ ```bash
345
+ git clone https://github.com/ph33nx/dripfeed.git
346
+ cd dripfeed
347
+ bun install
348
+ bun run test # run tests
349
+ bun typecheck # type check
350
+ bun run check # lint + format
351
+ ```
352
+
353
+ ## License
354
+
355
+ MIT
package/dist/cli.cjs ADDED
@@ -0,0 +1,270 @@
1
+ #!/usr/bin/env node
2
+ const require_runner = require("./runner-Dc1JRBps.cjs");
3
+ let citty = require("citty");
4
+ //#region src/cli.ts
5
+ const VALID_REPORT_FORMATS = [
6
+ "console",
7
+ "json",
8
+ "markdown"
9
+ ];
10
+ const VALID_EXPORT_FORMATS = ["csv", "json"];
11
+ const validateFormat = (format, valid, command) => {
12
+ if (!valid.includes(format)) {
13
+ process.stderr.write(`Unsupported format "${format}" for ${command}. Use: ${valid.join(", ")}\n`);
14
+ process.exit(1);
15
+ }
16
+ };
17
+ (0, citty.runMain)((0, citty.defineCommand)({
18
+ meta: {
19
+ name: "dripfeed",
20
+ version: "0.1.0",
21
+ description: "SQLite-native API soak testing. Drip, not firehose."
22
+ },
23
+ subCommands: {
24
+ run: (0, citty.defineCommand)({
25
+ meta: {
26
+ name: "run",
27
+ description: "Start a soak test"
28
+ },
29
+ args: {
30
+ duration: {
31
+ type: "string",
32
+ alias: "d",
33
+ description: "Test duration (e.g. \"30s\", \"5m\", \"2h\")"
34
+ },
35
+ interval: {
36
+ type: "string",
37
+ alias: "i",
38
+ description: "Request interval (e.g. \"3s\", \"500ms\")"
39
+ },
40
+ db: {
41
+ type: "string",
42
+ description: "SQLite database path"
43
+ },
44
+ report: {
45
+ type: "string",
46
+ alias: "r",
47
+ description: "Report format: console, json, markdown",
48
+ default: "console"
49
+ },
50
+ output: {
51
+ type: "string",
52
+ alias: "o",
53
+ description: "Report output file path"
54
+ },
55
+ quiet: {
56
+ type: "boolean",
57
+ alias: "q",
58
+ description: "Suppress live output",
59
+ default: false
60
+ }
61
+ },
62
+ run: async ({ args }) => {
63
+ const reportFormat = args.report ?? "console";
64
+ validateFormat(reportFormat, VALID_REPORT_FORMATS, "report");
65
+ const overrides = {};
66
+ if (args.duration) overrides.duration = args.duration;
67
+ if (args.interval) overrides.interval = args.interval;
68
+ if (args.db) overrides.db = args.db;
69
+ let config;
70
+ try {
71
+ config = await require_runner.loadDripfeedConfig(overrides);
72
+ } catch (err) {
73
+ if (err && typeof err === "object" && "issues" in err) {
74
+ process.stderr.write("Invalid config. Run `dripfeed init` to create a starter config.\n");
75
+ const { issues } = err;
76
+ process.stderr.write(`Details: ${JSON.stringify(issues, null, 2)}\n`);
77
+ } else process.stderr.write(`Config error: ${err instanceof Error ? err.message : err}\n`);
78
+ process.exit(1);
79
+ }
80
+ const reporters = [];
81
+ const shouldQuiet = args.quiet || reportFormat !== "console";
82
+ if (!shouldQuiet) reporters.push(require_runner.createConsoleReporter());
83
+ if (reportFormat === "json") reporters.push(require_runner.createJsonReporter(args.output));
84
+ else if (reportFormat === "markdown") reporters.push(require_runner.createMarkdownReporter(args.output));
85
+ if (!shouldQuiet) {
86
+ const interval = config.interval ?? "3s";
87
+ const duration = args.duration ? ` for ${args.duration}` : "";
88
+ process.stdout.write(`\ndripfeed v0.1.0 — every ${interval}${duration} | Ctrl+C to stop\n\n`);
89
+ }
90
+ if ((await require_runner.createSoakTest(config, reporters).run({ duration: args.duration })).thresholds?.some((t) => !t.passed)) process.exit(1);
91
+ }
92
+ }),
93
+ init: (0, citty.defineCommand)({
94
+ meta: {
95
+ name: "init",
96
+ description: "Generate a starter dripfeed config file"
97
+ },
98
+ args: { format: {
99
+ type: "string",
100
+ description: "Config format: ts, json",
101
+ default: "ts"
102
+ } },
103
+ run: async ({ args }) => {
104
+ const { writeFile, access } = await import("node:fs/promises");
105
+ const format = args.format ?? "ts";
106
+ if (format !== "ts" && format !== "json") {
107
+ process.stderr.write(`Unsupported format "${format}". Use: ts, json\n`);
108
+ process.exit(1);
109
+ }
110
+ const filename = format === "ts" ? "dripfeed.config.ts" : "dripfeed.config.json";
111
+ try {
112
+ await access(filename);
113
+ process.stderr.write(`${filename} already exists. Delete it first or use a different format.\n`);
114
+ process.exit(1);
115
+ } catch {}
116
+ if (format === "ts") await writeFile(filename, `import type { DripfeedConfig } from 'dripfeed';
117
+
118
+ const config: DripfeedConfig = {
119
+ \tinterval: '3s',
120
+ \ttimeout: '30s',
121
+ \tstorage: 'sqlite',
122
+ \trotation: 'weighted-random',
123
+ \tendpoints: [
124
+ \t\t{
125
+ \t\t\tname: 'health',
126
+ \t\t\turl: 'https://api.example.com/health',
127
+ \t\t},
128
+ \t\t{
129
+ \t\t\tname: 'users',
130
+ \t\t\turl: 'https://api.example.com/v1/users',
131
+ \t\t\tweight: 3,
132
+ \t\t},
133
+ \t],
134
+ \tthresholds: {
135
+ \t\terror_rate: '< 1%',
136
+ \t\tp95: '< 500ms',
137
+ \t},
138
+ };
139
+
140
+ export default config;
141
+ `);
142
+ else await writeFile(filename, JSON.stringify({
143
+ interval: "3s",
144
+ timeout: "30s",
145
+ storage: "sqlite",
146
+ rotation: "weighted-random",
147
+ endpoints: [{
148
+ name: "health",
149
+ url: "https://api.example.com/health"
150
+ }, {
151
+ name: "users",
152
+ url: "https://api.example.com/v1/users",
153
+ weight: 3
154
+ }],
155
+ thresholds: {
156
+ error_rate: "< 1%",
157
+ p95: "< 500ms"
158
+ }
159
+ }, null, 2));
160
+ process.stdout.write(`Created ${filename}\n`);
161
+ }
162
+ }),
163
+ report: (0, citty.defineCommand)({
164
+ meta: {
165
+ name: "report",
166
+ description: "Generate a report from an existing SQLite database"
167
+ },
168
+ args: {
169
+ db: {
170
+ type: "string",
171
+ description: "SQLite database path",
172
+ default: "dripfeed-results.db"
173
+ },
174
+ format: {
175
+ type: "string",
176
+ description: "Report format: console, json, markdown",
177
+ default: "console"
178
+ },
179
+ output: {
180
+ type: "string",
181
+ alias: "o",
182
+ description: "Output file path"
183
+ }
184
+ },
185
+ run: async ({ args }) => {
186
+ const format = args.format ?? "console";
187
+ validateFormat(format, VALID_REPORT_FORMATS, "report");
188
+ const storage = require_runner.createSqliteStorage(args.db ?? "dripfeed-results.db");
189
+ await storage.init();
190
+ const results = await storage.getAll();
191
+ await storage.close();
192
+ if (results.length === 0) {
193
+ process.stdout.write("No results found in database.\n");
194
+ return;
195
+ }
196
+ const stats = require_runner.computeStats(results, new Date(results[0]?.timestamp ?? Date.now()), void 0, new Date(results[results.length - 1]?.timestamp ?? Date.now()));
197
+ if (format === "console") require_runner.createConsoleReporter().onComplete(stats);
198
+ else if (format === "json") require_runner.createJsonReporter(args.output).onComplete(stats);
199
+ else if (format === "markdown") require_runner.createMarkdownReporter(args.output).onComplete(stats);
200
+ if (args.output) process.stdout.write(`Report written to ${args.output}\n`);
201
+ }
202
+ }),
203
+ export: (0, citty.defineCommand)({
204
+ meta: {
205
+ name: "export",
206
+ description: "Export results from SQLite to CSV or JSON"
207
+ },
208
+ args: {
209
+ db: {
210
+ type: "string",
211
+ description: "SQLite database path",
212
+ default: "dripfeed-results.db"
213
+ },
214
+ format: {
215
+ type: "string",
216
+ description: "Export format: csv, json",
217
+ default: "csv"
218
+ },
219
+ output: {
220
+ type: "string",
221
+ alias: "o",
222
+ description: "Output file path"
223
+ }
224
+ },
225
+ run: async ({ args }) => {
226
+ const format = args.format ?? "csv";
227
+ validateFormat(format, VALID_EXPORT_FORMATS, "export");
228
+ const { writeFile } = await import("node:fs/promises");
229
+ const storage = require_runner.createSqliteStorage(args.db ?? "dripfeed-results.db");
230
+ await storage.init();
231
+ const results = await storage.getAll();
232
+ await storage.close();
233
+ let output;
234
+ if (format === "json") output = JSON.stringify(results, null, 2);
235
+ else {
236
+ const headers = [
237
+ "timestamp",
238
+ "endpoint",
239
+ "method",
240
+ "url",
241
+ "status",
242
+ "duration_ms",
243
+ "error",
244
+ "response_body"
245
+ ];
246
+ const escapeCsv = (s) => {
247
+ if (s === null) return "";
248
+ return s.includes(",") || s.includes("\"") || s.includes("\n") ? `"${s.replace(/"/g, "\"\"")}"` : s;
249
+ };
250
+ const rows = results.map((r) => [
251
+ r.timestamp,
252
+ r.endpoint,
253
+ r.method,
254
+ r.url,
255
+ r.status ?? "",
256
+ r.duration_ms,
257
+ escapeCsv(r.error),
258
+ escapeCsv(r.response_body)
259
+ ].join(","));
260
+ output = [headers.join(","), ...rows].join("\n");
261
+ }
262
+ if (args.output) {
263
+ await writeFile(args.output, output);
264
+ process.stdout.write(`Exported ${results.length} results to ${args.output}\n`);
265
+ } else process.stdout.write(`${output}\n`);
266
+ }
267
+ })
268
+ }
269
+ }));
270
+ //#endregion
package/dist/cli.d.cts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/cli.d.mts ADDED
@@ -0,0 +1 @@
1
+ export { };