postgresai 0.14.0-beta.4 → 0.14.0-beta.5
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/README.md +31 -3
- package/bin/postgres-ai.ts +226 -16
- package/bunfig.toml +11 -3
- package/dist/bin/postgres-ai.js +726 -95
- package/dist/sql/01.role.sql +16 -0
- package/dist/sql/02.permissions.sql +37 -0
- package/dist/sql/03.optional_rds.sql +6 -0
- package/dist/sql/04.optional_self_managed.sql +8 -0
- package/dist/sql/05.helpers.sql +439 -0
- package/dist/sql/sql/01.role.sql +16 -0
- package/dist/sql/sql/02.permissions.sql +37 -0
- package/dist/sql/sql/03.optional_rds.sql +6 -0
- package/dist/sql/sql/04.optional_self_managed.sql +8 -0
- package/dist/sql/sql/05.helpers.sql +439 -0
- package/lib/checkup.ts +3 -0
- package/lib/config.ts +4 -4
- package/lib/init.ts +9 -3
- package/lib/issues.ts +318 -0
- package/lib/mcp-server.ts +207 -73
- package/lib/metrics-embedded.ts +2 -2
- package/package.json +4 -2
- package/sql/05.helpers.sql +31 -7
- package/test/auth.test.ts +258 -0
- package/test/checkup.integration.test.ts +46 -0
- package/test/checkup.test.ts +3 -2
- package/test/init.integration.test.ts +98 -0
- package/test/init.test.ts +72 -0
- package/test/issues.cli.test.ts +314 -0
- package/test/issues.test.ts +456 -0
- package/test/mcp-server.test.ts +988 -0
- package/test/schema-validation.test.ts +1 -1
package/README.md
CHANGED
|
@@ -303,17 +303,24 @@ Normalization:
|
|
|
303
303
|
|
|
304
304
|
### Examples
|
|
305
305
|
|
|
306
|
-
|
|
306
|
+
For production (uses default URLs):
|
|
307
307
|
|
|
308
308
|
```bash
|
|
309
|
+
# Production auth - uses console.postgres.ai by default
|
|
310
|
+
postgresai auth --debug
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
For staging/development environments:
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
# Linux/macOS (bash/zsh)
|
|
309
317
|
export PGAI_API_BASE_URL=https://v2.postgres.ai/api/general/
|
|
310
318
|
export PGAI_UI_BASE_URL=https://console-dev.postgres.ai
|
|
311
319
|
postgresai auth --debug
|
|
312
320
|
```
|
|
313
321
|
|
|
314
|
-
Windows PowerShell:
|
|
315
|
-
|
|
316
322
|
```powershell
|
|
323
|
+
# Windows PowerShell
|
|
317
324
|
$env:PGAI_API_BASE_URL = "https://v2.postgres.ai/api/general/"
|
|
318
325
|
$env:PGAI_UI_BASE_URL = "https://console-dev.postgres.ai"
|
|
319
326
|
postgresai auth --debug
|
|
@@ -330,6 +337,27 @@ postgresai auth --debug \
|
|
|
330
337
|
Notes:
|
|
331
338
|
- If `PGAI_UI_BASE_URL` is not set, the default is `https://console.postgres.ai`.
|
|
332
339
|
|
|
340
|
+
## Development
|
|
341
|
+
|
|
342
|
+
### Testing
|
|
343
|
+
|
|
344
|
+
The CLI uses [Bun](https://bun.sh/) as the test runner with built-in coverage reporting.
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
# Run tests with coverage (default)
|
|
348
|
+
bun run test
|
|
349
|
+
|
|
350
|
+
# Run tests without coverage (faster iteration during development)
|
|
351
|
+
bun run test:fast
|
|
352
|
+
|
|
353
|
+
# Run tests with coverage and show report location
|
|
354
|
+
bun run test:coverage
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Coverage configuration is in `bunfig.toml`. Reports are generated in `coverage/` directory:
|
|
358
|
+
- `coverage/lcov-report/index.html` - HTML coverage report
|
|
359
|
+
- `coverage/lcov.info` - LCOV format for CI integration
|
|
360
|
+
|
|
333
361
|
## Requirements
|
|
334
362
|
|
|
335
363
|
- Node.js 18 or higher
|
package/bin/postgres-ai.ts
CHANGED
|
@@ -10,7 +10,7 @@ import * as os from "os";
|
|
|
10
10
|
import * as crypto from "node:crypto";
|
|
11
11
|
import { Client } from "pg";
|
|
12
12
|
import { startMcpServer } from "../lib/mcp-server";
|
|
13
|
-
import { fetchIssues, fetchIssueComments, createIssueComment, fetchIssue } from "../lib/issues";
|
|
13
|
+
import { fetchIssues, fetchIssueComments, createIssueComment, fetchIssue, createIssue, updateIssue, updateIssueComment } from "../lib/issues";
|
|
14
14
|
import { resolveBaseUrls } from "../lib/util";
|
|
15
15
|
import { applyInitPlan, buildInitPlan, connectWithSslFallback, DEFAULT_MONITORING_USER, redactPasswordsInSql, resolveAdminConnection, resolveMonitoringPassword, verifyInitSetup } from "../lib/init";
|
|
16
16
|
import * as pkce from "../lib/pkce";
|
|
@@ -42,7 +42,7 @@ async function execPromise(command: string): Promise<{ stdout: string; stderr: s
|
|
|
42
42
|
childProcess.exec(command, (error, stdout, stderr) => {
|
|
43
43
|
if (error) {
|
|
44
44
|
const err = error as Error & { code: number };
|
|
45
|
-
err.code = error.code
|
|
45
|
+
err.code = typeof error.code === "number" ? error.code : 1;
|
|
46
46
|
reject(err);
|
|
47
47
|
} else {
|
|
48
48
|
resolve({ stdout, stderr });
|
|
@@ -56,7 +56,7 @@ async function execFilePromise(file: string, args: string[]): Promise<{ stdout:
|
|
|
56
56
|
childProcess.execFile(file, args, (error, stdout, stderr) => {
|
|
57
57
|
if (error) {
|
|
58
58
|
const err = error as Error & { code: number };
|
|
59
|
-
err.code = error.code
|
|
59
|
+
err.code = typeof error.code === "number" ? error.code : 1;
|
|
60
60
|
reject(err);
|
|
61
61
|
} else {
|
|
62
62
|
resolve({ stdout, stderr });
|
|
@@ -1174,6 +1174,11 @@ mon
|
|
|
1174
1174
|
.option("--tag <tag>", "Docker image tag to use (e.g., 0.14.0, 0.14.0-dev.33)")
|
|
1175
1175
|
.option("-y, --yes", "accept all defaults and skip interactive prompts", false)
|
|
1176
1176
|
.action(async (opts: { demo: boolean; apiKey?: string; dbUrl?: string; tag?: string; yes: boolean }) => {
|
|
1177
|
+
// Get apiKey from global program options (--api-key is defined globally)
|
|
1178
|
+
// This is needed because Commander.js routes --api-key to the global option, not the subcommand's option
|
|
1179
|
+
const globalOpts = program.opts<CliOptions>();
|
|
1180
|
+
const apiKey = opts.apiKey || globalOpts.apiKey;
|
|
1181
|
+
|
|
1177
1182
|
console.log("\n=================================");
|
|
1178
1183
|
console.log(" PostgresAI monitoring local install");
|
|
1179
1184
|
console.log("=================================\n");
|
|
@@ -1185,17 +1190,33 @@ mon
|
|
|
1185
1190
|
|
|
1186
1191
|
// Update .env with custom tag if provided
|
|
1187
1192
|
const envFile = path.resolve(projectDir, ".env");
|
|
1188
|
-
const imageTag = opts.tag || pkg.version;
|
|
1189
1193
|
|
|
1190
|
-
// Build .env content
|
|
1191
|
-
|
|
1192
|
-
|
|
1194
|
+
// Build .env content, preserving important existing values
|
|
1195
|
+
// Read existing .env first to preserve CI/custom settings
|
|
1196
|
+
let existingTag: string | null = null;
|
|
1197
|
+
let existingRegistry: string | null = null;
|
|
1198
|
+
let existingPassword: string | null = null;
|
|
1199
|
+
|
|
1193
1200
|
if (fs.existsSync(envFile)) {
|
|
1194
1201
|
const existingEnv = fs.readFileSync(envFile, "utf8");
|
|
1202
|
+
// Extract existing values
|
|
1203
|
+
const tagMatch = existingEnv.match(/^PGAI_TAG=(.+)$/m);
|
|
1204
|
+
if (tagMatch) existingTag = tagMatch[1].trim();
|
|
1205
|
+
const registryMatch = existingEnv.match(/^PGAI_REGISTRY=(.+)$/m);
|
|
1206
|
+
if (registryMatch) existingRegistry = registryMatch[1].trim();
|
|
1195
1207
|
const pwdMatch = existingEnv.match(/^GF_SECURITY_ADMIN_PASSWORD=(.+)$/m);
|
|
1196
|
-
if (pwdMatch)
|
|
1197
|
-
|
|
1198
|
-
|
|
1208
|
+
if (pwdMatch) existingPassword = pwdMatch[1].trim();
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// Priority: CLI --tag flag > existing .env > package version
|
|
1212
|
+
const imageTag = opts.tag || existingTag || pkg.version;
|
|
1213
|
+
|
|
1214
|
+
const envLines: string[] = [`PGAI_TAG=${imageTag}`];
|
|
1215
|
+
if (existingRegistry) {
|
|
1216
|
+
envLines.push(`PGAI_REGISTRY=${existingRegistry}`);
|
|
1217
|
+
}
|
|
1218
|
+
if (existingPassword) {
|
|
1219
|
+
envLines.push(`GF_SECURITY_ADMIN_PASSWORD=${existingPassword}`);
|
|
1199
1220
|
}
|
|
1200
1221
|
fs.writeFileSync(envFile, envLines.join("\n") + "\n", { encoding: "utf8", mode: 0o600 });
|
|
1201
1222
|
|
|
@@ -1210,7 +1231,7 @@ mon
|
|
|
1210
1231
|
opts.dbUrl = undefined;
|
|
1211
1232
|
}
|
|
1212
1233
|
|
|
1213
|
-
if (opts.demo &&
|
|
1234
|
+
if (opts.demo && apiKey) {
|
|
1214
1235
|
console.error("✗ Cannot use --api-key with --demo mode");
|
|
1215
1236
|
console.error("✗ Demo mode is for testing only and does not support API key integration");
|
|
1216
1237
|
console.error("\nUse demo mode without API key: postgres-ai mon local-install --demo");
|
|
@@ -1232,11 +1253,11 @@ mon
|
|
|
1232
1253
|
console.log("Step 1: Postgres AI API Configuration (Optional)");
|
|
1233
1254
|
console.log("An API key enables automatic upload of PostgreSQL reports to Postgres AI\n");
|
|
1234
1255
|
|
|
1235
|
-
if (
|
|
1256
|
+
if (apiKey) {
|
|
1236
1257
|
console.log("Using API key provided via --api-key parameter");
|
|
1237
|
-
config.writeConfig({ apiKey
|
|
1258
|
+
config.writeConfig({ apiKey });
|
|
1238
1259
|
// Keep reporter compatibility (docker-compose mounts .pgwatch-config)
|
|
1239
|
-
fs.writeFileSync(path.resolve(projectDir, ".pgwatch-config"), `api_key=${
|
|
1260
|
+
fs.writeFileSync(path.resolve(projectDir, ".pgwatch-config"), `api_key=${apiKey}\n`, {
|
|
1240
1261
|
encoding: "utf8",
|
|
1241
1262
|
mode: 0o600
|
|
1242
1263
|
});
|
|
@@ -2102,7 +2123,8 @@ auth
|
|
|
2102
2123
|
}
|
|
2103
2124
|
|
|
2104
2125
|
// Step 3: Open browser
|
|
2105
|
-
|
|
2126
|
+
// Pass api_url so UI calls oauth_approve on the same backend where oauth_init created the session
|
|
2127
|
+
const authUrl = `${uiBaseUrl}/cli/auth?state=${encodeURIComponent(params.state)}&code_challenge=${encodeURIComponent(params.codeChallenge)}&code_challenge_method=S256&redirect_uri=${encodeURIComponent(redirectUri)}&api_url=${encodeURIComponent(apiBaseUrl)}`;
|
|
2106
2128
|
|
|
2107
2129
|
if (opts.debug) {
|
|
2108
2130
|
console.log(`Debug: Auth URL: ${authUrl}`);
|
|
@@ -2491,7 +2513,7 @@ issues
|
|
|
2491
2513
|
});
|
|
2492
2514
|
|
|
2493
2515
|
issues
|
|
2494
|
-
.command("
|
|
2516
|
+
.command("post-comment <issueId> <content>")
|
|
2495
2517
|
.description("post a new comment to an issue")
|
|
2496
2518
|
.option("--parent <uuid>", "parent comment id")
|
|
2497
2519
|
.option("--debug", "enable debug output")
|
|
@@ -2536,6 +2558,194 @@ issues
|
|
|
2536
2558
|
}
|
|
2537
2559
|
});
|
|
2538
2560
|
|
|
2561
|
+
issues
|
|
2562
|
+
.command("create <title>")
|
|
2563
|
+
.description("create a new issue")
|
|
2564
|
+
.option("--org-id <id>", "organization id (defaults to config orgId)", (v) => parseInt(v, 10))
|
|
2565
|
+
.option("--project-id <id>", "project id", (v) => parseInt(v, 10))
|
|
2566
|
+
.option("--description <text>", "issue description (supports \\\\n)")
|
|
2567
|
+
.option(
|
|
2568
|
+
"--label <label>",
|
|
2569
|
+
"issue label (repeatable)",
|
|
2570
|
+
(value: string, previous: string[]) => {
|
|
2571
|
+
previous.push(value);
|
|
2572
|
+
return previous;
|
|
2573
|
+
},
|
|
2574
|
+
[] as string[]
|
|
2575
|
+
)
|
|
2576
|
+
.option("--debug", "enable debug output")
|
|
2577
|
+
.option("--json", "output raw JSON")
|
|
2578
|
+
.action(async (rawTitle: string, opts: { orgId?: number; projectId?: number; description?: string; label?: string[]; debug?: boolean; json?: boolean }) => {
|
|
2579
|
+
try {
|
|
2580
|
+
const rootOpts = program.opts<CliOptions>();
|
|
2581
|
+
const cfg = config.readConfig();
|
|
2582
|
+
const { apiKey } = getConfig(rootOpts);
|
|
2583
|
+
if (!apiKey) {
|
|
2584
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
2585
|
+
process.exitCode = 1;
|
|
2586
|
+
return;
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
const title = interpretEscapes(String(rawTitle || "").trim());
|
|
2590
|
+
if (!title) {
|
|
2591
|
+
console.error("title is required");
|
|
2592
|
+
process.exitCode = 1;
|
|
2593
|
+
return;
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
const orgId = typeof opts.orgId === "number" && !Number.isNaN(opts.orgId) ? opts.orgId : cfg.orgId;
|
|
2597
|
+
if (typeof orgId !== "number") {
|
|
2598
|
+
console.error("org_id is required. Either pass --org-id or run 'pgai auth' to store it in config.");
|
|
2599
|
+
process.exitCode = 1;
|
|
2600
|
+
return;
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
const description = opts.description !== undefined ? interpretEscapes(String(opts.description)) : undefined;
|
|
2604
|
+
const labels = Array.isArray(opts.label) && opts.label.length > 0 ? opts.label.map(String) : undefined;
|
|
2605
|
+
const projectId = typeof opts.projectId === "number" && !Number.isNaN(opts.projectId) ? opts.projectId : undefined;
|
|
2606
|
+
|
|
2607
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
2608
|
+
const result = await createIssue({
|
|
2609
|
+
apiKey,
|
|
2610
|
+
apiBaseUrl,
|
|
2611
|
+
title,
|
|
2612
|
+
orgId,
|
|
2613
|
+
description,
|
|
2614
|
+
projectId,
|
|
2615
|
+
labels,
|
|
2616
|
+
debug: !!opts.debug,
|
|
2617
|
+
});
|
|
2618
|
+
printResult(result, opts.json);
|
|
2619
|
+
} catch (err) {
|
|
2620
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2621
|
+
console.error(message);
|
|
2622
|
+
process.exitCode = 1;
|
|
2623
|
+
}
|
|
2624
|
+
});
|
|
2625
|
+
|
|
2626
|
+
issues
|
|
2627
|
+
.command("update <issueId>")
|
|
2628
|
+
.description("update an existing issue (title/description/status/labels)")
|
|
2629
|
+
.option("--title <text>", "new title (supports \\\\n)")
|
|
2630
|
+
.option("--description <text>", "new description (supports \\\\n)")
|
|
2631
|
+
.option("--status <value>", "status: open|closed|0|1")
|
|
2632
|
+
.option(
|
|
2633
|
+
"--label <label>",
|
|
2634
|
+
"set labels (repeatable). If provided, replaces existing labels.",
|
|
2635
|
+
(value: string, previous: string[]) => {
|
|
2636
|
+
previous.push(value);
|
|
2637
|
+
return previous;
|
|
2638
|
+
},
|
|
2639
|
+
[] as string[]
|
|
2640
|
+
)
|
|
2641
|
+
.option("--clear-labels", "set labels to an empty list")
|
|
2642
|
+
.option("--debug", "enable debug output")
|
|
2643
|
+
.option("--json", "output raw JSON")
|
|
2644
|
+
.action(async (issueId: string, opts: { title?: string; description?: string; status?: string; label?: string[]; clearLabels?: boolean; debug?: boolean; json?: boolean }) => {
|
|
2645
|
+
try {
|
|
2646
|
+
const rootOpts = program.opts<CliOptions>();
|
|
2647
|
+
const cfg = config.readConfig();
|
|
2648
|
+
const { apiKey } = getConfig(rootOpts);
|
|
2649
|
+
if (!apiKey) {
|
|
2650
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
2651
|
+
process.exitCode = 1;
|
|
2652
|
+
return;
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
2656
|
+
|
|
2657
|
+
const title = opts.title !== undefined ? interpretEscapes(String(opts.title)) : undefined;
|
|
2658
|
+
const description = opts.description !== undefined ? interpretEscapes(String(opts.description)) : undefined;
|
|
2659
|
+
|
|
2660
|
+
let status: number | undefined = undefined;
|
|
2661
|
+
if (opts.status !== undefined) {
|
|
2662
|
+
const raw = String(opts.status).trim().toLowerCase();
|
|
2663
|
+
if (raw === "open") status = 0;
|
|
2664
|
+
else if (raw === "closed") status = 1;
|
|
2665
|
+
else {
|
|
2666
|
+
const n = Number(raw);
|
|
2667
|
+
if (!Number.isFinite(n)) {
|
|
2668
|
+
console.error("status must be open|closed|0|1");
|
|
2669
|
+
process.exitCode = 1;
|
|
2670
|
+
return;
|
|
2671
|
+
}
|
|
2672
|
+
status = n;
|
|
2673
|
+
}
|
|
2674
|
+
if (status !== 0 && status !== 1) {
|
|
2675
|
+
console.error("status must be 0 (open) or 1 (closed)");
|
|
2676
|
+
process.exitCode = 1;
|
|
2677
|
+
return;
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
let labels: string[] | undefined = undefined;
|
|
2682
|
+
if (opts.clearLabels) {
|
|
2683
|
+
labels = [];
|
|
2684
|
+
} else if (Array.isArray(opts.label) && opts.label.length > 0) {
|
|
2685
|
+
labels = opts.label.map(String);
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
const result = await updateIssue({
|
|
2689
|
+
apiKey,
|
|
2690
|
+
apiBaseUrl,
|
|
2691
|
+
issueId,
|
|
2692
|
+
title,
|
|
2693
|
+
description,
|
|
2694
|
+
status,
|
|
2695
|
+
labels,
|
|
2696
|
+
debug: !!opts.debug,
|
|
2697
|
+
});
|
|
2698
|
+
printResult(result, opts.json);
|
|
2699
|
+
} catch (err) {
|
|
2700
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2701
|
+
console.error(message);
|
|
2702
|
+
process.exitCode = 1;
|
|
2703
|
+
}
|
|
2704
|
+
});
|
|
2705
|
+
|
|
2706
|
+
issues
|
|
2707
|
+
.command("update-comment <commentId> <content>")
|
|
2708
|
+
.description("update an existing issue comment")
|
|
2709
|
+
.option("--debug", "enable debug output")
|
|
2710
|
+
.option("--json", "output raw JSON")
|
|
2711
|
+
.action(async (commentId: string, content: string, opts: { debug?: boolean; json?: boolean }) => {
|
|
2712
|
+
try {
|
|
2713
|
+
if (opts.debug) {
|
|
2714
|
+
// eslint-disable-next-line no-console
|
|
2715
|
+
console.log(`Debug: Original content: ${JSON.stringify(content)}`);
|
|
2716
|
+
}
|
|
2717
|
+
content = interpretEscapes(content);
|
|
2718
|
+
if (opts.debug) {
|
|
2719
|
+
// eslint-disable-next-line no-console
|
|
2720
|
+
console.log(`Debug: Interpreted content: ${JSON.stringify(content)}`);
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
const rootOpts = program.opts<CliOptions>();
|
|
2724
|
+
const cfg = config.readConfig();
|
|
2725
|
+
const { apiKey } = getConfig(rootOpts);
|
|
2726
|
+
if (!apiKey) {
|
|
2727
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
2728
|
+
process.exitCode = 1;
|
|
2729
|
+
return;
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
2733
|
+
|
|
2734
|
+
const result = await updateIssueComment({
|
|
2735
|
+
apiKey,
|
|
2736
|
+
apiBaseUrl,
|
|
2737
|
+
commentId,
|
|
2738
|
+
content,
|
|
2739
|
+
debug: !!opts.debug,
|
|
2740
|
+
});
|
|
2741
|
+
printResult(result, opts.json);
|
|
2742
|
+
} catch (err) {
|
|
2743
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2744
|
+
console.error(message);
|
|
2745
|
+
process.exitCode = 1;
|
|
2746
|
+
}
|
|
2747
|
+
});
|
|
2748
|
+
|
|
2539
2749
|
// MCP server
|
|
2540
2750
|
const mcp = program.command("mcp").description("MCP server integration");
|
|
2541
2751
|
|
package/bunfig.toml
CHANGED
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
# Integration tests that connect to databases need longer timeouts
|
|
7
7
|
timeout = 30000
|
|
8
8
|
|
|
9
|
-
# Coverage settings
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
# Coverage settings - enabled by default for test runs
|
|
10
|
+
coverage = true
|
|
11
|
+
coverageDir = "coverage"
|
|
12
|
+
|
|
13
|
+
# Skip coverage for test files and node_modules
|
|
14
|
+
coverageSkipTestFiles = true
|
|
15
|
+
|
|
16
|
+
# Reporter format for CI integration
|
|
17
|
+
# - text: console output with summary table
|
|
18
|
+
# - lcov: standard format for coverage tools
|
|
19
|
+
coverageReporter = ["text", "lcov"]
|