semlint-cli 0.1.9 → 0.1.10
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 +27 -1
- package/dist/config.js +23 -0
- package/dist/filter.js +26 -0
- package/dist/filter.test.js +42 -0
- package/dist/git.js +37 -12
- package/dist/git.test.js +24 -0
- package/dist/init.js +5 -0
- package/dist/main.js +4 -2
- package/dist/rules.js +4 -4
- package/dist/secrets.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,7 +57,6 @@ pnpm check
|
|
|
57
57
|
|
|
58
58
|
Default diff behavior (without `--base`/`--head`) uses your local branch state:
|
|
59
59
|
|
|
60
|
-
- tracked changes across commits since merge-base,
|
|
61
60
|
- staged changes,
|
|
62
61
|
- unstaged changes,
|
|
63
62
|
- untracked files.
|
|
@@ -67,6 +66,8 @@ If you pass `--base` or `--head`, Semlint uses explicit `git diff <base> <head>`
|
|
|
67
66
|
Before backend execution, Semlint shows the included and excluded diff files and asks for confirmation by default.
|
|
68
67
|
Use `--yes` / `-y` to skip this prompt.
|
|
69
68
|
|
|
69
|
+
You can configure local diff selection and path filtering via `diff.file_kinds`, `diff.include_globs`, and `diff.exclude_globs` in `semlint.json`.
|
|
70
|
+
|
|
70
71
|
## Exit codes
|
|
71
72
|
|
|
72
73
|
- `0`: no blocking diagnostics
|
|
@@ -96,6 +97,11 @@ Unknown fields are ignored.
|
|
|
96
97
|
"execution": {
|
|
97
98
|
"batch": false
|
|
98
99
|
},
|
|
100
|
+
"diff": {
|
|
101
|
+
"file_kinds": ["staged", "unstaged", "untracked"],
|
|
102
|
+
"include_globs": [],
|
|
103
|
+
"exclude_globs": []
|
|
104
|
+
},
|
|
99
105
|
"security": {
|
|
100
106
|
"secret_guard": true,
|
|
101
107
|
"allow_patterns": [],
|
|
@@ -118,6 +124,26 @@ Unknown fields are ignored.
|
|
|
118
124
|
}
|
|
119
125
|
```
|
|
120
126
|
|
|
127
|
+
## Diff management
|
|
128
|
+
|
|
129
|
+
Use the `diff` section to control which local change kinds and file paths are included:
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"diff": {
|
|
134
|
+
"file_kinds": ["staged", "unstaged", "untracked"],
|
|
135
|
+
"include_globs": ["src/**/*.ts"],
|
|
136
|
+
"exclude_globs": ["**/*.test.ts"]
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
- `file_kinds`: local diff sources included by default (`staged`, `unstaged`, `untracked`)
|
|
142
|
+
- `include_globs`: optional allowlist of changed files to keep
|
|
143
|
+
- `exclude_globs`: optional denylist applied after include globs (exclude wins)
|
|
144
|
+
|
|
145
|
+
`diff.*` settings are applied before security ignore filtering and secret scanning.
|
|
146
|
+
|
|
121
147
|
## Config scaffolding and auto-detection
|
|
122
148
|
|
|
123
149
|
Run:
|
package/dist/config.js
CHANGED
|
@@ -8,6 +8,7 @@ exports.loadEffectiveConfig = loadEffectiveConfig;
|
|
|
8
8
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
10
10
|
const utils_1 = require("./utils");
|
|
11
|
+
const VALID_DIFF_FILE_KINDS = new Set(["staged", "unstaged", "untracked"]);
|
|
11
12
|
const DEFAULTS = {
|
|
12
13
|
backend: "cursor-cli",
|
|
13
14
|
model: "auto",
|
|
@@ -18,6 +19,11 @@ const DEFAULTS = {
|
|
|
18
19
|
head: "HEAD",
|
|
19
20
|
debug: false,
|
|
20
21
|
batchMode: false,
|
|
22
|
+
diff: {
|
|
23
|
+
fileKinds: ["staged", "unstaged", "untracked"],
|
|
24
|
+
includeGlobs: [],
|
|
25
|
+
excludeGlobs: []
|
|
26
|
+
},
|
|
21
27
|
rulesDisable: [],
|
|
22
28
|
severityOverrides: {},
|
|
23
29
|
rulesIncludeGlobs: [],
|
|
@@ -146,6 +152,18 @@ function sanitizeGlobList(value) {
|
|
|
146
152
|
return trimmed === "" ? [] : [trimmed];
|
|
147
153
|
});
|
|
148
154
|
}
|
|
155
|
+
function sanitizeDiffFileKinds(value) {
|
|
156
|
+
if (!Array.isArray(value)) {
|
|
157
|
+
return [...DEFAULTS.diff.fileKinds];
|
|
158
|
+
}
|
|
159
|
+
return value.flatMap((candidate) => {
|
|
160
|
+
if (typeof candidate !== "string") {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
const normalized = candidate.trim();
|
|
164
|
+
return VALID_DIFF_FILE_KINDS.has(normalized) ? [normalized] : [];
|
|
165
|
+
});
|
|
166
|
+
}
|
|
149
167
|
function ensureSelectedBackendIsConfigured(backend, backendConfigs) {
|
|
150
168
|
if (!(backend in backendConfigs)) {
|
|
151
169
|
throw new Error(`Backend "${backend}" is not configured. Add it under backends.${backend} with executable and args (including "{prompt}") in semlint.json.`);
|
|
@@ -174,6 +192,11 @@ function loadEffectiveConfig(options) {
|
|
|
174
192
|
(typeof fileConfig.execution?.batch === "boolean"
|
|
175
193
|
? fileConfig.execution.batch
|
|
176
194
|
: DEFAULTS.batchMode),
|
|
195
|
+
diff: {
|
|
196
|
+
fileKinds: sanitizeDiffFileKinds(fileConfig.diff?.file_kinds),
|
|
197
|
+
includeGlobs: sanitizeGlobList(fileConfig.diff?.include_globs),
|
|
198
|
+
excludeGlobs: sanitizeGlobList(fileConfig.diff?.exclude_globs)
|
|
199
|
+
},
|
|
177
200
|
rulesDisable: Array.isArray(fileConfig.rules?.disable)
|
|
178
201
|
? fileConfig.rules?.disable.filter((item) => typeof item === "string")
|
|
179
202
|
: DEFAULTS.rulesDisable,
|
package/dist/filter.js
CHANGED
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.extractChangedFilesFromDiff = extractChangedFilesFromDiff;
|
|
7
|
+
exports.filterDiffByPathGlobs = filterDiffByPathGlobs;
|
|
7
8
|
exports.getRuleCandidateFiles = getRuleCandidateFiles;
|
|
8
9
|
exports.shouldRunRule = shouldRunRule;
|
|
9
10
|
exports.buildScopedDiff = buildScopedDiff;
|
|
@@ -29,6 +30,31 @@ function extractChangedFilesFromDiff(diff) {
|
|
|
29
30
|
}
|
|
30
31
|
return Array.from(files);
|
|
31
32
|
}
|
|
33
|
+
function filterDiffByPathGlobs(diff, includeGlobs = [], excludeGlobs = []) {
|
|
34
|
+
const chunks = (0, diff_1.splitDiffIntoFileChunks)(diff);
|
|
35
|
+
const includeMatcher = includeGlobs.length > 0 ? (0, picomatch_1.default)(includeGlobs) : null;
|
|
36
|
+
const excludeMatcher = excludeGlobs.length > 0 ? (0, picomatch_1.default)(excludeGlobs) : null;
|
|
37
|
+
const excludedFiles = [];
|
|
38
|
+
const filteredDiff = chunks
|
|
39
|
+
.filter((chunk) => {
|
|
40
|
+
if (chunk.file === "") {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
const included = includeMatcher ? includeMatcher(chunk.file) : true;
|
|
44
|
+
const excluded = excludeMatcher ? excludeMatcher(chunk.file) : false;
|
|
45
|
+
const keep = included && !excluded;
|
|
46
|
+
if (!keep) {
|
|
47
|
+
excludedFiles.push(chunk.file);
|
|
48
|
+
}
|
|
49
|
+
return keep;
|
|
50
|
+
})
|
|
51
|
+
.map((chunk) => chunk.chunk)
|
|
52
|
+
.join("\n");
|
|
53
|
+
return {
|
|
54
|
+
filteredDiff,
|
|
55
|
+
excludedFiles: Array.from(new Set(excludedFiles)).sort((a, b) => a.localeCompare(b))
|
|
56
|
+
};
|
|
57
|
+
}
|
|
32
58
|
function matchesAnyRegex(diff, regexes) {
|
|
33
59
|
for (const candidate of regexes) {
|
|
34
60
|
try {
|
package/dist/filter.test.js
CHANGED
|
@@ -40,3 +40,45 @@ function makeRule(overrides = {}) {
|
|
|
40
40
|
const candidates = (0, filter_1.getRuleCandidateFiles)(rule, changedFiles, ["src/**/*.ts"], []);
|
|
41
41
|
strict_1.default.deepEqual(candidates, ["docs/readme.md"]);
|
|
42
42
|
});
|
|
43
|
+
(0, node_test_1.default)("filterDiffByPathGlobs applies include and exclude globs", () => {
|
|
44
|
+
const diff = [
|
|
45
|
+
"diff --git a/src/a.ts b/src/a.ts",
|
|
46
|
+
"--- a/src/a.ts",
|
|
47
|
+
"+++ b/src/a.ts",
|
|
48
|
+
"@@ -0,0 +1 @@",
|
|
49
|
+
"+const a = 1;",
|
|
50
|
+
"diff --git a/src/a.test.ts b/src/a.test.ts",
|
|
51
|
+
"--- a/src/a.test.ts",
|
|
52
|
+
"+++ b/src/a.test.ts",
|
|
53
|
+
"@@ -0,0 +1 @@",
|
|
54
|
+
"+const test = 1;",
|
|
55
|
+
"diff --git a/docs/readme.md b/docs/readme.md",
|
|
56
|
+
"--- a/docs/readme.md",
|
|
57
|
+
"+++ b/docs/readme.md",
|
|
58
|
+
"@@ -0,0 +1 @@",
|
|
59
|
+
"+# docs"
|
|
60
|
+
].join("\n");
|
|
61
|
+
const result = (0, filter_1.filterDiffByPathGlobs)(diff, ["src/**/*.ts"], ["**/*.test.ts"]);
|
|
62
|
+
strict_1.default.match(result.filteredDiff, /src\/a\.ts/);
|
|
63
|
+
strict_1.default.doesNotMatch(result.filteredDiff, /src\/a\.test\.ts/);
|
|
64
|
+
strict_1.default.doesNotMatch(result.filteredDiff, /docs\/readme\.md/);
|
|
65
|
+
strict_1.default.deepEqual(result.excludedFiles, ["docs/readme.md", "src/a.test.ts"]);
|
|
66
|
+
});
|
|
67
|
+
(0, node_test_1.default)("filterDiffByPathGlobs keeps all files when include/exclude are empty", () => {
|
|
68
|
+
const diff = [
|
|
69
|
+
"diff --git a/src/a.ts b/src/a.ts",
|
|
70
|
+
"--- a/src/a.ts",
|
|
71
|
+
"+++ b/src/a.ts",
|
|
72
|
+
"@@ -0,0 +1 @@",
|
|
73
|
+
"+const a = 1;",
|
|
74
|
+
"diff --git a/docs/readme.md b/docs/readme.md",
|
|
75
|
+
"--- a/docs/readme.md",
|
|
76
|
+
"+++ b/docs/readme.md",
|
|
77
|
+
"@@ -0,0 +1 @@",
|
|
78
|
+
"+# docs"
|
|
79
|
+
].join("\n");
|
|
80
|
+
const result = (0, filter_1.filterDiffByPathGlobs)(diff, [], []);
|
|
81
|
+
strict_1.default.match(result.filteredDiff, /src\/a\.ts/);
|
|
82
|
+
strict_1.default.match(result.filteredDiff, /docs\/readme\.md/);
|
|
83
|
+
strict_1.default.deepEqual(result.excludedFiles, []);
|
|
84
|
+
});
|
package/dist/git.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.getGitDiff = getGitDiff;
|
|
4
4
|
exports.getRepoRoot = getRepoRoot;
|
|
5
5
|
exports.getLocalBranchDiff = getLocalBranchDiff;
|
|
6
|
+
exports.selectLocalBranchDiffChunks = selectLocalBranchDiffChunks;
|
|
6
7
|
const node_child_process_1 = require("node:child_process");
|
|
7
8
|
const node_os_1 = require("node:os");
|
|
8
9
|
function runGitCommand(args, okExitCodes = [0]) {
|
|
@@ -69,18 +70,42 @@ async function getNoIndexDiffForFile(filePath) {
|
|
|
69
70
|
* - Untracked files (as full-file diffs)
|
|
70
71
|
* Does not include already-committed changes on the branch.
|
|
71
72
|
*/
|
|
72
|
-
async function getLocalBranchDiff() {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
73
|
+
async function getLocalBranchDiff(fileKinds = ["staged", "unstaged", "untracked"]) {
|
|
74
|
+
const parts = {
|
|
75
|
+
stagedDiff: "",
|
|
76
|
+
unstagedDiff: "",
|
|
77
|
+
untrackedDiffs: []
|
|
78
|
+
};
|
|
79
|
+
if (fileKinds.includes("staged")) {
|
|
80
|
+
const stagedResult = await runGitCommand(["diff", "--cached"]);
|
|
81
|
+
parts.stagedDiff = stagedResult.stdout;
|
|
82
|
+
}
|
|
83
|
+
if (fileKinds.includes("unstaged")) {
|
|
84
|
+
const unstagedResult = await runGitCommand(["diff"]);
|
|
85
|
+
parts.unstagedDiff = unstagedResult.stdout;
|
|
86
|
+
}
|
|
87
|
+
if (fileKinds.includes("untracked")) {
|
|
88
|
+
const untrackedFiles = await getUntrackedFiles();
|
|
89
|
+
for (const filePath of untrackedFiles) {
|
|
90
|
+
const fileDiff = await getNoIndexDiffForFile(filePath);
|
|
91
|
+
if (fileDiff.trim() !== "") {
|
|
92
|
+
parts.untrackedDiffs.push(fileDiff);
|
|
93
|
+
}
|
|
81
94
|
}
|
|
82
95
|
}
|
|
83
|
-
return
|
|
84
|
-
|
|
85
|
-
|
|
96
|
+
return selectLocalBranchDiffChunks(parts, fileKinds);
|
|
97
|
+
}
|
|
98
|
+
function selectLocalBranchDiffChunks(parts, fileKinds = ["staged", "unstaged", "untracked"]) {
|
|
99
|
+
const selectedKinds = new Set(fileKinds);
|
|
100
|
+
const chunks = [];
|
|
101
|
+
if (selectedKinds.has("staged") && parts.stagedDiff !== "") {
|
|
102
|
+
chunks.push(parts.stagedDiff);
|
|
103
|
+
}
|
|
104
|
+
if (selectedKinds.has("unstaged") && parts.unstagedDiff !== "") {
|
|
105
|
+
chunks.push(parts.unstagedDiff);
|
|
106
|
+
}
|
|
107
|
+
if (selectedKinds.has("untracked")) {
|
|
108
|
+
chunks.push(...parts.untrackedDiffs.filter((chunk) => chunk !== ""));
|
|
109
|
+
}
|
|
110
|
+
return chunks.join("\n");
|
|
86
111
|
}
|
package/dist/git.test.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
7
|
+
const node_test_1 = __importDefault(require("node:test"));
|
|
8
|
+
const git_1 = require("./git");
|
|
9
|
+
(0, node_test_1.default)("selectLocalBranchDiffChunks includes only selected change kinds", () => {
|
|
10
|
+
const combined = (0, git_1.selectLocalBranchDiffChunks)({
|
|
11
|
+
stagedDiff: "STAGED_DIFF",
|
|
12
|
+
unstagedDiff: "UNSTAGED_DIFF",
|
|
13
|
+
untrackedDiffs: ["UNTRACKED_ONE", "", "UNTRACKED_TWO"]
|
|
14
|
+
}, ["staged", "untracked"]);
|
|
15
|
+
strict_1.default.equal(combined, ["STAGED_DIFF", "UNTRACKED_ONE", "UNTRACKED_TWO"].join("\n"));
|
|
16
|
+
});
|
|
17
|
+
(0, node_test_1.default)("selectLocalBranchDiffChunks returns empty string for empty selection", () => {
|
|
18
|
+
const combined = (0, git_1.selectLocalBranchDiffChunks)({
|
|
19
|
+
stagedDiff: "STAGED_DIFF",
|
|
20
|
+
unstagedDiff: "UNSTAGED_DIFF",
|
|
21
|
+
untrackedDiffs: ["UNTRACKED_ONE"]
|
|
22
|
+
}, []);
|
|
23
|
+
strict_1.default.equal(combined, "");
|
|
24
|
+
});
|
package/dist/init.js
CHANGED
|
@@ -87,6 +87,11 @@ function scaffoldConfig(force = false) {
|
|
|
87
87
|
execution: {
|
|
88
88
|
batch: false
|
|
89
89
|
},
|
|
90
|
+
diff: {
|
|
91
|
+
file_kinds: ["staged", "unstaged", "untracked"],
|
|
92
|
+
include_globs: ["src/**"],
|
|
93
|
+
exclude_globs: []
|
|
94
|
+
},
|
|
90
95
|
security: {
|
|
91
96
|
secret_guard: true,
|
|
92
97
|
allow_patterns: [],
|
package/dist/main.js
CHANGED
|
@@ -103,8 +103,10 @@ async function runSemlint(options) {
|
|
|
103
103
|
const repoRoot = await timedAsync(config.debug, "Resolved git repo root", () => (0, git_1.getRepoRoot)());
|
|
104
104
|
const scanRoot = repoRoot ?? process.cwd();
|
|
105
105
|
(0, utils_1.debugLog)(config.debug, `Using diff/ignore scan root: ${scanRoot}`);
|
|
106
|
-
const rawDiff = await timedAsync(config.debug, "Computed git diff", () => useLocalBranchDiff ? (0, git_1.getLocalBranchDiff)() : (0, git_1.getGitDiff)(config.base, config.head));
|
|
107
|
-
const { filteredDiff:
|
|
106
|
+
const rawDiff = await timedAsync(config.debug, "Computed git diff", () => useLocalBranchDiff ? (0, git_1.getLocalBranchDiff)(config.diff.fileKinds) : (0, git_1.getGitDiff)(config.base, config.head));
|
|
107
|
+
const { filteredDiff: globFilteredDiff, excludedFiles: globExcludedFiles } = timed(config.debug, "Filtered diff by configured include/exclude globs", () => (0, filter_1.filterDiffByPathGlobs)(rawDiff, config.diff.includeGlobs, config.diff.excludeGlobs));
|
|
108
|
+
const { filteredDiff: diff, excludedFiles: ignoreExcludedFiles } = timed(config.debug, "Filtered diff by ignore rules", () => (0, secrets_1.filterDiffByIgnoreRules)(globFilteredDiff, scanRoot, config.security.ignoreFiles));
|
|
109
|
+
const excludedFiles = Array.from(new Set([...globExcludedFiles, ...ignoreExcludedFiles])).sort((a, b) => a.localeCompare(b));
|
|
108
110
|
if (excludedFiles.length > 0) {
|
|
109
111
|
(0, utils_1.debugLog)(config.debug, `Excluded ${excludedFiles.length} file(s) by ignore/security rules: ${excludedFiles.join(", ")}`);
|
|
110
112
|
}
|
package/dist/rules.js
CHANGED
|
@@ -13,7 +13,7 @@ function assertNonEmptyString(value, fieldName, filePath) {
|
|
|
13
13
|
}
|
|
14
14
|
return value;
|
|
15
15
|
}
|
|
16
|
-
function
|
|
16
|
+
function assert_string_array(value, fieldName, filePath) {
|
|
17
17
|
if (value === undefined) {
|
|
18
18
|
return undefined;
|
|
19
19
|
}
|
|
@@ -36,9 +36,9 @@ function validateRuleObject(raw, filePath) {
|
|
|
36
36
|
title: assertNonEmptyString(obj.title, "title", filePath),
|
|
37
37
|
severity_default: severity,
|
|
38
38
|
prompt: assertNonEmptyString(obj.prompt, "prompt", filePath),
|
|
39
|
-
include_globs:
|
|
40
|
-
exclude_globs:
|
|
41
|
-
diff_regex:
|
|
39
|
+
include_globs: assert_string_array(obj.include_globs, "include_globs", filePath),
|
|
40
|
+
exclude_globs: assert_string_array(obj.exclude_globs, "exclude_globs", filePath),
|
|
41
|
+
diff_regex: assert_string_array(obj.diff_regex, "diff_regex", filePath)
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
function loadRules(rulesDir, disabledRuleIds, severityOverrides) {
|
package/dist/secrets.js
CHANGED
|
@@ -93,7 +93,8 @@ function parseAllowMatchers(patterns) {
|
|
|
93
93
|
});
|
|
94
94
|
}
|
|
95
95
|
function filterDiffByIgnoreRules(diff, cwd, ignoreFiles) {
|
|
96
|
-
|
|
96
|
+
// Keep built-ins first so repo-specific negation patterns can carve out safe exceptions.
|
|
97
|
+
const ignorePatterns = [...BUILTIN_SENSITIVE_GLOBS, ...readIgnorePatterns(cwd, ignoreFiles)];
|
|
97
98
|
const ignoreMatcher = createIgnoreMatcher(ignorePatterns);
|
|
98
99
|
const chunks = (0, diff_1.splitDiffIntoFileChunks)(diff);
|
|
99
100
|
const excludedFiles = chunks
|