v11y-check 0.9.0 → 0.10.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/esm/cli.js CHANGED
@@ -1,5 +1,5 @@
1
- import process from "node:process";
2
- import { reportError } from "@williamthorsen/nmr-core";
1
+ import process from 'node:process';
2
+ import { reportError } from '@williamthorsen/nmr-core';
3
3
  import { loadConfig } from "./config.js";
4
4
  import { formatCheckJson, formatCheckText } from "./format-check.js";
5
5
  import { formatCheckVerboseText } from "./format-verbose.js";
@@ -9,217 +9,219 @@ import { parseAuditCiOutput, runAudit, runReport } from "./run-audit.js";
9
9
  import { syncAllowlist } from "./sync.js";
10
10
  import { withTempDir } from "./tmp.js";
11
11
  import { AUDIT_SCOPES, isSeverityAtOrAbove } from "./types.js";
12
- function extractMessage(error) {
13
- return error instanceof Error ? error.message : String(error);
12
+ export function extractMessage(error) {
13
+ return error instanceof Error ? error.message : String(error);
14
14
  }
15
- async function auditCommand(options) {
16
- let loaded;
17
- try {
18
- loaded = await loadConfig(options.configPath);
19
- } catch (error) {
20
- reportError(extractMessage(error));
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);
15
+ export async function auditCommand(options) {
16
+ let loaded;
17
+ try {
18
+ loaded = await loadConfig(options.configPath);
19
+ }
20
+ catch (error) {
21
+ reportError(extractMessage(error));
22
+ return 1;
23
+ }
24
+ const { config } = loaded;
25
+ const scopes = options.scopes.length > 0 ? options.scopes : [...AUDIT_SCOPES];
26
+ try {
27
+ return await withTempDir(async (tempDir) => {
28
+ let exitCode = 0;
29
+ const allResults = [];
30
+ for (const scope of scopes) {
31
+ const configPath = await generateAuditCiConfig(config[scope], scope, tempDir);
32
+ const result = runAudit({ configPath, json: options.json });
33
+ for (const warning of result.warnings) {
34
+ process.stderr.write(`warning: ${warning}\n`);
35
+ }
36
+ if (result.staleEntries.length > 0) {
37
+ process.stderr.write(`warning: stale allowlist entries in ${scope}: ${result.staleEntries.join(', ')}\n`);
38
+ }
39
+ if (options.json) {
40
+ const parsed = parseAuditCiOutput(result.stdout);
41
+ for (const warning of parsed.warnings) {
42
+ process.stderr.write(`warning: ${warning}\n`);
43
+ }
44
+ for (const r of parsed.results) {
45
+ if (!allResults.some((existing) => existing.id === r.id)) {
46
+ allResults.push(r);
47
+ }
48
+ }
49
+ }
50
+ else {
51
+ if (result.stdout.length > 0) {
52
+ process.stdout.write(result.stdout);
53
+ }
54
+ if (result.stderr.length > 0) {
55
+ process.stderr.write(result.stderr);
56
+ }
57
+ }
58
+ if (result.exitCode !== 0) {
59
+ exitCode = result.exitCode;
60
+ }
49
61
  }
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
- reportError(extractMessage(error));
70
- return 1;
71
- }
62
+ if (options.json) {
63
+ process.stdout.write(JSON.stringify(allResults, null, 2) + '\n');
64
+ }
65
+ return exitCode;
66
+ });
67
+ }
68
+ catch (error) {
69
+ reportError(extractMessage(error));
70
+ return 1;
71
+ }
72
72
  }
73
- const CHECK_SCOPE_ORDER = ["prod", "dev"];
74
- async function checkCommand(options) {
75
- let loaded;
76
- try {
77
- loaded = await loadConfig(options.configPath);
78
- } catch (error) {
79
- reportError(extractMessage(error));
80
- return 1;
81
- }
82
- const { config } = loaded;
83
- const requestedScopes = options.scopes.length > 0 ? options.scopes : [...CHECK_SCOPE_ORDER];
84
- const scopes = CHECK_SCOPE_ORDER.filter((s) => requestedScopes.includes(s));
85
- try {
86
- return await withTempDir(async (tempDir) => {
87
- const checkResult = {
88
- dev: { allowed: [], belowThreshold: [], stale: [], unallowed: [] },
89
- prod: { allowed: [], belowThreshold: [], stale: [], unallowed: [] }
90
- };
91
- const thresholds = {};
92
- for (const scope of scopes) {
93
- const scopeConfig = config[scope];
94
- const effectiveThreshold = scopeConfig.severityThreshold ?? "low";
95
- thresholds[scope] = effectiveThreshold;
96
- const strippedScope = { ...scopeConfig, allowlist: [], severityThreshold: "low" };
97
- const strippedConfigPath = await generateAuditCiConfig(strippedScope, scope, tempDir);
98
- const reportOptions = { configPath: strippedConfigPath };
99
- if (options.verbose) {
100
- reportOptions.reportType = "full";
101
- }
102
- const report = runReport(reportOptions);
103
- for (const warning of report.warnings) {
104
- process.stderr.write(`warning: ${warning}
105
- `);
106
- }
107
- if (report.stderr.length > 0) {
108
- process.stderr.write(report.stderr);
109
- }
110
- const allowedIds = new Set(scopeConfig.allowlist.map((entry) => entry.id));
111
- const foundIds = new Set(report.results.map((r) => r.id));
112
- const allowlistById = new Map(scopeConfig.allowlist.map((entry) => [entry.id, entry]));
113
- const scopeResult = { allowed: [], belowThreshold: [], stale: [], unallowed: [] };
114
- for (const result of report.results) {
115
- if (!isSeverityAtOrAbove(result.severity, effectiveThreshold)) {
116
- scopeResult.belowThreshold.push(result);
117
- } else if (allowedIds.has(result.id)) {
118
- const entry = allowlistById.get(result.id);
119
- scopeResult.allowed.push(buildAllowedVuln(result, entry));
120
- } else {
121
- scopeResult.unallowed.push(result);
122
- }
123
- }
124
- for (const entry of scopeConfig.allowlist) {
125
- if (!foundIds.has(entry.id)) {
126
- scopeResult.stale.push({ id: entry.id });
127
- }
128
- }
129
- checkResult[scope] = scopeResult;
130
- }
131
- if (options.verbose && !options.json) {
132
- process.stdout.write(formatCheckVerboseText(checkResult, scopes, void 0, thresholds));
133
- } else if (options.json) {
134
- process.stdout.write(formatCheckJson(checkResult, scopes));
135
- } else {
136
- process.stdout.write(formatCheckText(checkResult, scopes, void 0, thresholds));
137
- }
138
- const hasUnallowed = scopes.some((s) => checkResult[s].unallowed.length > 0);
139
- return hasUnallowed ? 1 : 0;
140
- });
141
- } catch (error) {
142
- reportError(extractMessage(error));
143
- return 1;
144
- }
73
+ const CHECK_SCOPE_ORDER = ['prod', 'dev'];
74
+ export async function checkCommand(options) {
75
+ let loaded;
76
+ try {
77
+ loaded = await loadConfig(options.configPath);
78
+ }
79
+ catch (error) {
80
+ reportError(extractMessage(error));
81
+ return 1;
82
+ }
83
+ const { config } = loaded;
84
+ const requestedScopes = options.scopes.length > 0 ? options.scopes : [...CHECK_SCOPE_ORDER];
85
+ const scopes = CHECK_SCOPE_ORDER.filter((s) => requestedScopes.includes(s));
86
+ try {
87
+ return await withTempDir(async (tempDir) => {
88
+ const checkResult = {
89
+ dev: { allowed: [], belowThreshold: [], stale: [], unallowed: [] },
90
+ prod: { allowed: [], belowThreshold: [], stale: [], unallowed: [] },
91
+ };
92
+ const thresholds = {};
93
+ for (const scope of scopes) {
94
+ const scopeConfig = config[scope];
95
+ const effectiveThreshold = scopeConfig.severityThreshold ?? 'low';
96
+ thresholds[scope] = effectiveThreshold;
97
+ const strippedScope = { ...scopeConfig, allowlist: [], severityThreshold: 'low' };
98
+ const strippedConfigPath = await generateAuditCiConfig(strippedScope, scope, tempDir);
99
+ const reportOptions = { configPath: strippedConfigPath };
100
+ if (options.verbose) {
101
+ reportOptions.reportType = 'full';
102
+ }
103
+ const report = runReport(reportOptions);
104
+ for (const warning of report.warnings) {
105
+ process.stderr.write(`warning: ${warning}\n`);
106
+ }
107
+ if (report.stderr.length > 0) {
108
+ process.stderr.write(report.stderr);
109
+ }
110
+ const allowedIds = new Set(scopeConfig.allowlist.map((entry) => entry.id));
111
+ const foundIds = new Set(report.results.map((r) => r.id));
112
+ const allowlistById = new Map(scopeConfig.allowlist.map((entry) => [entry.id, entry]));
113
+ const scopeResult = { allowed: [], belowThreshold: [], stale: [], unallowed: [] };
114
+ for (const result of report.results) {
115
+ if (!isSeverityAtOrAbove(result.severity, effectiveThreshold)) {
116
+ scopeResult.belowThreshold.push(result);
117
+ }
118
+ else if (allowedIds.has(result.id)) {
119
+ const entry = allowlistById.get(result.id);
120
+ scopeResult.allowed.push(buildAllowedVuln(result, entry));
121
+ }
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, undefined, thresholds));
135
+ }
136
+ else if (options.json) {
137
+ process.stdout.write(formatCheckJson(checkResult, scopes));
138
+ }
139
+ else {
140
+ process.stdout.write(formatCheckText(checkResult, scopes, undefined, thresholds));
141
+ }
142
+ const hasUnallowed = scopes.some((s) => checkResult[s].unallowed.length > 0);
143
+ return hasUnallowed ? 1 : 0;
144
+ });
145
+ }
146
+ catch (error) {
147
+ reportError(extractMessage(error));
148
+ return 1;
149
+ }
145
150
  }
146
- async function syncCommand(options) {
147
- let loaded;
148
- try {
149
- loaded = await loadConfig(options.configPath);
150
- } catch (error) {
151
- reportError(extractMessage(error));
152
- return 1;
153
- }
154
- const { configSource } = loaded;
155
- if (configSource === "defaults") {
156
- const { configResult } = scaffoldConfig({ dryRun: false, force: false });
157
- if (configResult.outcome === "failed") {
158
- reportError("Failed to create config file");
159
- return 1;
151
+ export async function syncCommand(options) {
152
+ let loaded;
153
+ try {
154
+ loaded = await loadConfig(options.configPath);
160
155
  }
161
- loaded = await loadConfig(options.configPath);
162
- }
163
- let { config } = loaded;
164
- const { configFilePath } = loaded;
165
- const scopes = options.scopes.length > 0 ? options.scopes : [...AUDIT_SCOPES];
166
- try {
167
- return await withTempDir(async (tempDir) => {
168
- for (const scope of scopes) {
169
- const strippedScope = { ...config[scope], allowlist: [] };
170
- const strippedConfigPath = await generateAuditCiConfig(strippedScope, scope, tempDir);
171
- const report = runReport({ configPath: strippedConfigPath });
172
- for (const warning of report.warnings) {
173
- process.stderr.write(`warning: ${warning}
174
- `);
175
- }
176
- const { syncResult, updatedConfig } = await syncAllowlist(config, scope, report.results, configFilePath);
177
- config = updatedConfig;
178
- if (options.json) {
179
- process.stdout.write(JSON.stringify(syncResult, null, 2) + "\n");
180
- } else {
181
- process.stdout.write(`
182
- --- ${scope} ---
183
- `);
184
- process.stdout.write(` added: ${syncResult.added.length}
185
- `);
186
- process.stdout.write(` kept: ${syncResult.kept.length}
187
- `);
188
- process.stdout.write(` removed: ${syncResult.removed.length}
189
- `);
156
+ catch (error) {
157
+ reportError(extractMessage(error));
158
+ return 1;
159
+ }
160
+ const { configSource } = loaded;
161
+ if (configSource === 'defaults') {
162
+ const { configResult } = scaffoldConfig({ dryRun: false, force: false });
163
+ if (configResult.outcome === 'failed') {
164
+ reportError('Failed to create config file');
165
+ return 1;
190
166
  }
191
- }
192
- if (configSource === "defaults") {
193
- const totalEntries = config.dev.allowlist.length + config.prod.allowlist.length;
194
- process.stdout.write(`Created config at ${configFilePath} with ${totalEntries} allowed entries.
195
- `);
196
- }
197
- return 0;
198
- });
199
- } catch (error) {
200
- reportError(extractMessage(error));
201
- return 1;
202
- }
167
+ loaded = await loadConfig(options.configPath);
168
+ }
169
+ let { config } = loaded;
170
+ const { configFilePath } = loaded;
171
+ const scopes = options.scopes.length > 0 ? options.scopes : [...AUDIT_SCOPES];
172
+ try {
173
+ return await withTempDir(async (tempDir) => {
174
+ for (const scope of scopes) {
175
+ const strippedScope = { ...config[scope], allowlist: [] };
176
+ const strippedConfigPath = await generateAuditCiConfig(strippedScope, scope, tempDir);
177
+ const report = runReport({ configPath: strippedConfigPath });
178
+ for (const warning of report.warnings) {
179
+ process.stderr.write(`warning: ${warning}\n`);
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
+ }
186
+ else {
187
+ process.stdout.write(`\n--- ${scope} ---\n`);
188
+ process.stdout.write(` added: ${syncResult.added.length}\n`);
189
+ process.stdout.write(` kept: ${syncResult.kept.length}\n`);
190
+ process.stdout.write(` removed: ${syncResult.removed.length}\n`);
191
+ }
192
+ }
193
+ if (configSource === 'defaults') {
194
+ const totalEntries = config.dev.allowlist.length + config.prod.allowlist.length;
195
+ process.stdout.write(`Created config at ${configFilePath} with ${totalEntries} allowed entries.\n`);
196
+ }
197
+ return 0;
198
+ });
199
+ }
200
+ catch (error) {
201
+ reportError(extractMessage(error));
202
+ return 1;
203
+ }
203
204
  }
204
205
  function buildAllowedVuln(result, entry) {
205
- const allowed = {
206
- id: result.id,
207
- path: result.path,
208
- paths: result.paths,
209
- url: result.url
210
- };
211
- if (result.ghsaId !== void 0) allowed.ghsaId = result.ghsaId;
212
- if (result.severity !== void 0) allowed.severity = result.severity;
213
- if (result.title !== void 0) allowed.title = result.title;
214
- if (result.description !== void 0) allowed.description = result.description;
215
- if (result.cvss !== void 0) allowed.cvss = result.cvss;
216
- if (entry?.reason !== void 0) allowed.reason = entry.reason;
217
- if (entry?.addedAt !== void 0) allowed.addedAt = entry.addedAt;
218
- return allowed;
206
+ const allowed = {
207
+ id: result.id,
208
+ path: result.path,
209
+ paths: result.paths,
210
+ url: result.url,
211
+ };
212
+ if (result.ghsaId !== undefined)
213
+ allowed.ghsaId = result.ghsaId;
214
+ if (result.severity !== undefined)
215
+ allowed.severity = result.severity;
216
+ if (result.title !== undefined)
217
+ allowed.title = result.title;
218
+ if (result.description !== undefined)
219
+ allowed.description = result.description;
220
+ if (result.cvss !== undefined)
221
+ allowed.cvss = result.cvss;
222
+ if (entry?.reason !== undefined)
223
+ allowed.reason = entry.reason;
224
+ if (entry?.addedAt !== undefined)
225
+ allowed.addedAt = entry.addedAt;
226
+ return allowed;
219
227
  }
220
- export {
221
- auditCommand,
222
- checkCommand,
223
- extractMessage,
224
- syncCommand
225
- };
@@ -1,4 +1,4 @@
1
- import type { V11yCheckConfig } from './types.ts';
1
+ import type { V11yCheckConfig } from './types.js';
2
2
  export declare const DEFAULT_CONFIG_PATH = ".config/v11y-check.config.json";
3
3
  export type ConfigSource = 'defaults' | 'file';
4
4
  export interface LoadConfigResult {
@@ -1,41 +1,38 @@
1
- import { existsSync } from "node:fs";
2
- import { readFile } from "node:fs/promises";
3
- import path from "node:path";
1
+ import { existsSync } from 'node:fs';
2
+ import { readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
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" };
5
+ export const DEFAULT_CONFIG_PATH = '.config/v11y-check.config.json';
6
+ export 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 === undefined && !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
+ }
22
+ catch {
23
+ throw new Error(`Config file not found: ${filePath}`);
24
+ }
25
+ let parsed;
26
+ try {
27
+ parsed = JSON.parse(raw);
28
+ }
29
+ catch {
30
+ throw new Error(`Config file is not valid JSON: ${filePath}`);
31
+ }
32
+ const result = v11yCheckConfigSchema.safeParse(parsed);
33
+ if (!result.success) {
34
+ const issues = result.error.issues.map((issue) => ` - ${issue.path.join('.')}: ${issue.message}`).join('\n');
35
+ throw new Error(`Invalid config in ${filePath}:\n${issues}`);
36
+ }
37
+ return { config: result.data, configDir, configFilePath: filePath, configSource: 'file' };
37
38
  }
38
- export {
39
- DEFAULT_CONFIG_PATH,
40
- loadConfig
41
- };
@@ -1,3 +1,3 @@
1
- import type { CheckResult } from './format-check.ts';
2
- import type { AuditScope } from './types.ts';
1
+ import type { CheckResult } from './format-check.js';
2
+ import type { AuditScope } from './types.js';
3
3
  export declare function formatActionHints(result: CheckResult, scopes: AuditScope[]): string;
@@ -1,34 +1,33 @@
1
- const HINT_ADD = "Run `v11y sync` to add the listed vulnerabilities to the allowlist.";
2
- const HINT_REMOVE = "Run `v11y sync` to remove stale allowlist entries.";
3
- const HINT_BOTH = "Run `v11y sync` to add the listed vulnerabilities to the allowlist and remove stale entries.";
1
+ const HINT_ADD = 'Run `v11y sync` to add the listed vulnerabilities to the allowlist.';
2
+ const HINT_REMOVE = 'Run `v11y sync` to remove stale allowlist entries.';
3
+ const HINT_BOTH = 'Run `v11y sync` to add the listed vulnerabilities to the allowlist and remove stale entries.';
4
4
  function buildVerboseFlag(scopes) {
5
- if (scopes.length === 1) {
6
- return `--${scopes[0]} --verbose`;
7
- }
8
- return "--verbose";
5
+ if (scopes.length === 1) {
6
+ return `--${scopes[0]} --verbose`;
7
+ }
8
+ return '--verbose';
9
9
  }
10
10
  function selectSyncHint(hasUnallowed, hasStale) {
11
- if (hasUnallowed && hasStale) return HINT_BOTH;
12
- if (hasUnallowed) return HINT_ADD;
13
- return HINT_REMOVE;
11
+ if (hasUnallowed && hasStale)
12
+ return HINT_BOTH;
13
+ if (hasUnallowed)
14
+ return HINT_ADD;
15
+ return HINT_REMOVE;
14
16
  }
15
- function formatActionHints(result, scopes) {
16
- const hasUnallowed = scopes.some((scope) => result[scope].unallowed.length > 0);
17
- const hasAllowed = scopes.some((scope) => result[scope].allowed.length > 0);
18
- const hasStale = scopes.some((scope) => result[scope].stale.length > 0);
19
- const hasVulns = hasUnallowed || hasAllowed;
20
- const needsSync = hasUnallowed || hasStale;
21
- if (!hasVulns && !needsSync) return "";
22
- const bullets = [];
23
- if (hasVulns) {
24
- bullets.push(` \u2022 Run \`v11y check ${buildVerboseFlag(scopes)}\` for full report`);
25
- }
26
- if (needsSync) {
27
- bullets.push(` \u2022 ${selectSyncHint(hasUnallowed, hasStale)}`);
28
- }
29
- return ` Actions:
30
- ${bullets.join("\n")}`;
17
+ export function formatActionHints(result, scopes) {
18
+ const hasUnallowed = scopes.some((scope) => result[scope].unallowed.length > 0);
19
+ const hasAllowed = scopes.some((scope) => result[scope].allowed.length > 0);
20
+ const hasStale = scopes.some((scope) => result[scope].stale.length > 0);
21
+ const hasVulns = hasUnallowed || hasAllowed;
22
+ const needsSync = hasUnallowed || hasStale;
23
+ if (!hasVulns && !needsSync)
24
+ return '';
25
+ const bullets = [];
26
+ if (hasVulns) {
27
+ bullets.push(` \u{2022} Run \`v11y check ${buildVerboseFlag(scopes)}\` for full report`);
28
+ }
29
+ if (needsSync) {
30
+ bullets.push(` \u{2022} ${selectSyncHint(hasUnallowed, hasStale)}`);
31
+ }
32
+ return ` Actions:\n${bullets.join('\n')}`;
31
33
  }
32
- export {
33
- formatActionHints
34
- };
@@ -1,4 +1,4 @@
1
- import type { AuditResult, AuditScope, SeverityThreshold } from './types.ts';
1
+ import type { AuditResult, AuditScope, SeverityThreshold } from './types.js';
2
2
  export interface AllowedVuln {
3
3
  addedAt?: string | undefined;
4
4
  cvss?: {