unguard 0.16.0 → 0.16.2
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/analyze-Cccs9TDD.js +5454 -0
- package/dist/analyze-Cccs9TDD.js.map +1 -0
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +433 -407
- package/dist/cli.js.map +1 -1
- package/dist/engine-C4KGqeYw.js +479 -0
- package/dist/engine-C4KGqeYw.js.map +1 -0
- package/dist/index.d.ts +236 -226
- package/dist/index.js +3 -15
- package/dist/worker.js +46 -44
- package/dist/worker.js.map +1 -1
- package/package.json +8 -7
- package/dist/chunk-3Y76J2K2.js +0 -511
- package/dist/chunk-3Y76J2K2.js.map +0 -1
- package/dist/chunk-Z34GRD3S.js +0 -5352
- package/dist/chunk-Z34GRD3S.js.map +0 -1
- package/dist/index.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,207 +1,236 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
executeScan,
|
|
6
|
-
isFailOn,
|
|
7
|
-
isRulePolicySeverity,
|
|
8
|
-
isSeverity,
|
|
9
|
-
loadBaseline,
|
|
10
|
-
toRulePolicyEntries,
|
|
11
|
-
writeBaseline
|
|
12
|
-
} from "./chunk-3Y76J2K2.js";
|
|
13
|
-
import "./chunk-Z34GRD3S.js";
|
|
14
|
-
|
|
15
|
-
// src/cli.ts
|
|
16
|
-
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
17
|
-
import { basename, relative, resolve } from "path";
|
|
18
|
-
import { parseArgs } from "util";
|
|
1
|
+
import { a as buildBaseline, c as isFailOn, d as toRulePolicyEntries, i as BASELINE_FILENAME, l as isRulePolicySeverity, o as loadBaseline, r as computeExitCode, s as writeBaseline, t as executeScan, u as isSeverity } from "./engine-C4KGqeYw.js";
|
|
2
|
+
import { basename, relative, resolve } from "node:path";
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
19
5
|
import pc from "picocolors";
|
|
20
|
-
|
|
6
|
+
//#region src/cli.ts
|
|
7
|
+
const COMMANDS = /* @__PURE__ */ new Set([
|
|
8
|
+
"scan",
|
|
9
|
+
"audit",
|
|
10
|
+
"baseline"
|
|
11
|
+
]);
|
|
21
12
|
async function main(argv) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
13
|
+
const rawArgs = argv.slice(2);
|
|
14
|
+
const first = rawArgs[0];
|
|
15
|
+
const command = first !== void 0 && COMMANDS.has(first) ? first : "scan";
|
|
16
|
+
const { values, positionals: cliPaths } = parseArgs({
|
|
17
|
+
args: first !== void 0 && COMMANDS.has(first) ? rawArgs.slice(1) : rawArgs,
|
|
18
|
+
options: {
|
|
19
|
+
strict: {
|
|
20
|
+
type: "boolean",
|
|
21
|
+
default: false
|
|
22
|
+
},
|
|
23
|
+
filter: { type: "string" },
|
|
24
|
+
fix: {
|
|
25
|
+
type: "boolean",
|
|
26
|
+
default: false
|
|
27
|
+
},
|
|
28
|
+
format: {
|
|
29
|
+
type: "string",
|
|
30
|
+
default: "grouped"
|
|
31
|
+
},
|
|
32
|
+
severity: {
|
|
33
|
+
type: "string",
|
|
34
|
+
multiple: true,
|
|
35
|
+
default: []
|
|
36
|
+
},
|
|
37
|
+
ignore: {
|
|
38
|
+
type: "string",
|
|
39
|
+
multiple: true,
|
|
40
|
+
default: []
|
|
41
|
+
},
|
|
42
|
+
rule: {
|
|
43
|
+
type: "string",
|
|
44
|
+
multiple: true,
|
|
45
|
+
default: []
|
|
46
|
+
},
|
|
47
|
+
config: { type: "string" },
|
|
48
|
+
"fail-on": { type: "string" },
|
|
49
|
+
concurrency: { type: "string" },
|
|
50
|
+
"no-baseline": {
|
|
51
|
+
type: "boolean",
|
|
52
|
+
default: false
|
|
53
|
+
},
|
|
54
|
+
"no-cache": {
|
|
55
|
+
type: "boolean",
|
|
56
|
+
default: false
|
|
57
|
+
},
|
|
58
|
+
version: {
|
|
59
|
+
type: "boolean",
|
|
60
|
+
short: "v",
|
|
61
|
+
default: false
|
|
62
|
+
},
|
|
63
|
+
help: {
|
|
64
|
+
type: "boolean",
|
|
65
|
+
short: "h",
|
|
66
|
+
default: false
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
allowPositionals: true
|
|
70
|
+
});
|
|
71
|
+
if (values.version) {
|
|
72
|
+
console.log(readVersion());
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
if (values.help) {
|
|
76
|
+
printHelp();
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
if (values.format !== "grouped" && values.format !== "flat" && values.format !== "json") {
|
|
80
|
+
console.error(pc.red(`Invalid --format value "${values.format}". Use grouped, flat, or json.`));
|
|
81
|
+
return 1;
|
|
82
|
+
}
|
|
83
|
+
let config = null;
|
|
84
|
+
try {
|
|
85
|
+
const configPath = findConfigPath(values.config);
|
|
86
|
+
config = configPath ? loadConfig(configPath) : null;
|
|
87
|
+
} catch (err) {
|
|
88
|
+
return printErrorAndFail(err);
|
|
89
|
+
}
|
|
90
|
+
const rulePolicyResult = parseRulePolicyArgs(values.rule);
|
|
91
|
+
if (!rulePolicyResult.ok) {
|
|
92
|
+
console.error(pc.red(rulePolicyResult.message));
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
const severityFiltersResult = parseSeverityFilters(values.severity);
|
|
96
|
+
if (!severityFiltersResult.ok) {
|
|
97
|
+
console.error(pc.red(severityFiltersResult.message));
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
const failOnResult = command === "audit" ? resolveFailOn(values["fail-on"], "none") : resolveFailOn(values["fail-on"], "info", config?.failOn);
|
|
101
|
+
if (!failOnResult.ok) {
|
|
102
|
+
console.error(pc.red(failOnResult.message));
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
const concurrencyResult = resolveConcurrency(values.concurrency, config?.concurrency);
|
|
106
|
+
if (!concurrencyResult.ok) {
|
|
107
|
+
console.error(pc.red(concurrencyResult.message));
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
110
|
+
const paths = cliPaths.length > 0 ? cliPaths : config?.paths ?? [];
|
|
111
|
+
const ignore = [...config?.ignore ?? [], ...values.ignore];
|
|
112
|
+
const rulePolicy = [...toRulePolicyEntries(config?.rules), ...rulePolicyResult.value];
|
|
113
|
+
const cacheEnabled = values["no-cache"] ? false : config?.cache ?? true;
|
|
114
|
+
let baseline = null;
|
|
115
|
+
if (command === "scan" && !values["no-baseline"]) try {
|
|
116
|
+
baseline = loadBaseline(process.cwd());
|
|
117
|
+
} catch (err) {
|
|
118
|
+
return printErrorAndFail(err);
|
|
119
|
+
}
|
|
120
|
+
const execution = await executeScan({
|
|
121
|
+
paths,
|
|
122
|
+
mode: command === "audit" ? "audit" : "scan",
|
|
123
|
+
strict: values.strict,
|
|
124
|
+
rules: values.filter ? [values.filter] : void 0,
|
|
125
|
+
ignore: ignore.length > 0 ? ignore : void 0,
|
|
126
|
+
rulePolicy: rulePolicy.length > 0 ? rulePolicy : void 0,
|
|
127
|
+
overrides: config?.overrides,
|
|
128
|
+
showSeverities: severityFiltersResult.value.length > 0 ? severityFiltersResult.value : config?.severity,
|
|
129
|
+
failOn: failOnResult.value,
|
|
130
|
+
concurrency: concurrencyResult.value,
|
|
131
|
+
cache: cacheEnabled,
|
|
132
|
+
baseline: baseline ?? void 0
|
|
133
|
+
});
|
|
134
|
+
if (command === "baseline") {
|
|
135
|
+
const data = buildBaseline(execution.visibleDiagnostics, process.cwd());
|
|
136
|
+
writeBaseline(data, process.cwd());
|
|
137
|
+
console.log(`Baseline written to ${BASELINE_FILENAME}: ${plural(execution.visibleDiagnostics.length, "known issue")} across ${plural(Object.keys(data.rules).length, "file")}.`);
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
let diagnostics = execution.visibleDiagnostics;
|
|
141
|
+
let exitCode = execution.exitCode;
|
|
142
|
+
let fixedCount = 0;
|
|
143
|
+
let fixedFileCount = 0;
|
|
144
|
+
if (values.fix) {
|
|
145
|
+
const result = applyFixes(diagnostics);
|
|
146
|
+
fixedCount = result.applied;
|
|
147
|
+
fixedFileCount = result.fileCount;
|
|
148
|
+
diagnostics = diagnostics.filter((d) => !result.fixed.has(d));
|
|
149
|
+
exitCode = computeExitCode(diagnostics, failOnResult.value);
|
|
150
|
+
}
|
|
151
|
+
if (values.format === "json") {
|
|
152
|
+
console.log(JSON.stringify(buildJsonReport(diagnostics, execution.fileCount, exitCode, values.fix ? fixedCount : null), null, 2));
|
|
153
|
+
return exitCode;
|
|
154
|
+
}
|
|
155
|
+
if (baseline !== null) console.log(pc.dim(`Using ${BASELINE_FILENAME} (run "unguard baseline" to regenerate, --no-baseline to ignore)`));
|
|
156
|
+
if (values.fix && fixedCount > 0) console.log(pc.green(`Applied ${fixedCount} fix${fixedCount === 1 ? "" : "es"} in ${fixedFileCount} file${fixedFileCount === 1 ? "" : "s"}. Re-run unguard to verify.`));
|
|
157
|
+
if (diagnostics.length === 0) {
|
|
158
|
+
console.log(pc.green(`No issues found in ${plural(execution.fileCount, "file")}.`));
|
|
159
|
+
return exitCode;
|
|
160
|
+
}
|
|
161
|
+
if (values.format === "flat") printDiagnosticsFlat(diagnostics);
|
|
162
|
+
else printDiagnostics(diagnostics);
|
|
163
|
+
printSummary(diagnostics, execution.fileCount);
|
|
164
|
+
return exitCode;
|
|
149
165
|
}
|
|
150
166
|
function buildJsonReport(diagnostics, fileCount, exitCode, fixedCount) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
167
|
+
return {
|
|
168
|
+
diagnostics: diagnostics.map((d) => ({
|
|
169
|
+
file: d.file,
|
|
170
|
+
line: d.line,
|
|
171
|
+
column: d.column,
|
|
172
|
+
severity: d.severity,
|
|
173
|
+
ruleId: d.ruleId,
|
|
174
|
+
message: d.message,
|
|
175
|
+
...d.annotation !== void 0 ? { annotation: d.annotation } : {},
|
|
176
|
+
fixable: d.fix !== void 0
|
|
177
|
+
})),
|
|
178
|
+
fileCount,
|
|
179
|
+
exitCode,
|
|
180
|
+
...fixedCount !== null ? { fixedCount } : {}
|
|
181
|
+
};
|
|
166
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Apply fix edits, last-to-first per file so earlier offsets stay valid.
|
|
185
|
+
* Overlapping edits in one pass are skipped — the re-run picks them up.
|
|
186
|
+
*/
|
|
167
187
|
function applyFixes(diagnostics) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
188
|
+
const byFile = groupByFile(diagnostics.filter((d) => d.fix !== void 0));
|
|
189
|
+
const fixed = /* @__PURE__ */ new Set();
|
|
190
|
+
for (const [file, fileDiagnostics] of byFile) {
|
|
191
|
+
const ordered = [...fileDiagnostics].sort((a, b) => (b.fix?.start ?? 0) - (a.fix?.start ?? 0));
|
|
192
|
+
let content = readFileSync(file, "utf8");
|
|
193
|
+
let lastAppliedStart = Number.POSITIVE_INFINITY;
|
|
194
|
+
for (const diagnostic of ordered) {
|
|
195
|
+
const fix = diagnostic.fix;
|
|
196
|
+
if (fix === void 0) continue;
|
|
197
|
+
if (fix.end > lastAppliedStart) continue;
|
|
198
|
+
content = content.slice(0, fix.start) + fix.text + content.slice(fix.end);
|
|
199
|
+
lastAppliedStart = fix.start;
|
|
200
|
+
fixed.add(diagnostic);
|
|
201
|
+
}
|
|
202
|
+
writeFileSync(file, content, "utf8");
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
applied: fixed.size,
|
|
206
|
+
fileCount: byFile.size,
|
|
207
|
+
fixed
|
|
208
|
+
};
|
|
185
209
|
}
|
|
186
210
|
function groupByFile(diagnostics) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
211
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
212
|
+
for (const diagnostic of diagnostics) {
|
|
213
|
+
let list = byFile.get(diagnostic.file);
|
|
214
|
+
if (list === void 0) {
|
|
215
|
+
list = [];
|
|
216
|
+
byFile.set(diagnostic.file, list);
|
|
217
|
+
}
|
|
218
|
+
list.push(diagnostic);
|
|
219
|
+
}
|
|
220
|
+
return byFile;
|
|
197
221
|
}
|
|
222
|
+
/** Surface a failure as output + exit code 1 instead of a thrown error. */
|
|
198
223
|
function printErrorAndFail(err) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
224
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
225
|
+
console.error(pc.red(message));
|
|
226
|
+
return 1;
|
|
227
|
+
}
|
|
228
|
+
function readVersion() {
|
|
229
|
+
const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
230
|
+
return typeof version === "string" ? version : "unknown";
|
|
202
231
|
}
|
|
203
232
|
function printHelp() {
|
|
204
|
-
|
|
233
|
+
console.log(`unguard: data-shape static analyzer
|
|
205
234
|
|
|
206
235
|
Usage:
|
|
207
236
|
unguard scan [paths...] [options]
|
|
@@ -231,6 +260,7 @@ Options:
|
|
|
231
260
|
--concurrency <n> Worker threads for tsconfig groups (default: auto, 1 disables)
|
|
232
261
|
--no-baseline Ignore unguard.baseline.json for this scan
|
|
233
262
|
--no-cache Disable on-disk diagnostic cache (node_modules/.cache/unguard)
|
|
263
|
+
-v, --version Print version and exit
|
|
234
264
|
-h, --help Show this help
|
|
235
265
|
|
|
236
266
|
Exit codes:
|
|
@@ -251,248 +281,244 @@ Examples:
|
|
|
251
281
|
unguard scan src --fail-on=error`);
|
|
252
282
|
}
|
|
253
283
|
function printDiagnostics(diagnostics) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
284
|
+
const byFile = groupByFile(diagnostics);
|
|
285
|
+
const cwd = `${process.cwd()}/`;
|
|
286
|
+
const cwdBase = process.cwd();
|
|
287
|
+
const prefixWidth = 22;
|
|
288
|
+
const columns = process.stdout.columns;
|
|
289
|
+
const wrapWidth = columns !== void 0 && columns > 42 ? columns - prefixWidth : null;
|
|
290
|
+
for (const [file, fileDiagnostics] of byFile) {
|
|
291
|
+
const rel = relative(cwdBase, file);
|
|
292
|
+
console.log(pc.underline(rel));
|
|
293
|
+
for (const diagnostic of fileDiagnostics) {
|
|
294
|
+
const loc = `${diagnostic.line}:${diagnostic.column}`;
|
|
295
|
+
const sev = diagnostic.severity === "error" ? pc.red("error".padEnd(7)) : diagnostic.severity === "warning" ? pc.yellow("warning") : pc.blue("info".padEnd(7));
|
|
296
|
+
const msg = diagnostic.message.replaceAll(cwd, "");
|
|
297
|
+
const head = ` ${pc.dim(loc.padEnd(10))} ${sev} `;
|
|
298
|
+
const rule = pc.dim(diagnostic.ruleId);
|
|
299
|
+
const annotation = diagnostic.annotation !== void 0 ? ` ${pc.cyan(diagnostic.annotation)}` : "";
|
|
300
|
+
if (wrapWidth === null) {
|
|
301
|
+
console.log(`${head}${msg} ${rule}${annotation}`);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const lines = wrapWords(msg, wrapWidth);
|
|
305
|
+
const tailWidth = diagnostic.ruleId.length + (diagnostic.annotation !== void 0 ? 2 + diagnostic.annotation.length : 0);
|
|
306
|
+
const last = lines.length - 1;
|
|
307
|
+
const tailFits = (lines[last]?.length ?? 0) + 2 + tailWidth <= wrapWidth;
|
|
308
|
+
const indent = " ".repeat(prefixWidth);
|
|
309
|
+
for (let i = 0; i < lines.length; i++) {
|
|
310
|
+
const start = i === 0 ? head : indent;
|
|
311
|
+
const tail = i === last && tailFits ? ` ${rule}${annotation}` : "";
|
|
312
|
+
console.log(`${start}${lines[i]}${tail}`);
|
|
313
|
+
}
|
|
314
|
+
if (!tailFits) console.log(`${indent}${rule}${annotation}`);
|
|
315
|
+
}
|
|
316
|
+
console.log();
|
|
317
|
+
}
|
|
288
318
|
}
|
|
319
|
+
/** Greedy word wrap; words longer than the width get their own line unbroken. */
|
|
289
320
|
function wrapWords(text, width) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
line += ` ${word}`;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
if (line !== "") lines.push(line);
|
|
303
|
-
return lines;
|
|
321
|
+
const lines = [];
|
|
322
|
+
let line = "";
|
|
323
|
+
for (const word of text.split(" ")) if (line === "") line = word;
|
|
324
|
+
else if (line.length + 1 + word.length > width) {
|
|
325
|
+
lines.push(line);
|
|
326
|
+
line = word;
|
|
327
|
+
} else line += ` ${word}`;
|
|
328
|
+
if (line !== "") lines.push(line);
|
|
329
|
+
return lines;
|
|
304
330
|
}
|
|
305
331
|
function printDiagnosticsFlat(diagnostics) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
332
|
+
const cwdBase = process.cwd();
|
|
333
|
+
const cwd = `${cwdBase}/`;
|
|
334
|
+
for (const diagnostic of diagnostics) {
|
|
335
|
+
const rel = relative(cwdBase, diagnostic.file);
|
|
336
|
+
const msg = diagnostic.message.replaceAll(cwd, "");
|
|
337
|
+
console.log(`${rel}:${diagnostic.line}:${diagnostic.column} ${diagnostic.severity} [${diagnostic.ruleId}] ${msg}`);
|
|
338
|
+
}
|
|
313
339
|
}
|
|
314
340
|
function printSummary(diagnostics, fileCount) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
console.log(
|
|
327
|
-
`
|
|
328
|
-
${pc.bold(String(diagnostics.length))} issue${diagnostics.length === 1 ? "" : "s"} in ${plural(fileCount, "file")}.`
|
|
329
|
-
);
|
|
341
|
+
const counts = /* @__PURE__ */ new Map();
|
|
342
|
+
for (const diagnostic of diagnostics) {
|
|
343
|
+
const prev = counts.get(diagnostic.ruleId);
|
|
344
|
+
counts.set(diagnostic.ruleId, (prev === void 0 ? 0 : prev) + 1);
|
|
345
|
+
}
|
|
346
|
+
const sorted = [...counts.entries()].sort((a, b) => b[1] - a[1]);
|
|
347
|
+
const maxRule = Math.max(...sorted.map(([rule]) => rule.length));
|
|
348
|
+
console.log(pc.bold("Summary"));
|
|
349
|
+
for (const [rule, count] of sorted) console.log(` ${rule.padEnd(maxRule)} ${pc.bold(String(count))}`);
|
|
350
|
+
console.log(`\n${pc.bold(String(diagnostics.length))} issue${diagnostics.length === 1 ? "" : "s"} in ${plural(fileCount, "file")}.`);
|
|
330
351
|
}
|
|
331
352
|
function plural(count, noun) {
|
|
332
|
-
|
|
353
|
+
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
333
354
|
}
|
|
334
355
|
function parseSeverityFilters(values) {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
356
|
+
const levels = splitCsv(values);
|
|
357
|
+
const parsed = [];
|
|
358
|
+
for (const value of levels) {
|
|
359
|
+
if (!isSeverity(value)) return {
|
|
360
|
+
ok: false,
|
|
361
|
+
message: `Invalid --severity value "${value}". Use error, warning, info.`
|
|
362
|
+
};
|
|
363
|
+
parsed.push(value);
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
ok: true,
|
|
367
|
+
value: parsed
|
|
368
|
+
};
|
|
344
369
|
}
|
|
345
370
|
function parseRulePolicyArgs(values) {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
371
|
+
const entries = [];
|
|
372
|
+
for (const arg of values) {
|
|
373
|
+
const separator = resolveRuleSeparator(arg);
|
|
374
|
+
if (separator === null) return {
|
|
375
|
+
ok: false,
|
|
376
|
+
message: `Invalid --rule value "${arg}". Use <selector>=<off|info|warning|error>.`
|
|
377
|
+
};
|
|
378
|
+
const selector = arg.slice(0, separator).trim();
|
|
379
|
+
const severity = arg.slice(separator + 1).trim();
|
|
380
|
+
if (!selector) return {
|
|
381
|
+
ok: false,
|
|
382
|
+
message: `Invalid --rule value "${arg}". Rule selector is required.`
|
|
383
|
+
};
|
|
384
|
+
if (!isRulePolicySeverity(severity)) return {
|
|
385
|
+
ok: false,
|
|
386
|
+
message: `Invalid --rule value "${arg}". Severity must be off, info, warning, or error.`
|
|
387
|
+
};
|
|
388
|
+
entries.push({
|
|
389
|
+
selector,
|
|
390
|
+
severity
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
ok: true,
|
|
395
|
+
value: entries
|
|
396
|
+
};
|
|
363
397
|
}
|
|
364
398
|
function resolveRuleSeparator(value) {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
399
|
+
const equalIndex = value.indexOf("=");
|
|
400
|
+
if (equalIndex >= 0) return equalIndex;
|
|
401
|
+
const colonIndex = value.lastIndexOf(":");
|
|
402
|
+
if (colonIndex <= 0) return null;
|
|
403
|
+
return colonIndex;
|
|
370
404
|
}
|
|
371
405
|
function splitCsv(values) {
|
|
372
|
-
|
|
406
|
+
return values.flatMap((value) => value.split(",")).map((value) => value.trim()).filter((value) => value.length > 0);
|
|
373
407
|
}
|
|
374
408
|
function findConfigPath(cliValue) {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
return null;
|
|
409
|
+
if (cliValue) {
|
|
410
|
+
const explicitPath = resolve(process.cwd(), cliValue);
|
|
411
|
+
if (!existsSync(explicitPath)) throw new Error(`Config file not found: ${cliValue}`);
|
|
412
|
+
return explicitPath;
|
|
413
|
+
}
|
|
414
|
+
for (const candidate of ["unguard.config.json", ".unguardrc.json"]) {
|
|
415
|
+
const candidatePath = resolve(process.cwd(), candidate);
|
|
416
|
+
if (existsSync(candidatePath)) return candidatePath;
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
386
419
|
}
|
|
387
420
|
function loadConfig(path) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
if (!Array.isArray(raw.overrides)) {
|
|
438
|
-
throw new Error(`Invalid config in ${basename(path)}: "overrides" must be an array.`);
|
|
439
|
-
}
|
|
440
|
-
config.overrides = raw.overrides.map((entry, index) => {
|
|
441
|
-
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
442
|
-
throw new Error(`Invalid config in ${basename(path)}: overrides[${index}] must be an object.`);
|
|
443
|
-
}
|
|
444
|
-
const override = entry;
|
|
445
|
-
if (!isStringArray(override.files) || override.files.length === 0) {
|
|
446
|
-
throw new Error(`Invalid config in ${basename(path)}: overrides[${index}].files must be a non-empty string array.`);
|
|
447
|
-
}
|
|
448
|
-
return {
|
|
449
|
-
files: override.files,
|
|
450
|
-
rules: parseRulesObject(override.rules, `overrides[${index}].rules`, basename(path))
|
|
451
|
-
};
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
return config;
|
|
421
|
+
const text = readFileSync(path, "utf8");
|
|
422
|
+
let parsed;
|
|
423
|
+
try {
|
|
424
|
+
parsed = JSON.parse(text);
|
|
425
|
+
} catch (err) {
|
|
426
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
427
|
+
throw new Error(`Invalid JSON in ${basename(path)}: ${message}`);
|
|
428
|
+
}
|
|
429
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`Invalid config in ${basename(path)}: expected a JSON object.`);
|
|
430
|
+
const raw = parsed;
|
|
431
|
+
const config = {};
|
|
432
|
+
if (raw.paths !== void 0) {
|
|
433
|
+
if (!isStringArray(raw.paths)) throw new Error(`Invalid config in ${basename(path)}: "paths" must be a string array.`);
|
|
434
|
+
config.paths = raw.paths;
|
|
435
|
+
}
|
|
436
|
+
if (raw.ignore !== void 0) {
|
|
437
|
+
if (!isStringArray(raw.ignore)) throw new Error(`Invalid config in ${basename(path)}: "ignore" must be a string array.`);
|
|
438
|
+
config.ignore = raw.ignore;
|
|
439
|
+
}
|
|
440
|
+
if (raw.failOn !== void 0) {
|
|
441
|
+
if (typeof raw.failOn !== "string" || !isFailOn(raw.failOn)) throw new Error(`Invalid config in ${basename(path)}: "failOn" must be one of none, error, warning, info.`);
|
|
442
|
+
config.failOn = raw.failOn;
|
|
443
|
+
}
|
|
444
|
+
if (raw.severity !== void 0) {
|
|
445
|
+
if (!isStringArray(raw.severity) || !raw.severity.every(isSeverity)) throw new Error(`Invalid config in ${basename(path)}: "severity" must be an array of error, warning, info.`);
|
|
446
|
+
config.severity = raw.severity;
|
|
447
|
+
}
|
|
448
|
+
if (raw.concurrency !== void 0) {
|
|
449
|
+
if (typeof raw.concurrency !== "number" || !Number.isInteger(raw.concurrency) || raw.concurrency < 1) throw new Error(`Invalid config in ${basename(path)}: "concurrency" must be a positive integer.`);
|
|
450
|
+
config.concurrency = raw.concurrency;
|
|
451
|
+
}
|
|
452
|
+
if (raw.cache !== void 0) {
|
|
453
|
+
if (typeof raw.cache !== "boolean") throw new Error(`Invalid config in ${basename(path)}: "cache" must be a boolean.`);
|
|
454
|
+
config.cache = raw.cache;
|
|
455
|
+
}
|
|
456
|
+
if (raw.rules !== void 0) config.rules = parseRulesObject(raw.rules, `"rules"`, basename(path));
|
|
457
|
+
if (raw.overrides !== void 0) {
|
|
458
|
+
if (!Array.isArray(raw.overrides)) throw new Error(`Invalid config in ${basename(path)}: "overrides" must be an array.`);
|
|
459
|
+
config.overrides = raw.overrides.map((entry, index) => {
|
|
460
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) throw new Error(`Invalid config in ${basename(path)}: overrides[${index}] must be an object.`);
|
|
461
|
+
const override = entry;
|
|
462
|
+
if (!isStringArray(override.files) || override.files.length === 0) throw new Error(`Invalid config in ${basename(path)}: overrides[${index}].files must be a non-empty string array.`);
|
|
463
|
+
return {
|
|
464
|
+
files: override.files,
|
|
465
|
+
rules: parseRulesObject(override.rules, `overrides[${index}].rules`, basename(path))
|
|
466
|
+
};
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
return config;
|
|
455
470
|
}
|
|
456
471
|
function parseRulesObject(value, label, configName) {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
}
|
|
465
|
-
rules[selector] = severity;
|
|
466
|
-
}
|
|
467
|
-
return rules;
|
|
472
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`Invalid config in ${configName}: ${label} must be an object.`);
|
|
473
|
+
const rules = {};
|
|
474
|
+
for (const [selector, severity] of Object.entries(value)) {
|
|
475
|
+
if (typeof severity !== "string" || !isRulePolicySeverity(severity)) throw new Error(`Invalid config in ${configName}: rule "${selector}" in ${label} must be off, info, warning, or error.`);
|
|
476
|
+
rules[selector] = severity;
|
|
477
|
+
}
|
|
478
|
+
return rules;
|
|
468
479
|
}
|
|
469
480
|
function isStringArray(value) {
|
|
470
|
-
|
|
481
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
471
482
|
}
|
|
472
483
|
function resolveFailOn(cliValue, fallback, configValue) {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
484
|
+
const selected = cliValue ?? configValue ?? fallback;
|
|
485
|
+
if (!isFailOn(selected)) return {
|
|
486
|
+
ok: false,
|
|
487
|
+
message: `Invalid --fail-on value "${selected}". Use none, error, warning, or info.`
|
|
488
|
+
};
|
|
489
|
+
return {
|
|
490
|
+
ok: true,
|
|
491
|
+
value: selected
|
|
492
|
+
};
|
|
478
493
|
}
|
|
479
494
|
function resolveConcurrency(cliValue, configValue) {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
495
|
+
if (cliValue !== void 0) {
|
|
496
|
+
const parsed = Number(cliValue);
|
|
497
|
+
if (!Number.isInteger(parsed) || parsed < 1) return {
|
|
498
|
+
ok: false,
|
|
499
|
+
message: `Invalid --concurrency value "${cliValue}". Use a positive integer.`
|
|
500
|
+
};
|
|
501
|
+
return {
|
|
502
|
+
ok: true,
|
|
503
|
+
value: parsed
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
if (configValue !== void 0) {
|
|
507
|
+
if (!Number.isInteger(configValue) || configValue < 1) return {
|
|
508
|
+
ok: false,
|
|
509
|
+
message: "Invalid concurrency in config: must be a positive integer."
|
|
510
|
+
};
|
|
511
|
+
return {
|
|
512
|
+
ok: true,
|
|
513
|
+
value: configValue
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
return {
|
|
517
|
+
ok: true,
|
|
518
|
+
value: void 0
|
|
519
|
+
};
|
|
494
520
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
521
|
+
//#endregion
|
|
522
|
+
export { main };
|
|
523
|
+
|
|
498
524
|
//# sourceMappingURL=cli.js.map
|