v11y-check 0.0.0 → 0.7.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 (42) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +148 -1
  3. package/bin/v11y.js +11 -0
  4. package/dist/esm/.cache +1 -0
  5. package/dist/esm/bin/route.d.ts +1 -0
  6. package/dist/esm/bin/route.js +170 -0
  7. package/dist/esm/bin/v11y.d.ts +1 -0
  8. package/dist/esm/bin/v11y.js +12 -0
  9. package/dist/esm/cli.d.ts +5 -0
  10. package/dist/esm/cli.js +231 -0
  11. package/dist/esm/config.d.ts +10 -0
  12. package/dist/esm/config.js +41 -0
  13. package/dist/esm/format-actions.d.ts +3 -0
  14. package/dist/esm/format-actions.js +34 -0
  15. package/dist/esm/format-check.d.ts +38 -0
  16. package/dist/esm/format-check.js +134 -0
  17. package/dist/esm/format-summary.d.ts +8 -0
  18. package/dist/esm/format-summary.js +12 -0
  19. package/dist/esm/format-time.d.ts +1 -0
  20. package/dist/esm/format-time.js +36 -0
  21. package/dist/esm/format-verbose.d.ts +3 -0
  22. package/dist/esm/format-verbose.js +144 -0
  23. package/dist/esm/generate.d.ts +9 -0
  24. package/dist/esm/generate.js +28 -0
  25. package/dist/esm/index.d.ts +5 -0
  26. package/dist/esm/index.js +20 -0
  27. package/dist/esm/init/initCommand.d.ts +6 -0
  28. package/dist/esm/init/initCommand.js +34 -0
  29. package/dist/esm/init/scaffold.d.ts +13 -0
  30. package/dist/esm/init/scaffold.js +45 -0
  31. package/dist/esm/init/templates.d.ts +1 -0
  32. package/dist/esm/init/templates.js +23 -0
  33. package/dist/esm/run-audit.d.ts +34 -0
  34. package/dist/esm/run-audit.js +166 -0
  35. package/dist/esm/sync.d.ts +20 -0
  36. package/dist/esm/sync.js +95 -0
  37. package/dist/esm/tmp.d.ts +1 -0
  38. package/dist/esm/tmp.js +14 -0
  39. package/dist/esm/types.d.ts +98 -0
  40. package/dist/esm/types.js +40 -0
  41. package/package.json +52 -2
  42. package/templates/audit.yaml.template +27 -0
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 William Thorsen
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
6
+
7
+ The software is provided "as is" and the author disclaims all warranties with regard to this software including all implied warranties of merchantability and fitness. In no event shall the author be liable for any special, direct, indirect, or consequential damages or any damages whatsoever resulting from loss of use, data or profits, whether in an action of contract, negligence or other tortious action, arising out of or in connection with the use or performance of this software.
package/README.md CHANGED
@@ -1 +1,148 @@
1
- Placeholder for `v11y-check` — awaiting first release.
1
+ # v11y-check
2
+
3
+ Wraps [audit-ci](https://github.com/IBM/audit-ci) with a richer config model, typed JSON source of truth, and a sync workflow that automates allowlist management.
4
+
5
+ <!-- section:release-notes -->
6
+ ## Release notes — v0.7.0 (2026-05-10)
7
+
8
+ ### 🎉 Features
9
+
10
+ - Rename audit-deps to v11y-check (#383)
11
+
12
+ The package previously published as `@williamthorsen/audit-deps` has been renamed to `v11y-check`. The CLI command is now `v11y-check`, and the default config file is `.config/v11y-check.config.json`. Existing users should install `v11y-check` in place of `@williamthorsen/audit-deps`, rename their config file, and update any scripts that invoke `audit-deps`. Behavior is unchanged.
13
+ <!-- /section:release-notes -->
14
+
15
+ ## Installation
16
+
17
+ ```shell
18
+ pnpm add -D v11y-check
19
+ ```
20
+
21
+ The package is `v11y-check`; the bin is `v11y`. The package also ships `v11y-check` as a bin alias, so `pnpm exec v11y-check` and `npx v11y-check` continue to work.
22
+
23
+ ## Quick start
24
+
25
+ ```shell
26
+ # Run a vulnerability check with sensible defaults (no config needed)
27
+ npx v11y-check check
28
+
29
+ # Scaffold a starter config and GitHub Actions workflow
30
+ npx v11y-check init
31
+
32
+ # Sync allowlists with current findings
33
+ npx v11y-check sync
34
+
35
+ # Raw audit-ci passthrough
36
+ npx v11y-check --raw
37
+ ```
38
+
39
+ No config file is required. When `.config/v11y-check.config.json` is absent, the tool uses built-in defaults: `severityThreshold: 'moderate'` for dev, `severityThreshold: 'low'` for prod, and empty allowlists.
40
+
41
+ ## Configuration
42
+
43
+ The source-of-truth config lives at `.config/v11y-check.config.json`:
44
+
45
+ ```json
46
+ {
47
+ "$schema": "https://github.com/williamthorsen/node-monorepo-tools/raw/v11y-check-v<version>/packages/v11y-check/schemas/config.json",
48
+ "dev": {
49
+ "severityThreshold": "moderate",
50
+ "allowlist": [
51
+ {
52
+ "addedAt": "2026-04-15T09:30:00.000Z",
53
+ "id": "GHSA-1234-5678-abcd",
54
+ "path": "lodash",
55
+ "reason": "Accepted risk: no user input reaches this path",
56
+ "url": "https://github.com/advisories/GHSA-1234-5678-abcd"
57
+ }
58
+ ]
59
+ },
60
+ "prod": {
61
+ "severityThreshold": "low",
62
+ "allowlist": []
63
+ }
64
+ }
65
+ ```
66
+
67
+ ### Fields
68
+
69
+ - **`$schema`** (optional) — JSON Schema URL for editor autocomplete and validation. Automatically included by `init` and `sync`.
70
+ - **`dev`** / **`prod`** — Scope-specific settings:
71
+ - **`severityThreshold`** (optional) — Fail on advisories at or above this severity. Valid values: `'low'`, `'moderate'`, `'high'`, `'critical'`. When omitted, audit-ci uses its own defaults.
72
+ - **`allowlist`** — Typed advisory entries with `id`, `path`, `url`, and optional `reason` and `addedAt`. `addedAt` is an ISO 8601 UTC datetime (e.g., `2026-04-15T09:30:00.000Z`) populated automatically by `v11y sync` on new entries; existing entries retain whatever value they had. Older `YYYY-MM-DD` values are still accepted.
73
+
74
+ ## CLI reference
75
+
76
+ ```
77
+ Usage: v11y [options]
78
+ v11y <command> [options]
79
+
80
+ Commands:
81
+ check (default) Grouped vulnerability check with severity indicators
82
+ sync Synchronize allowlists with current audit findings
83
+ init Scaffold a starter config file and GitHub Actions workflow
84
+
85
+ Scope options:
86
+ --dev Target dev dependencies only
87
+ --prod Target production dependencies only
88
+
89
+ Other options:
90
+ --config <path> Path to config file (default: .config/v11y-check.config.json)
91
+ --json Output results as JSON
92
+ --raw Run raw audit-ci passthrough
93
+ --verbose, -v Show detailed per-vulnerability output
94
+ --help, -h Show this help message
95
+ --version, -V Show version number
96
+ ```
97
+
98
+ ### Init options
99
+
100
+ ```
101
+ --dry-run, -n Preview changes without writing files
102
+ --force, -f Overwrite existing files
103
+ ```
104
+
105
+ ### JSON output shape
106
+
107
+ `--json` emits one object per requested scope plus a top-level `summary` block:
108
+
109
+ ```jsonc
110
+ {
111
+ "prod": { "allowed": [...], "belowThreshold": [...], "stale": [...], "unallowed": [...] },
112
+ "dev": { "allowed": [...], "belowThreshold": [...], "stale": [...], "unallowed": [...] },
113
+ "summary": { "status": "vulnerabilities-found", "count": 3 }
114
+ }
115
+ ```
116
+
117
+ `summary.status` is the highest-severity finding category present across the requested scopes. Below-threshold findings never affect it.
118
+
119
+ | `status` | When |
120
+ | ---------------------------- | ------------------------------------------------- |
121
+ | `vulnerabilities-found` | At least one unallowed advisory |
122
+ | `suppressed-vulnerabilities` | No unallowed; at least one allowlisted advisory |
123
+ | `stale-overrides` | No advisories; at least one stale allowlist entry |
124
+ | `none` | No findings |
125
+
126
+ `summary.count` is the total across the requested scopes for the active category. It is `0` when `status` is `none`.
127
+
128
+ ## Scaffolded GitHub Actions workflow
129
+
130
+ `v11y init` scaffolds `.github/workflows/audit.yaml` alongside the config file. The workflow is a thin caller that delegates to the versioned reusable workflow published at `williamthorsen/node-monorepo-tools/.github/workflows/audit.reusable.yaml@workflow/audit-v1`, so your repository tracks the reusable workflow at a stable version tag.
131
+
132
+ The scaffolded workflow triggers on pull requests to `main`/`next`, on a daily schedule, and on manual `workflow_dispatch`. Commit the file into your repository so the caller runs in CI. If the reusable workflow's caller-side requirements change (for example, the tag bumps), re-run `v11y init --force` to refresh the file.
133
+
134
+ ## Migration from v0.3
135
+
136
+ v0.4 introduces breaking changes to the config schema:
137
+
138
+ - **`outDir` removed.** Intermediate audit-ci files are now written to a temp directory and cleaned up automatically. Remove `outDir` from your config.
139
+ - **Severity booleans replaced by `severityThreshold`.** Replace `"moderate": true` with `"severityThreshold": "moderate"`, `"high": true` with `"severityThreshold": "high"`, etc. Only one threshold per scope is supported.
140
+ - **`generate` subcommand removed.** The `v11y generate` command no longer exists. Flat audit-ci configs are now managed internally.
141
+ - **Config is optional.** All commands now work without a config file, using built-in defaults.
142
+
143
+ To migrate an existing config:
144
+
145
+ 1. Remove the `outDir` field.
146
+ 2. Replace severity booleans with `severityThreshold` in each scope.
147
+ 3. Optionally add a `$schema` field (run `v11y init --force` to regenerate, or add it manually).
148
+ 4. Delete any generated `audit-ci.*.json` files that were in your config directory.
package/bin/v11y.js ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ try {
3
+ await import('../dist/esm/bin/v11y.js');
4
+ } catch (error) {
5
+ if (error.code === 'ERR_MODULE_NOT_FOUND') {
6
+ process.stderr.write('v11y: build output not found — run `pnpm run build` first\n');
7
+ } else {
8
+ process.stderr.write(`v11y: failed to load: ${error.message}\n`);
9
+ }
10
+ process.exit(1);
11
+ }
@@ -0,0 +1 @@
1
+ cc7eb157b4617c1c38c8507dc4279631b07d91e6b647199879b5724bcaa312d8
@@ -0,0 +1 @@
1
+ export declare function routeCommand(args: string[]): Promise<number>;
@@ -0,0 +1,170 @@
1
+ import process from "node:process";
2
+ import { parseArgs, readPackageVersion, translateParseError } from "@williamthorsen/nmr-core";
3
+ import { auditCommand, checkCommand, extractMessage, syncCommand } from "../cli.js";
4
+ import { initCommand } from "../init/initCommand.js";
5
+ const VERSION = readPackageVersion(import.meta.url);
6
+ const SUBCOMMANDS = ["check", "init", "sync"];
7
+ const MIN_PREFIX_LENGTH = 3;
8
+ function showHelp() {
9
+ console.info(`
10
+ Usage: v11y [options]
11
+ v11y <command> [options]
12
+
13
+ Commands:
14
+ check (default) Grouped vulnerability check with severity indicators
15
+ sync Synchronize allowlists with current audit findings
16
+ init Scaffold a starter config file and GitHub Actions workflow
17
+
18
+ Scope options:
19
+ --dev Target dev dependencies only
20
+ --prod Target production dependencies only
21
+
22
+ Other options:
23
+ --config <path> Path to config file (default: .config/v11y-check.config.json)
24
+ --json Output results as JSON
25
+ --raw Run raw audit-ci passthrough
26
+ --verbose, -v Show detailed per-vulnerability output
27
+ --help, -h Show this help message
28
+ --version, -V Show version number
29
+ `);
30
+ }
31
+ function showInitHelp() {
32
+ console.info(`
33
+ Usage: v11y init [options]
34
+
35
+ Scaffold a starter config file and GitHub Actions workflow.
36
+
37
+ Options:
38
+ --dry-run, -n Preview changes without writing files
39
+ --force, -f Overwrite existing files
40
+ --help, -h Show this help message
41
+ `);
42
+ }
43
+ function showSyncHelp() {
44
+ console.info(`
45
+ Usage: v11y sync [options]
46
+
47
+ Synchronize allowlists with current audit findings.
48
+
49
+ Scope options:
50
+ --dev Target dev dependencies only
51
+ --prod Target production dependencies only
52
+
53
+ Other options:
54
+ --config <path> Path to config file (default: .config/v11y-check.config.json)
55
+ --json Output results as JSON
56
+ --help, -h Show this help message
57
+ `);
58
+ }
59
+ function parseSharedFlags(flags) {
60
+ const flagSchema = {
61
+ config: { long: "--config", type: "string" },
62
+ dev: { long: "--dev", type: "boolean" },
63
+ json: { long: "--json", type: "boolean", short: "-j" },
64
+ prod: { long: "--prod", type: "boolean" },
65
+ verbose: { long: "--verbose", type: "boolean", short: "-v" }
66
+ };
67
+ const { flags: parsed } = parseArgs(flags, flagSchema);
68
+ if (parsed.dev && parsed.prod) {
69
+ throw new Error("Cannot specify both --dev and --prod");
70
+ }
71
+ const scopes = [];
72
+ if (parsed.dev) scopes.push("dev");
73
+ if (parsed.prod) scopes.push("prod");
74
+ return {
75
+ configPath: parsed.config,
76
+ json: parsed.json,
77
+ scopes,
78
+ verbose: parsed.verbose
79
+ };
80
+ }
81
+ function findTypoMatch(input) {
82
+ if (input.length < MIN_PREFIX_LENGTH || input.startsWith("-")) {
83
+ return void 0;
84
+ }
85
+ for (const cmd of SUBCOMMANDS) {
86
+ if (cmd !== input && cmd.startsWith(input)) {
87
+ return cmd;
88
+ }
89
+ }
90
+ return void 0;
91
+ }
92
+ async function routeCommand(args) {
93
+ const command = args[0];
94
+ if (command === "--help" || command === "-h") {
95
+ showHelp();
96
+ return 0;
97
+ }
98
+ if (command === "--version" || command === "-V") {
99
+ console.info(VERSION);
100
+ return 0;
101
+ }
102
+ if (command === "check") {
103
+ return handleSubcommand(args.slice(1), checkCommand);
104
+ }
105
+ if (command === "init") {
106
+ return handleInit(args.slice(1));
107
+ }
108
+ if (command === "sync") {
109
+ return handleSubcommand(args.slice(1), syncCommand, showSyncHelp);
110
+ }
111
+ if (command !== void 0 && !command.startsWith("-")) {
112
+ const typoMatch = findTypoMatch(command);
113
+ if (typoMatch !== void 0) {
114
+ process.stderr.write(`Error: Unknown command '${command}'. Did you mean 'v11y ${typoMatch}'?
115
+ `);
116
+ return 1;
117
+ }
118
+ process.stderr.write(`Error: Unknown command '${command}'.
119
+ `);
120
+ return 1;
121
+ }
122
+ if (args.includes("--raw")) {
123
+ const filteredArgs = args.filter((a) => a !== "--raw");
124
+ return handleSubcommand(filteredArgs, auditCommand);
125
+ }
126
+ return handleSubcommand(args, checkCommand);
127
+ }
128
+ async function handleSubcommand(flags, handler, helpFn = showHelp) {
129
+ if (flags.some((f) => f === "--help" || f === "-h")) {
130
+ helpFn();
131
+ return 0;
132
+ }
133
+ let options;
134
+ try {
135
+ options = parseSharedFlags(flags);
136
+ } catch (error) {
137
+ process.stderr.write(`Error: ${translateParseError(error)}
138
+ `);
139
+ return 1;
140
+ }
141
+ try {
142
+ return await handler(options);
143
+ } catch (error) {
144
+ process.stderr.write(`Error: ${extractMessage(error)}
145
+ `);
146
+ return 1;
147
+ }
148
+ }
149
+ function handleInit(flags) {
150
+ if (flags.some((f) => f === "--help" || f === "-h")) {
151
+ showInitHelp();
152
+ return 0;
153
+ }
154
+ const initFlagSchema = {
155
+ dryRun: { long: "--dry-run", type: "boolean", short: "-n" },
156
+ force: { long: "--force", type: "boolean", short: "-f" }
157
+ };
158
+ let parsed;
159
+ try {
160
+ parsed = parseArgs(flags, initFlagSchema);
161
+ } catch (error) {
162
+ process.stderr.write(`Error: ${translateParseError(error)}
163
+ `);
164
+ return 1;
165
+ }
166
+ return initCommand({ dryRun: parsed.flags.dryRun, force: parsed.flags.force });
167
+ }
168
+ export {
169
+ routeCommand
170
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import process from "node:process";
2
+ import { routeCommand } from "./route.js";
3
+ let exitCode;
4
+ try {
5
+ exitCode = await routeCommand(process.argv.slice(2));
6
+ } catch (error) {
7
+ const message = error instanceof Error ? error.message : String(error);
8
+ process.stderr.write(`v11y: unexpected error: ${message}
9
+ `);
10
+ exitCode = 1;
11
+ }
12
+ process.exit(exitCode);
@@ -0,0 +1,5 @@
1
+ import type { CommandOptions } from './types.ts';
2
+ export declare function extractMessage(error: unknown): string;
3
+ export declare function auditCommand(options: CommandOptions): Promise<number>;
4
+ export declare function checkCommand(options: CommandOptions): Promise<number>;
5
+ export declare function syncCommand(options: CommandOptions): Promise<number>;
@@ -0,0 +1,231 @@
1
+ import process from "node:process";
2
+ import { loadConfig } from "./config.js";
3
+ import { formatCheckJson, formatCheckText } from "./format-check.js";
4
+ import { formatCheckVerboseText } from "./format-verbose.js";
5
+ import { generateAuditCiConfig } from "./generate.js";
6
+ import { scaffoldConfig } from "./init/scaffold.js";
7
+ import { parseAuditCiOutput, runAudit, runReport } from "./run-audit.js";
8
+ import { syncAllowlist } from "./sync.js";
9
+ import { withTempDir } from "./tmp.js";
10
+ import { AUDIT_SCOPES, isSeverityAtOrAbove } from "./types.js";
11
+ function extractMessage(error) {
12
+ return error instanceof Error ? error.message : String(error);
13
+ }
14
+ async function auditCommand(options) {
15
+ let loaded;
16
+ try {
17
+ loaded = await loadConfig(options.configPath);
18
+ } catch (error) {
19
+ process.stderr.write(`Error: ${extractMessage(error)}
20
+ `);
21
+ return 1;
22
+ }
23
+ const { config } = loaded;
24
+ const scopes = options.scopes.length > 0 ? options.scopes : [...AUDIT_SCOPES];
25
+ try {
26
+ return await withTempDir(async (tempDir) => {
27
+ let exitCode = 0;
28
+ const allResults = [];
29
+ for (const scope of scopes) {
30
+ const configPath = await generateAuditCiConfig(config[scope], scope, tempDir);
31
+ const result = runAudit({ configPath, json: options.json });
32
+ for (const warning of result.warnings) {
33
+ process.stderr.write(`warning: ${warning}
34
+ `);
35
+ }
36
+ if (result.staleEntries.length > 0) {
37
+ process.stderr.write(`warning: stale allowlist entries in ${scope}: ${result.staleEntries.join(", ")}
38
+ `);
39
+ }
40
+ if (options.json) {
41
+ const parsed = parseAuditCiOutput(result.stdout);
42
+ for (const warning of parsed.warnings) {
43
+ process.stderr.write(`warning: ${warning}
44
+ `);
45
+ }
46
+ for (const r of parsed.results) {
47
+ if (!allResults.some((existing) => existing.id === r.id)) {
48
+ allResults.push(r);
49
+ }
50
+ }
51
+ } else {
52
+ if (result.stdout.length > 0) {
53
+ process.stdout.write(result.stdout);
54
+ }
55
+ if (result.stderr.length > 0) {
56
+ process.stderr.write(result.stderr);
57
+ }
58
+ }
59
+ if (result.exitCode !== 0) {
60
+ exitCode = result.exitCode;
61
+ }
62
+ }
63
+ if (options.json) {
64
+ process.stdout.write(JSON.stringify(allResults, null, 2) + "\n");
65
+ }
66
+ return exitCode;
67
+ });
68
+ } catch (error) {
69
+ process.stderr.write(`Error: ${extractMessage(error)}
70
+ `);
71
+ return 1;
72
+ }
73
+ }
74
+ const CHECK_SCOPE_ORDER = ["prod", "dev"];
75
+ async function checkCommand(options) {
76
+ let loaded;
77
+ try {
78
+ loaded = await loadConfig(options.configPath);
79
+ } catch (error) {
80
+ process.stderr.write(`Error: ${extractMessage(error)}
81
+ `);
82
+ return 1;
83
+ }
84
+ const { config } = loaded;
85
+ const requestedScopes = options.scopes.length > 0 ? options.scopes : [...CHECK_SCOPE_ORDER];
86
+ const scopes = CHECK_SCOPE_ORDER.filter((s) => requestedScopes.includes(s));
87
+ try {
88
+ return await withTempDir(async (tempDir) => {
89
+ const checkResult = {
90
+ dev: { allowed: [], belowThreshold: [], stale: [], unallowed: [] },
91
+ prod: { allowed: [], belowThreshold: [], stale: [], unallowed: [] }
92
+ };
93
+ const thresholds = {};
94
+ for (const scope of scopes) {
95
+ const scopeConfig = config[scope];
96
+ const effectiveThreshold = scopeConfig.severityThreshold ?? "low";
97
+ thresholds[scope] = effectiveThreshold;
98
+ const strippedScope = { ...scopeConfig, allowlist: [], severityThreshold: "low" };
99
+ const strippedConfigPath = await generateAuditCiConfig(strippedScope, scope, tempDir);
100
+ const reportOptions = { configPath: strippedConfigPath };
101
+ if (options.verbose) {
102
+ reportOptions.reportType = "full";
103
+ }
104
+ const report = runReport(reportOptions);
105
+ for (const warning of report.warnings) {
106
+ process.stderr.write(`warning: ${warning}
107
+ `);
108
+ }
109
+ if (report.stderr.length > 0) {
110
+ process.stderr.write(report.stderr);
111
+ }
112
+ const allowedIds = new Set(scopeConfig.allowlist.map((entry) => entry.id));
113
+ const foundIds = new Set(report.results.map((r) => r.id));
114
+ const allowlistById = new Map(scopeConfig.allowlist.map((entry) => [entry.id, entry]));
115
+ const scopeResult = { allowed: [], belowThreshold: [], stale: [], unallowed: [] };
116
+ for (const result of report.results) {
117
+ if (!isSeverityAtOrAbove(result.severity, effectiveThreshold)) {
118
+ scopeResult.belowThreshold.push(result);
119
+ } else if (allowedIds.has(result.id)) {
120
+ const entry = allowlistById.get(result.id);
121
+ scopeResult.allowed.push(buildAllowedVuln(result, entry));
122
+ } else {
123
+ scopeResult.unallowed.push(result);
124
+ }
125
+ }
126
+ for (const entry of scopeConfig.allowlist) {
127
+ if (!foundIds.has(entry.id)) {
128
+ scopeResult.stale.push({ id: entry.id });
129
+ }
130
+ }
131
+ checkResult[scope] = scopeResult;
132
+ }
133
+ if (options.verbose && !options.json) {
134
+ process.stdout.write(formatCheckVerboseText(checkResult, scopes, void 0, thresholds));
135
+ } else if (options.json) {
136
+ process.stdout.write(formatCheckJson(checkResult, scopes));
137
+ } else {
138
+ process.stdout.write(formatCheckText(checkResult, scopes, void 0, thresholds));
139
+ }
140
+ const hasUnallowed = scopes.some((s) => checkResult[s].unallowed.length > 0);
141
+ return hasUnallowed ? 1 : 0;
142
+ });
143
+ } catch (error) {
144
+ process.stderr.write(`Error: ${extractMessage(error)}
145
+ `);
146
+ return 1;
147
+ }
148
+ }
149
+ async function syncCommand(options) {
150
+ let loaded;
151
+ try {
152
+ loaded = await loadConfig(options.configPath);
153
+ } catch (error) {
154
+ process.stderr.write(`Error: ${extractMessage(error)}
155
+ `);
156
+ return 1;
157
+ }
158
+ const { configSource } = loaded;
159
+ if (configSource === "defaults") {
160
+ const { configResult } = scaffoldConfig({ dryRun: false, force: false });
161
+ if (configResult.outcome === "failed") {
162
+ process.stderr.write(`Error: Failed to create config file
163
+ `);
164
+ return 1;
165
+ }
166
+ loaded = await loadConfig(options.configPath);
167
+ }
168
+ let { config } = loaded;
169
+ const { configFilePath } = loaded;
170
+ const scopes = options.scopes.length > 0 ? options.scopes : [...AUDIT_SCOPES];
171
+ try {
172
+ return await withTempDir(async (tempDir) => {
173
+ for (const scope of scopes) {
174
+ const strippedScope = { ...config[scope], allowlist: [] };
175
+ const strippedConfigPath = await generateAuditCiConfig(strippedScope, scope, tempDir);
176
+ const report = runReport({ configPath: strippedConfigPath });
177
+ for (const warning of report.warnings) {
178
+ process.stderr.write(`warning: ${warning}
179
+ `);
180
+ }
181
+ const { syncResult, updatedConfig } = await syncAllowlist(config, scope, report.results, configFilePath);
182
+ config = updatedConfig;
183
+ if (options.json) {
184
+ process.stdout.write(JSON.stringify(syncResult, null, 2) + "\n");
185
+ } else {
186
+ process.stdout.write(`
187
+ --- ${scope} ---
188
+ `);
189
+ process.stdout.write(` added: ${syncResult.added.length}
190
+ `);
191
+ process.stdout.write(` kept: ${syncResult.kept.length}
192
+ `);
193
+ process.stdout.write(` removed: ${syncResult.removed.length}
194
+ `);
195
+ }
196
+ }
197
+ if (configSource === "defaults") {
198
+ const totalEntries = config.dev.allowlist.length + config.prod.allowlist.length;
199
+ process.stdout.write(`Created config at ${configFilePath} with ${totalEntries} allowed entries.
200
+ `);
201
+ }
202
+ return 0;
203
+ });
204
+ } catch (error) {
205
+ process.stderr.write(`Error: ${extractMessage(error)}
206
+ `);
207
+ return 1;
208
+ }
209
+ }
210
+ function buildAllowedVuln(result, entry) {
211
+ const allowed = {
212
+ id: result.id,
213
+ path: result.path,
214
+ paths: result.paths,
215
+ url: result.url
216
+ };
217
+ if (result.ghsaId !== void 0) allowed.ghsaId = result.ghsaId;
218
+ if (result.severity !== void 0) allowed.severity = result.severity;
219
+ if (result.title !== void 0) allowed.title = result.title;
220
+ if (result.description !== void 0) allowed.description = result.description;
221
+ if (result.cvss !== void 0) allowed.cvss = result.cvss;
222
+ if (entry?.reason !== void 0) allowed.reason = entry.reason;
223
+ if (entry?.addedAt !== void 0) allowed.addedAt = entry.addedAt;
224
+ return allowed;
225
+ }
226
+ export {
227
+ auditCommand,
228
+ checkCommand,
229
+ extractMessage,
230
+ syncCommand
231
+ };
@@ -0,0 +1,10 @@
1
+ import type { V11yCheckConfig } from './types.ts';
2
+ export declare const DEFAULT_CONFIG_PATH = ".config/v11y-check.config.json";
3
+ export type ConfigSource = 'defaults' | 'file';
4
+ export interface LoadConfigResult {
5
+ config: V11yCheckConfig;
6
+ configDir: string;
7
+ configFilePath: string;
8
+ configSource: ConfigSource;
9
+ }
10
+ export declare function loadConfig(configPath?: string, cwd?: string): Promise<LoadConfigResult>;
@@ -0,0 +1,41 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { DEFAULT_CONFIG, v11yCheckConfigSchema } from "./types.js";
5
+ const DEFAULT_CONFIG_PATH = ".config/v11y-check.config.json";
6
+ async function loadConfig(configPath, cwd) {
7
+ const resolvedCwd = cwd ?? process.cwd();
8
+ const filePath = path.resolve(resolvedCwd, configPath ?? DEFAULT_CONFIG_PATH);
9
+ const configDir = path.dirname(filePath);
10
+ if (configPath === void 0 && !existsSync(filePath)) {
11
+ return {
12
+ config: DEFAULT_CONFIG,
13
+ configDir,
14
+ configFilePath: filePath,
15
+ configSource: "defaults"
16
+ };
17
+ }
18
+ let raw;
19
+ try {
20
+ raw = await readFile(filePath, "utf8");
21
+ } catch {
22
+ throw new Error(`Config file not found: ${filePath}`);
23
+ }
24
+ let parsed;
25
+ try {
26
+ parsed = JSON.parse(raw);
27
+ } catch {
28
+ throw new Error(`Config file is not valid JSON: ${filePath}`);
29
+ }
30
+ const result = v11yCheckConfigSchema.safeParse(parsed);
31
+ if (!result.success) {
32
+ const issues = result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n");
33
+ throw new Error(`Invalid config in ${filePath}:
34
+ ${issues}`);
35
+ }
36
+ return { config: result.data, configDir, configFilePath: filePath, configSource: "file" };
37
+ }
38
+ export {
39
+ DEFAULT_CONFIG_PATH,
40
+ loadConfig
41
+ };
@@ -0,0 +1,3 @@
1
+ import type { CheckResult } from './format-check.ts';
2
+ import type { AuditScope } from './types.ts';
3
+ export declare function formatActionHints(result: CheckResult, scopes: AuditScope[]): string;