@vhite/vhite 0.1.0 → 0.8.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/README.md +119 -22
- package/bin/cli.js +43 -200
- package/bin/commands/baseline.js +27 -0
- package/bin/commands/hook.js +58 -0
- package/bin/commands/info.js +39 -0
- package/bin/commands/scan.js +257 -0
- package/bin/commands/shared.js +67 -0
- package/bin/commands/watch.js +44 -0
- package/bin/output/text.js +42 -0
- package/package.json +32 -5
- package/rules/a11y/a11y-click-div.js +39 -0
- package/rules/a11y/a11y-img-alt.js +36 -0
- package/rules/a11y/a11y-input-label.js +40 -0
- package/rules/{console-log.js → core/console-log.js} +2 -1
- package/rules/{debugger-statement.js → core/debugger-statement.js} +2 -1
- package/rules/{empty-block.js → core/empty-block.js} +2 -1
- package/rules/{empty-catch.js → core/empty-catch.js} +2 -1
- package/rules/{hardcoded-secret.js → core/hardcoded-secret.js} +9 -4
- package/rules/{leftover-todo.js → core/leftover-todo.js} +6 -2
- package/rules/{unused-import.js → core/unused-import.js} +3 -1
- package/rules/index.js +18 -8
- package/rules/react/react-dangerous-html.js +30 -0
- package/rules/react/react-missing-key.js +56 -0
- package/rules/shared/jsx-helpers.js +30 -0
- package/src/core/args.js +140 -0
- package/src/core/config.js +92 -0
- package/src/core/engine.js +144 -0
- package/src/core/exit-codes.js +15 -0
- package/src/{setup.js → core/setup.js} +1 -1
- package/src/core/version.js +6 -0
- package/src/{safe-fix.js → fix/safe-fix.js} +1 -1
- package/src/output/explanations-frontend.js +22 -0
- package/src/{explanations.js → output/explanations.js} +2 -0
- package/src/output/json-output.js +47 -0
- package/src/{reporter.js → output/reporter.js} +20 -1
- package/src/{run-report.js → output/run-report.js} +1 -1
- package/src/output/sarif.js +60 -0
- package/src/scan/baseline.js +60 -0
- package/src/scan/git.js +28 -0
- package/src/scan/ignore.js +68 -0
- package/src/scan/suppress.js +53 -0
- package/src/smart/consent.js +34 -0
- package/src/smart/fixes.js +143 -0
- package/src/smart/notes.js +73 -0
- package/src/config.js +0 -42
- package/src/engine.js +0 -80
- /package/src/{files.js → core/files.js} +0 -0
- /package/src/{approve.js → fix/approve.js} +0 -0
- /package/src/{backup.js → fix/backup.js} +0 -0
- /package/src/{fixers → fix/fixers}/debugger-statement.js +0 -0
- /package/src/{fixers → fix/fixers}/index.js +0 -0
- /package/src/{fixers → fix/fixers}/leftover-todo.js +0 -0
- /package/src/{fixers → fix/fixers}/unused-import.js +0 -0
- /package/src/{gitignore.js → fix/gitignore.js} +0 -0
package/README.md
CHANGED
|
@@ -1,32 +1,103 @@
|
|
|
1
1
|
# @vhite/vhite
|
|
2
2
|
|
|
3
|
-
vhite scans front-end code for
|
|
3
|
+
vhite scans front-end code for problems commonly left behind by AI code generators. It explains each finding in plain English, and it can safely fix some of them.
|
|
4
4
|
|
|
5
5
|
## Install and run
|
|
6
6
|
|
|
7
|
-
Run it on any project
|
|
7
|
+
Run it on any project without installing it:
|
|
8
8
|
|
|
9
9
|
npx @vhite/vhite path/to/your/project
|
|
10
10
|
|
|
11
|
-
Or
|
|
11
|
+
Or on the current folder:
|
|
12
12
|
|
|
13
|
-
npx @vhite/vhite
|
|
13
|
+
npx @vhite/vhite
|
|
14
|
+
|
|
15
|
+
To install it once and use a short command:
|
|
16
|
+
|
|
17
|
+
npm install -g @vhite/vhite
|
|
18
|
+
vhite path/to/your/project
|
|
19
|
+
|
|
20
|
+
## Commands
|
|
21
|
+
|
|
22
|
+
vhite [path] Scan a folder (the default command)
|
|
23
|
+
vhite scan [path] Scan and report
|
|
24
|
+
vhite fix [path] Scan, then choose which fixes to apply
|
|
25
|
+
vhite baseline [path] Save current problems so only new ones are reported
|
|
26
|
+
vhite rules List all rules
|
|
27
|
+
vhite explain <rule> Explain one rule
|
|
28
|
+
vhite version Show the version
|
|
29
|
+
|
|
30
|
+
## Options
|
|
31
|
+
|
|
32
|
+
--format text|json Output format (default: text)
|
|
33
|
+
--quiet, -q Print only errors; the exit code still applies
|
|
34
|
+
--fix Apply all safe fixes without asking, then rescan
|
|
35
|
+
--changed Scan only files changed since the last commit (Git)
|
|
36
|
+
--staged Scan only files staged for commit (Git)
|
|
37
|
+
--no-baseline Ignore vhite.baseline.json for this run
|
|
38
|
+
--rule <name> Run only this rule (repeatable)
|
|
39
|
+
--disable <name> Turn off a rule (repeatable)
|
|
40
|
+
--include <path> Scan only files under this path (repeatable)
|
|
41
|
+
--exclude <path> Skip files under this path (repeatable)
|
|
42
|
+
--help, -h Show help
|
|
43
|
+
|
|
44
|
+
## Exit codes
|
|
45
|
+
|
|
46
|
+
0 no problems
|
|
47
|
+
1 problems found (errors or warnings; info notes do not count)
|
|
48
|
+
2 configuration or runtime error
|
|
49
|
+
3 fixes applied and no problems remain
|
|
50
|
+
4 fixes were rolled back
|
|
51
|
+
|
|
52
|
+
Use these in CI. For example, a build fails when the exit code is 1 or higher.
|
|
14
53
|
|
|
15
54
|
## What it checks
|
|
16
55
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
56
|
+
JavaScript and JSX files (`.js`, `.jsx`, `.mjs`, `.cjs`):
|
|
57
|
+
|
|
58
|
+
- **debugger-statement:** `debugger` lines that pause the app when developer tools are open.
|
|
59
|
+
- **console-log:** `console.log` calls left over from debugging.
|
|
60
|
+
- **empty-catch:** errors that are caught and silently ignored.
|
|
61
|
+
- **unused-import:** imports the file never uses.
|
|
62
|
+
- **empty-block:** `if`, `else`, and loop bodies with no code.
|
|
63
|
+
- **hardcoded-secret:** passwords, API keys, and tokens written directly in the code.
|
|
64
|
+
- **leftover-todo:** comments. TODO and FIXME notes are reported separately from other comments.
|
|
65
|
+
- **react-missing-key:** list items created with `.map()` that have no `key`.
|
|
66
|
+
- **react-dangerous-html:** uses of `dangerouslySetInnerHTML`.
|
|
67
|
+
- **a11y-img-alt:** images with no `alt` attribute.
|
|
68
|
+
- **a11y-input-label:** form inputs with no label or accessible name.
|
|
69
|
+
- **a11y-click-div:** clickable `div` or `span` elements with no `role`.
|
|
70
|
+
|
|
71
|
+
HTML and CSS files (`.html`, `.css`) are checked for hardcoded secrets only.
|
|
72
|
+
|
|
73
|
+
Run `vhite rules` for the full list, with each rule's default severity and whether it has an automatic fix.
|
|
74
|
+
|
|
75
|
+
## Severity
|
|
76
|
+
|
|
77
|
+
Each rule has a severity: `error`, `warning`, or `info`. Errors and warnings count toward the exit code. Info notes are shown but don't fail a run.
|
|
78
|
+
|
|
79
|
+
Change a rule's severity in `vhite.config.json`:
|
|
80
|
+
|
|
81
|
+
{
|
|
82
|
+
"rules": {
|
|
83
|
+
"console-log": "error",
|
|
84
|
+
"leftover-todo": "off"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
Use `"off"` to turn a rule off. You can also turn rules off with a list:
|
|
24
89
|
|
|
25
|
-
|
|
90
|
+
{
|
|
91
|
+
"disabledRules": ["console-log"]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
Unknown rule names are reported as warnings, and the rest of the config still applies.
|
|
26
95
|
|
|
27
96
|
## Safe fixes
|
|
28
97
|
|
|
29
|
-
|
|
98
|
+
Smart notes and smart fixes are optional. When you use them, the lines of code around each finding are sent to Google’s free service, with secrets removed first.
|
|
99
|
+
|
|
100
|
+
Some problems can be fixed automatically: debugger statements, unused imports, and comments. vhite never changes a file without your approval.
|
|
30
101
|
|
|
31
102
|
1. It scans the project and lists the problems it found.
|
|
32
103
|
2. It lists the fixes it can make and asks which ones to apply. Enter numbers such as `1,3`, type `all`, or press Enter to skip.
|
|
@@ -34,23 +105,49 @@ Some problems can be fixed automatically: debugger statements, unused imports, a
|
|
|
34
105
|
4. After each change, it scans the file again. If the change created a new problem or didn't fix the issue, the original is restored automatically.
|
|
35
106
|
5. At the end, it can ask whether to delete the backups from this run and whether to create a Markdown report in `.vhite-fix/reports/`.
|
|
36
107
|
|
|
37
|
-
The first time a fix is applied, vhite adds `.vhite-fix/` to your `.gitignore
|
|
108
|
+
The first time a fix is applied, vhite adds `.vhite-fix/` to your `.gitignore`.
|
|
38
109
|
|
|
39
|
-
|
|
110
|
+
For scripts and CI, use `--fix` to apply all safe fixes without asking. `vhite fix` requires an interactive terminal unless `--fix` is given.
|
|
40
111
|
|
|
41
|
-
##
|
|
112
|
+
## Baselines
|
|
42
113
|
|
|
43
|
-
|
|
114
|
+
Projects that already have problems can start with a baseline:
|
|
44
115
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
116
|
+
vhite baseline
|
|
117
|
+
|
|
118
|
+
This saves the current findings to `vhite.baseline.json`. Later scans hide those known findings and report only new ones. A finding stays hidden even if its line number moves. Commit this file so your whole team shares the same baseline, and use `--no-baseline` to see everything.
|
|
119
|
+
|
|
120
|
+
## Ignoring a finding
|
|
121
|
+
|
|
122
|
+
Add a comment to skip a finding you've reviewed:
|
|
123
|
+
|
|
124
|
+
// vhite-disable-next-line console-log
|
|
125
|
+
console.log("kept on purpose");
|
|
126
|
+
|
|
127
|
+
console.log("kept on purpose"); // vhite-disable-line console-log
|
|
128
|
+
|
|
129
|
+
Leave out the rule name to skip every rule on that line.
|
|
130
|
+
|
|
131
|
+
## Scanning only changed files
|
|
132
|
+
|
|
133
|
+
vhite --changed # files changed since the last commit
|
|
134
|
+
vhite --staged # files staged for commit
|
|
135
|
+
|
|
136
|
+
Both need the project to be a Git repository.
|
|
137
|
+
|
|
138
|
+
## Settings file
|
|
139
|
+
|
|
140
|
+
Ignore patterns come from your `.gitignore`. vhite also skips `node_modules`, `dist`, `build`, and `.git` by default.
|
|
141
|
+
|
|
142
|
+
## Reports
|
|
143
|
+
|
|
144
|
+
--format json Prints results as JSON for scripts and other tools
|
|
48
145
|
|
|
49
|
-
|
|
146
|
+
The JSON output includes each finding's rule, severity, file, line, column, and message, plus a summary and any fixes that were applied.
|
|
50
147
|
|
|
51
148
|
## Limits
|
|
52
149
|
|
|
53
|
-
vhite finds common patterns. It
|
|
150
|
+
vhite finds common patterns. It can't understand your whole app, so review each fix before you commit it. Removing an unused import also stops that module from loading, which is usually what you want, but check modules that only run for their side effects. The accessibility and React rules are heuristics and may report some false positives.
|
|
54
151
|
|
|
55
152
|
## License
|
|
56
153
|
|
package/bin/cli.js
CHANGED
|
@@ -1,216 +1,59 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
import { FIX_DIR } from "../src/gitignore.js";
|
|
25
|
-
|
|
26
|
-
import {
|
|
27
|
-
askYesNo,
|
|
28
|
-
writeRunReport,
|
|
29
|
-
} from "../src/run-report.js";
|
|
30
|
-
|
|
31
|
-
// Capture terminal messages so the report can include them.
|
|
32
|
-
const logs = [];
|
|
33
|
-
|
|
34
|
-
const originalLog = console.log;
|
|
35
|
-
const originalWarn = console.warn;
|
|
36
|
-
const originalError = console.error;
|
|
37
|
-
|
|
38
|
-
function capture(method, label = "") {
|
|
39
|
-
return (...args) => {
|
|
40
|
-
const message = args
|
|
41
|
-
.map((arg) => {
|
|
42
|
-
if (typeof arg === "string") {
|
|
43
|
-
return arg;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
try {
|
|
47
|
-
return JSON.stringify(arg) ?? String(arg);
|
|
48
|
-
} catch {
|
|
49
|
-
return String(arg);
|
|
50
|
-
}
|
|
51
|
-
})
|
|
52
|
-
.join(" ");
|
|
53
|
-
|
|
54
|
-
logs.push(label ? `${label}: ${message}` : message);
|
|
55
|
-
method(...args);
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
console.log = capture(originalLog);
|
|
60
|
-
console.warn = capture(originalWarn, "WARN");
|
|
61
|
-
console.error = capture(originalError, "ERROR");
|
|
62
|
-
|
|
63
|
-
const startedAt = new Date();
|
|
64
|
-
const target = resolve(process.argv[2] ?? ".");
|
|
65
|
-
|
|
66
|
-
let files = [];
|
|
67
|
-
let issues = [];
|
|
68
|
-
let proposals = [];
|
|
69
|
-
let fixResults = [];
|
|
70
|
-
let backupCleanup = null;
|
|
71
|
-
let config = null;
|
|
72
|
-
|
|
73
|
-
console.log(
|
|
74
|
-
`vhite is installed and running
|
|
75
|
-
loading..
|
|
76
|
-
....
|
|
77
|
-
......
|
|
78
|
-
........
|
|
79
|
-
Done..`
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
if (
|
|
84
|
-
!existsSync(join(target, CONFIG_NAME)) &&
|
|
85
|
-
canAskQuestions()
|
|
86
|
-
) {
|
|
87
|
-
await runFirstTimeSetup(target);
|
|
3
|
+
import { parseArgs, UsageError } from "../src/core/args.js";
|
|
4
|
+
import { EXIT } from "../src/core/exit-codes.js";
|
|
5
|
+
import { readVersion } from "../src/core/version.js";
|
|
6
|
+
import { USAGE } from "./output/text.js";
|
|
7
|
+
import { listRules, explainRule } from "./commands/info.js";
|
|
8
|
+
import { runBaseline } from "./commands/baseline.js";
|
|
9
|
+
import { runScan } from "./commands/scan.js";
|
|
10
|
+
import { runHook } from "./commands/hook.js";
|
|
11
|
+
import { runWatch } from "./commands/watch.js";
|
|
12
|
+
|
|
13
|
+
async function main() {
|
|
14
|
+
let options;
|
|
15
|
+
try {
|
|
16
|
+
options = parseArgs(process.argv.slice(2));
|
|
17
|
+
} catch (error) {
|
|
18
|
+
if (error instanceof UsageError) {
|
|
19
|
+
console.error(`Error: ${error.message}\n`);
|
|
20
|
+
console.error(USAGE);
|
|
21
|
+
return EXIT.ERROR;
|
|
22
|
+
}
|
|
23
|
+
throw error;
|
|
88
24
|
}
|
|
89
25
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
console.log(`Warning: ${warning}`);
|
|
26
|
+
if (options.command === "help") {
|
|
27
|
+
console.log(USAGE);
|
|
28
|
+
return EXIT.CLEAN;
|
|
94
29
|
}
|
|
95
30
|
|
|
96
|
-
if (
|
|
97
|
-
console.log(
|
|
98
|
-
|
|
99
|
-
);
|
|
31
|
+
if (options.command === "version") {
|
|
32
|
+
console.log(await readVersion());
|
|
33
|
+
return EXIT.CLEAN;
|
|
100
34
|
}
|
|
101
35
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
console.log(
|
|
105
|
-
`vhite found ${files.length} front-end files in ${relative(process.cwd(), target) || "."}\n`
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
issues = await runRules(files, {
|
|
109
|
-
disabledRules: config.disabledRules,
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
console.log(formatReport(issues, target));
|
|
113
|
-
proposals = await proposeFixes(issues);
|
|
114
|
-
|
|
115
|
-
if (proposals.length > 0 && canAskQuestions()) {
|
|
116
|
-
const chosen = await chooseProposals(target, proposals);
|
|
117
|
-
|
|
118
|
-
if (chosen.length > 0) {
|
|
119
|
-
const result = await applyApprovedFixes(
|
|
120
|
-
target,
|
|
121
|
-
chosen,
|
|
122
|
-
{ disabledRules: config.disabledRules }
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
fixResults = result.results;
|
|
126
|
-
|
|
127
|
-
if (result.gitignoreUpdated) {
|
|
128
|
-
console.log(`Added ${FIX_DIR}/ to .gitignore`);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
for (const item of fixResults) {
|
|
132
|
-
if (item.status === "applied") {
|
|
133
|
-
console.log(
|
|
134
|
-
`Applied: ${relative(target, item.proposal.file)} line ${item.proposal.line}`
|
|
135
|
-
);
|
|
136
|
-
} else {
|
|
137
|
-
const label = item.status === "skipped" ? "Skipped" : "Rolled back";
|
|
138
|
-
console.log(
|
|
139
|
-
`${label}: ${relative(target, item.proposal.file)} line ${item.proposal.line}. ${item.reason}`
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
36
|
+
if (options.command === "rules") {
|
|
37
|
+
return listRules();
|
|
144
38
|
}
|
|
145
39
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
(item) => typeof item.backupPath === "string"
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
if (canAskQuestions() && hasRunBackups) {
|
|
152
|
-
const shouldDelete = await askYesNo(
|
|
153
|
-
"Do you want to delete the backup files created during this run?"
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
if (shouldDelete) {
|
|
157
|
-
const result = await deleteRunBackups(fixResults);
|
|
158
|
-
backupCleanup = {
|
|
159
|
-
requested: true,
|
|
160
|
-
deleted: result.deleted,
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
console.log(
|
|
164
|
-
`Deleted ${result.deleted} backup file(s) created during this run.`
|
|
165
|
-
);
|
|
166
|
-
} else {
|
|
167
|
-
backupCleanup = {
|
|
168
|
-
requested: false,
|
|
169
|
-
deleted: 0,
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
console.log("Kept the backup files created during this run.");
|
|
173
|
-
}
|
|
40
|
+
if (options.command === "explain") {
|
|
41
|
+
return explainRule(options.ruleName);
|
|
174
42
|
}
|
|
175
43
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (canAskQuestions()) {
|
|
180
|
-
const shouldCreateReport = await askYesNo(
|
|
181
|
-
"Would you like to create a detailed Markdown report of this run?"
|
|
182
|
-
);
|
|
183
|
-
|
|
184
|
-
if (shouldCreateReport) {
|
|
185
|
-
// Include the log messages generated before report creation.
|
|
186
|
-
const reportLogs = [...logs];
|
|
187
|
-
const finishedAt = new Date();
|
|
188
|
-
|
|
189
|
-
const result = await writeRunReport({
|
|
190
|
-
root: target,
|
|
191
|
-
target,
|
|
192
|
-
startedAt,
|
|
193
|
-
finishedAt,
|
|
194
|
-
filesScanned: files.length,
|
|
195
|
-
issues,
|
|
196
|
-
proposals,
|
|
197
|
-
fixResults,
|
|
198
|
-
logs: reportLogs,
|
|
199
|
-
backupCleanup,
|
|
200
|
-
});
|
|
44
|
+
if (options.command === "baseline") {
|
|
45
|
+
return runBaseline(options);
|
|
46
|
+
}
|
|
201
47
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
48
|
+
if (options.command === "hook") {
|
|
49
|
+
return runHook(options);
|
|
50
|
+
}
|
|
205
51
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
);
|
|
209
|
-
} else {
|
|
210
|
-
console.log("Report generation skipped.");
|
|
211
|
-
}
|
|
52
|
+
if (options.watch) {
|
|
53
|
+
return runWatch(options);
|
|
212
54
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
process.exitCode = 1;
|
|
55
|
+
|
|
56
|
+
return runScan(options);
|
|
216
57
|
}
|
|
58
|
+
|
|
59
|
+
process.exitCode = await main();
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { resolve, relative } from "node:path";
|
|
2
|
+
import { loadConfig } from "../../src/core/config.js";
|
|
3
|
+
import { EXIT } from "../../src/core/exit-codes.js";
|
|
4
|
+
import { BASELINE_NAME, buildBaseline, saveBaseline } from "../../src/scan/baseline.js";
|
|
5
|
+
import { selectFiles, collectIssues, disabledSetFor } from "./shared.js";
|
|
6
|
+
|
|
7
|
+
export async function runBaseline(options) {
|
|
8
|
+
const target = resolve(options.path ?? ".");
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const config = await loadConfig(target);
|
|
12
|
+
const disabled = disabledSetFor(config, options);
|
|
13
|
+
const files = await selectFiles(target, options);
|
|
14
|
+
const { issues } = await collectIssues(files, target, disabled, config, null);
|
|
15
|
+
|
|
16
|
+
const baseline = buildBaseline(issues, target);
|
|
17
|
+
const path = await saveBaseline(target, baseline);
|
|
18
|
+
const count = issues.length;
|
|
19
|
+
|
|
20
|
+
console.log(`Saved ${count} current issue(s) to ${relative(process.cwd(), path) || BASELINE_NAME}`);
|
|
21
|
+
console.log("Only new issues will be reported from now on.");
|
|
22
|
+
return EXIT.CLEAN;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error(`Error: ${error.message}`);
|
|
25
|
+
return EXIT.ERROR;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { readFile, writeFile, rm, mkdir, chmod, stat } from "node:fs/promises";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { EXIT } from "../../src/core/exit-codes.js";
|
|
4
|
+
|
|
5
|
+
const MARKER = "# vhite-pre-commit-hook";
|
|
6
|
+
const HOOK_SCRIPT = `#!/bin/sh
|
|
7
|
+
${MARKER}
|
|
8
|
+
# Scans staged files before each commit. Remove with: vhite hook remove
|
|
9
|
+
npx --yes @vhite/vhite --staged --no-prompt
|
|
10
|
+
`;
|
|
11
|
+
|
|
12
|
+
async function gitDirFor(root) {
|
|
13
|
+
try {
|
|
14
|
+
const info = await stat(join(root, ".git"));
|
|
15
|
+
return info.isDirectory() ? join(root, ".git") : null;
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function runHook(options) {
|
|
22
|
+
const root = resolve(options.path ?? ".");
|
|
23
|
+
const gitDir = await gitDirFor(root);
|
|
24
|
+
|
|
25
|
+
if (!gitDir) {
|
|
26
|
+
console.error("Error: this folder is not a Git repository (no .git folder found)");
|
|
27
|
+
return EXIT.ERROR;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const hookPath = join(gitDir, "hooks", "pre-commit");
|
|
31
|
+
const existing = await readFile(hookPath, "utf8").catch(() => null);
|
|
32
|
+
const isOurs = existing !== null && existing.includes(MARKER);
|
|
33
|
+
|
|
34
|
+
if (options.hookAction === "install") {
|
|
35
|
+
if (existing !== null && !isOurs && !options.force) {
|
|
36
|
+
console.error("Error: a pre-commit hook already exists. Use --force to replace it.");
|
|
37
|
+
return EXIT.ERROR;
|
|
38
|
+
}
|
|
39
|
+
await mkdir(join(gitDir, "hooks"), { recursive: true });
|
|
40
|
+
await writeFile(hookPath, HOOK_SCRIPT, "utf8");
|
|
41
|
+
await chmod(hookPath, 0o755).catch(() => {});
|
|
42
|
+
console.log(`Installed pre-commit hook at ${hookPath}`);
|
|
43
|
+
return EXIT.CLEAN;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (existing === null) {
|
|
47
|
+
console.log("No pre-commit hook to remove.");
|
|
48
|
+
return EXIT.CLEAN;
|
|
49
|
+
}
|
|
50
|
+
if (!isOurs) {
|
|
51
|
+
console.error("Error: the existing pre-commit hook was not installed by vhite, so it was left alone.");
|
|
52
|
+
return EXIT.ERROR;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
await rm(hookPath);
|
|
56
|
+
console.log("Removed the vhite pre-commit hook.");
|
|
57
|
+
return EXIT.CLEAN;
|
|
58
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { severityOf } from "../../src/core/config.js";
|
|
2
|
+
import { EXIT } from "../../src/core/exit-codes.js";
|
|
3
|
+
import { fixers } from "../../src/fix/fixers/index.js";
|
|
4
|
+
import { explanations } from "../../src/output/explanations.js";
|
|
5
|
+
import { rules } from "../../rules/index.js";
|
|
6
|
+
import { KNOWN_RULES } from "./shared.js";
|
|
7
|
+
|
|
8
|
+
export function listRules() {
|
|
9
|
+
console.log("Rule Severity Fix Description");
|
|
10
|
+
for (const rule of rules) {
|
|
11
|
+
const severity = severityOf(rule.name, { severities: new Map() });
|
|
12
|
+
const fixable = fixers[rule.name] ? "yes" : "no";
|
|
13
|
+
console.log(
|
|
14
|
+
`${rule.name.padEnd(22)}${severity.padEnd(10)}${fixable.padEnd(6)}${rule.description}`
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
return EXIT.CLEAN;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function explainRule(name) {
|
|
21
|
+
if (!KNOWN_RULES.has(name)) {
|
|
22
|
+
console.error(`Error: unknown rule "${name}"`);
|
|
23
|
+
console.error(`Known rules: ${[...KNOWN_RULES].join(", ")}`);
|
|
24
|
+
return EXIT.ERROR;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const rule = rules.find((item) => item.name === name);
|
|
28
|
+
const info = explanations[name];
|
|
29
|
+
|
|
30
|
+
console.log(name);
|
|
31
|
+
console.log(` ${rule.description}`);
|
|
32
|
+
if (info) {
|
|
33
|
+
console.log(` What happened: ${info.whatHappened}`);
|
|
34
|
+
console.log(` Proposed change: ${info.proposedChange}`);
|
|
35
|
+
}
|
|
36
|
+
console.log(` Default severity: ${severityOf(name, { severities: new Map() })}`);
|
|
37
|
+
console.log(` Automatic fix: ${fixers[name] ? "yes" : "no"}`);
|
|
38
|
+
return EXIT.CLEAN;
|
|
39
|
+
}
|