semlint-cli 0.1.7 → 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.
@@ -0,0 +1,8 @@
1
+ {
2
+ "id": "SEMLINT_COUPLING_004",
3
+ "title": "Check for unwanted hidden coupling",
4
+ "severity_default": "warn",
5
+ "include_globs": [],
6
+ "exclude_globs": [],
7
+ "prompt": "Flag any coupling that is not obvious at first glance. (hidden/implicit coupling, and forms of connascence)"
8
+ }
@@ -2,11 +2,7 @@
2
2
  "id": "SEMLINT_NAMING_001",
3
3
  "title": "Ambient naming convention consistency",
4
4
  "severity_default": "warn",
5
- "include_globs": ["src/**/*.ts"],
6
- "exclude_globs": ["**/*.test.ts", "**/*.spec.ts"],
7
- "diff_regex": [
8
- "^[+-].*\\b(const|let|var|function|class|interface|type|enum)\\b",
9
- "^[+-].*\\b[A-Za-z_][A-Za-z0-9_]*\\b"
10
- ],
5
+ "include_globs": [],
6
+ "exclude_globs": [],
11
7
  "prompt": "Verify naming is consistent with the ambient naming conventions already used in surrounding code. Both in term of semantic and casing."
12
8
  }
@@ -2,11 +2,7 @@
2
2
  "id": "SEMLINT_PATTERN_002",
3
3
  "title": "Ambient pattern is respected",
4
4
  "severity_default": "warn",
5
- "include_globs": ["src/**/*.ts"],
6
- "exclude_globs": ["**/*.test.ts", "**/*.spec.ts"],
7
- "diff_regex": [
8
- "^[+-].*\\b(async|await|Promise|try|catch|throw|switch|map|filter|reduce|forEach)\\b",
9
- "^[+-].*\\b(import|export|class|interface|type|function|return)\\b"
10
- ],
11
- "prompt": "Check whether ambient implementation patterns are respected. Compare new or changed code against nearby established patterns. Flag clear regressions where the proposed change deviates from consistent local patterns without obvious justification."
5
+ "include_globs": [],
6
+ "exclude_globs": [],
7
+ "prompt": "Any change that breaks an established local pattern should be flagged by default. Consistency with adjacent implementations is the enforced invariant."
12
8
  }
@@ -2,11 +2,7 @@
2
2
  "id": "SEMLINT_SWE_003",
3
3
  "title": "Obvious SWE mistakes",
4
4
  "severity_default": "warn",
5
- "include_globs": ["src/**/*.ts"],
6
- "exclude_globs": ["**/*.test.ts", "**/*.spec.ts"],
7
- "diff_regex": [
8
- "^[+-].*\\b(any|as\\s+any|TODO|FIXME|console\\.log|@ts-ignore|throw\\s+new\\s+Error|catch\\s*\\()\\b",
9
- "^[+-].*\\b(if|else|switch|return|await|Promise|map|forEach|reduce)\\b"
10
- ],
5
+ "include_globs": [],
6
+ "exclude_globs": [],
11
7
  "prompt": "Find obvious software-engineering mistakes in the proposed change that are likely unintended. Do not nitpick style or architecture unless the issue is clearly harmful. Report only high-signal findings."
12
8
  }
package/README.md CHANGED
@@ -51,18 +51,23 @@ pnpm check
51
51
  - `--head <ref>`: head git ref for explicit ref-to-ref diff
52
52
  - `--fail-on <error|warn|never>`: failure threshold (default `error`)
53
53
  - `--batch`: run all selected rules in one backend call
54
+ - `--yes` / `-y`: auto-accept diff file confirmation (useful for CI)
54
55
  - `--debug`: enable debug logs to stderr
55
56
  - `init --force`: overwrite an existing `semlint.json`
56
57
 
57
58
  Default diff behavior (without `--base`/`--head`) uses your local branch state:
58
59
 
59
- - tracked changes across commits since merge-base,
60
60
  - staged changes,
61
61
  - unstaged changes,
62
62
  - untracked files.
63
63
 
64
64
  If you pass `--base` or `--head`, Semlint uses explicit `git diff <base> <head>` mode.
65
65
 
66
+ Before backend execution, Semlint shows the included and excluded diff files and asks for confirmation by default.
67
+ Use `--yes` / `-y` to skip this prompt.
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
+
66
71
  ## Exit codes
67
72
 
68
73
  - `0`: no blocking diagnostics
@@ -92,6 +97,11 @@ Unknown fields are ignored.
92
97
  "execution": {
93
98
  "batch": false
94
99
  },
100
+ "diff": {
101
+ "file_kinds": ["staged", "unstaged", "untracked"],
102
+ "include_globs": [],
103
+ "exclude_globs": []
104
+ },
95
105
  "security": {
96
106
  "secret_guard": true,
97
107
  "allow_patterns": [],
@@ -114,6 +124,26 @@ Unknown fields are ignored.
114
124
  }
115
125
  ```
116
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
+
117
147
  ## Config scaffolding and auto-detection
118
148
 
119
149
  Run:
@@ -228,6 +258,16 @@ Config:
228
258
  - `allow_files`: file glob allowlist to skip secret scanning for known-safe files (example: `["src/test-fixtures/**"]`)
229
259
  - `ignore_files`: ignore files Semlint reads for path-level filtering (default: `.gitignore`, `.cursorignore`, `.semlintignore`, `.cursoringore`)
230
260
 
261
+ Ignore file patterns are evaluated with standard gitignore semantics (including basename matching and `!` negation).
262
+
263
+ ## Security responsibility model
264
+
265
+ Security in Semlint is shared, and the repository owner remains responsible for secure setup:
266
+
267
+ - **Diff-level (your responsibility):** carefully configure ignore files and Semlint security settings so sensitive files/content never enter the analyzed diff.
268
+ - **Semlint guard (best-effort control):** Semlint filters diff paths and scans added lines, but this is not a complete prevention system.
269
+ - **Agent/runtime level (handled by your backend tool):** after Semlint sends a prompt to your configured CLI, what the agent can do is controlled by the native agent/backend configuration.
270
+
231
271
  ## Prompt files
232
272
 
233
273
  Core system prompts are externalized under `prompts/` so prompt behavior is easy to inspect and iterate:
package/dist/cli.js CHANGED
@@ -7,18 +7,22 @@ Object.defineProperty(exports, "__esModule", { value: true });
7
7
  const picocolors_1 = __importDefault(require("picocolors"));
8
8
  const init_1 = require("./init");
9
9
  const main_1 = require("./main");
10
+ const security_1 = require("./security");
10
11
  const HELP_TEXT = [
11
12
  "Usage:",
12
- " semlint check [--backend <name>] [--model <name>] [--config <path>] [--format <text|json>] [--base <ref>] [--head <ref>] [--fail-on <error|warn|never>] [--batch] [--debug]",
13
- " semlint init [--force]",
14
- " semlint --help",
13
+ " semlint-cli check [--backend <name>] [--model <name>] [--config <path>] [--format <text|json>] [--base <ref>] [--head <ref>] [--fail-on <error|warn|never>] [--batch] [--yes|-y] [--debug]",
14
+ " semlint-cli init [--force]",
15
+ " semlint-cli security",
16
+ " semlint-cli --help",
15
17
  "",
16
18
  "Commands:",
17
19
  " check Run semantic lint rules against your git diff",
18
20
  " init Create semlint.json, .semlint/rules/, and an example rule to edit",
21
+ " security Show security responsibility guidance",
19
22
  "",
20
23
  "Options:",
21
- " -h, --help Show this help text"
24
+ " -h, --help Show this help text",
25
+ " -y, --yes Auto-accept diff file confirmation"
22
26
  ].join("\n");
23
27
  class HelpRequestedError extends Error {
24
28
  constructor() {
@@ -45,7 +49,7 @@ function parseArgs(argv) {
45
49
  throw new HelpRequestedError();
46
50
  }
47
51
  const [command, ...rest] = argv;
48
- if (!command || (command !== "check" && command !== "init")) {
52
+ if (!command || (command !== "check" && command !== "init" && command !== "security")) {
49
53
  throw new Error(HELP_TEXT);
50
54
  }
51
55
  if (command === "init") {
@@ -62,12 +66,25 @@ function parseArgs(argv) {
62
66
  }
63
67
  return options;
64
68
  }
69
+ if (command === "security") {
70
+ if (rest.length > 0) {
71
+ throw new Error(`Unknown flag for security: ${rest[0]}`);
72
+ }
73
+ return {
74
+ command: "security",
75
+ debug: false
76
+ };
77
+ }
65
78
  const options = {
66
79
  command: "check",
67
80
  debug: false
68
81
  };
69
82
  for (let i = 0; i < rest.length; i += 1) {
70
83
  const token = rest[i];
84
+ if (token === "--yes" || token === "-y") {
85
+ options.autoAccept = true;
86
+ continue;
87
+ }
71
88
  if (token === "--debug") {
72
89
  options.debug = true;
73
90
  continue;
@@ -121,7 +138,11 @@ function parseArgs(argv) {
121
138
  async function main() {
122
139
  try {
123
140
  const options = parseArgs(process.argv.slice(2));
124
- const exitCode = options.command === "init" ? (0, init_1.scaffoldConfig)(options.force) : await (0, main_1.runSemlint)(options);
141
+ const exitCode = options.command === "init"
142
+ ? (0, init_1.scaffoldConfig)(options.force)
143
+ : options.command === "security"
144
+ ? (0, security_1.printSecurityGuide)()
145
+ : await (0, main_1.runSemlint)(options);
125
146
  process.exitCode = exitCode;
126
147
  }
127
148
  catch (error) {
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,8 +19,15 @@ 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: {},
29
+ rulesIncludeGlobs: [],
30
+ rulesExcludeGlobs: [],
23
31
  backendConfigs: {},
24
32
  security: {
25
33
  secretGuard: true,
@@ -132,6 +140,30 @@ function sanitizeAllowFiles(value) {
132
140
  return trimmed === "" ? [] : [trimmed];
133
141
  });
134
142
  }
143
+ function sanitizeGlobList(value) {
144
+ if (!Array.isArray(value)) {
145
+ return [];
146
+ }
147
+ return value.flatMap((candidate) => {
148
+ if (typeof candidate !== "string") {
149
+ return [];
150
+ }
151
+ const trimmed = candidate.trim();
152
+ return trimmed === "" ? [] : [trimmed];
153
+ });
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
+ }
135
167
  function ensureSelectedBackendIsConfigured(backend, backendConfigs) {
136
168
  if (!(backend in backendConfigs)) {
137
169
  throw new Error(`Backend "${backend}" is not configured. Add it under backends.${backend} with executable and args (including "{prompt}") in semlint.json.`);
@@ -160,10 +192,17 @@ function loadEffectiveConfig(options) {
160
192
  (typeof fileConfig.execution?.batch === "boolean"
161
193
  ? fileConfig.execution.batch
162
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
+ },
163
200
  rulesDisable: Array.isArray(fileConfig.rules?.disable)
164
201
  ? fileConfig.rules?.disable.filter((item) => typeof item === "string")
165
202
  : DEFAULTS.rulesDisable,
166
203
  severityOverrides: sanitizeSeverityOverrides((fileConfig.rules?.severity_overrides ?? undefined)),
204
+ rulesIncludeGlobs: sanitizeGlobList(fileConfig.rules?.include_globs),
205
+ rulesExcludeGlobs: sanitizeGlobList(fileConfig.rules?.exclude_globs),
167
206
  backendConfigs,
168
207
  security: {
169
208
  secretGuard: typeof fileConfig.security?.secret_guard === "boolean"
package/dist/dispatch.js CHANGED
@@ -24,7 +24,7 @@ async function runBatchDispatch(input) {
24
24
  let backendErrors = 0;
25
25
  (0, utils_1.debugLog)(config.debug, `Running ${rules.length} rule(s) in batch mode`);
26
26
  const combinedDiff = rules
27
- .map((rule) => (0, filter_1.buildScopedDiff)(rule, diff, changedFiles))
27
+ .map((rule) => (0, filter_1.buildScopedDiff)(rule, diff, changedFiles, config.rulesIncludeGlobs, config.rulesExcludeGlobs))
28
28
  .filter((chunk) => chunk.trim() !== "")
29
29
  .join("\n");
30
30
  const batchPrompt = buildBatchPrompt(rules, combinedDiff || diff);
@@ -68,7 +68,7 @@ async function runParallelDispatch(input) {
68
68
  const runResults = await Promise.all(rules.map(async (rule) => {
69
69
  const ruleStartedAt = Date.now();
70
70
  (0, utils_1.debugLog)(config.debug, `Rule ${rule.id}: started`);
71
- const scopedDiff = (0, filter_1.buildScopedDiff)(rule, diff, changedFiles);
71
+ const scopedDiff = (0, filter_1.buildScopedDiff)(rule, diff, changedFiles, config.rulesIncludeGlobs, config.rulesExcludeGlobs);
72
72
  const prompt = (0, filter_1.buildRulePrompt)(rule, scopedDiff);
73
73
  try {
74
74
  const result = await backend.runRule({
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 {
@@ -43,10 +69,18 @@ function matchesAnyRegex(diff, regexes) {
43
69
  }
44
70
  return false;
45
71
  }
46
- function getRuleCandidateFiles(rule, changedFiles) {
72
+ function resolveRuleGlobs(ruleGlobs, globalGlobs) {
73
+ if (ruleGlobs === undefined) {
74
+ return globalGlobs;
75
+ }
76
+ return ruleGlobs;
77
+ }
78
+ function getRuleCandidateFiles(rule, changedFiles, globalIncludeGlobs = [], globalExcludeGlobs = []) {
47
79
  let fileCandidates = changedFiles;
48
- const includeMatcher = rule.include_globs && rule.include_globs.length > 0 ? (0, picomatch_1.default)(rule.include_globs) : null;
49
- const excludeMatcher = rule.exclude_globs && rule.exclude_globs.length > 0 ? (0, picomatch_1.default)(rule.exclude_globs) : null;
80
+ const effectiveIncludeGlobs = resolveRuleGlobs(rule.include_globs, globalIncludeGlobs);
81
+ const effectiveExcludeGlobs = resolveRuleGlobs(rule.exclude_globs, globalExcludeGlobs);
82
+ const includeMatcher = effectiveIncludeGlobs.length > 0 ? (0, picomatch_1.default)(effectiveIncludeGlobs) : null;
83
+ const excludeMatcher = effectiveExcludeGlobs.length > 0 ? (0, picomatch_1.default)(effectiveExcludeGlobs) : null;
50
84
  if (includeMatcher) {
51
85
  fileCandidates = changedFiles.filter((filePath) => includeMatcher(filePath));
52
86
  if (fileCandidates.length === 0) {
@@ -58,8 +92,8 @@ function getRuleCandidateFiles(rule, changedFiles) {
58
92
  }
59
93
  return fileCandidates;
60
94
  }
61
- function shouldRunRule(rule, changedFiles, diff) {
62
- const fileCandidates = getRuleCandidateFiles(rule, changedFiles);
95
+ function shouldRunRule(rule, changedFiles, diff, globalIncludeGlobs = [], globalExcludeGlobs = []) {
96
+ const fileCandidates = getRuleCandidateFiles(rule, changedFiles, globalIncludeGlobs, globalExcludeGlobs);
63
97
  if (fileCandidates.length === 0) {
64
98
  return false;
65
99
  }
@@ -68,8 +102,8 @@ function shouldRunRule(rule, changedFiles, diff) {
68
102
  }
69
103
  return true;
70
104
  }
71
- function buildScopedDiff(rule, fullDiff, changedFiles) {
72
- const candidateFiles = new Set(getRuleCandidateFiles(rule, changedFiles));
105
+ function buildScopedDiff(rule, fullDiff, changedFiles, globalIncludeGlobs = [], globalExcludeGlobs = []) {
106
+ const candidateFiles = new Set(getRuleCandidateFiles(rule, changedFiles, globalIncludeGlobs, globalExcludeGlobs));
73
107
  if (candidateFiles.size === 0) {
74
108
  return fullDiff;
75
109
  }
@@ -0,0 +1,84 @@
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 filter_1 = require("./filter");
9
+ function makeRule(overrides = {}) {
10
+ return {
11
+ id: "SEMLINT_TEST_001",
12
+ title: "Test rule",
13
+ severity_default: "warn",
14
+ prompt: "Test",
15
+ sourcePath: "/tmp/SEMLINT_TEST_001.json",
16
+ effectiveSeverity: "warn",
17
+ ...overrides
18
+ };
19
+ }
20
+ (0, node_test_1.default)("getRuleCandidateFiles inherits global include/exclude globs", () => {
21
+ const changedFiles = ["src/a.ts", "src/a.test.ts", "docs/readme.md"];
22
+ const rule = makeRule();
23
+ const candidates = (0, filter_1.getRuleCandidateFiles)(rule, changedFiles, ["src/**/*.ts"], ["**/*.test.ts", "**/*.spec.ts"]);
24
+ strict_1.default.deepEqual(candidates, ["src/a.ts"]);
25
+ });
26
+ (0, node_test_1.default)("getRuleCandidateFiles lets empty rule globs disable global filters", () => {
27
+ const changedFiles = ["src/a.ts", "src/a.test.ts", "docs/readme.md"];
28
+ const rule = makeRule({
29
+ include_globs: [],
30
+ exclude_globs: []
31
+ });
32
+ const candidates = (0, filter_1.getRuleCandidateFiles)(rule, changedFiles, ["src/**/*.ts"], ["**/*.test.ts", "**/*.spec.ts"]);
33
+ strict_1.default.deepEqual(candidates, changedFiles);
34
+ });
35
+ (0, node_test_1.default)("getRuleCandidateFiles uses explicit rule globs instead of globals", () => {
36
+ const changedFiles = ["src/a.ts", "docs/readme.md"];
37
+ const rule = makeRule({
38
+ include_globs: ["docs/**/*.md"]
39
+ });
40
+ const candidates = (0, filter_1.getRuleCandidateFiles)(rule, changedFiles, ["src/**/*.ts"], []);
41
+ strict_1.default.deepEqual(candidates, ["docs/readme.md"]);
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 stagedResult = await runGitCommand(["diff", "--cached"]);
74
- const unstagedResult = await runGitCommand(["diff"]);
75
- const untrackedFiles = await getUntrackedFiles();
76
- const untrackedDiffChunks = [];
77
- for (const filePath of untrackedFiles) {
78
- const fileDiff = await getNoIndexDiffForFile(filePath);
79
- if (fileDiff.trim() !== "") {
80
- untrackedDiffChunks.push(fileDiff);
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 [stagedResult.stdout, unstagedResult.stdout, ...untrackedDiffChunks]
84
- .filter((chunk) => chunk !== "")
85
- .join("\n");
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
  }
@@ -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: [],
@@ -95,6 +100,8 @@ function scaffoldConfig(force = false) {
95
100
  },
96
101
  rules: {
97
102
  disable: [],
103
+ include_globs: ["src/**/*.ts"],
104
+ exclude_globs: [],
98
105
  severity_overrides: {}
99
106
  },
100
107
  backends: {
@@ -108,6 +115,10 @@ function scaffoldConfig(force = false) {
108
115
  node_fs_1.default.writeFileSync(targetPath, `${JSON.stringify(scaffold, null, 2)}\n`, "utf8");
109
116
  process.stdout.write(picocolors_1.default.green(`Created ${targetPath}\n`));
110
117
  process.stdout.write(picocolors_1.default.cyan(`Backend setup: ${detected.backend} (${detected.reason})\n`));
118
+ process.stderr.write(`${picocolors_1.default.bgRed(picocolors_1.default.white(picocolors_1.default.bold(" SECURITY WARNING ")))}
119
+ ${picocolors_1.default.red("Semlint can only filter and scan diff content before invoking your backend. You must configure ignore files and security settings for your repository.")}
120
+ ${picocolors_1.default.red("During backend execution, security is handled by your agent/backend native configuration.")}
121
+ ${picocolors_1.default.yellow("Review semlint.json security.*, ignore files, and backend/org security settings before use.\n")}`);
111
122
  const rulesDir = node_path_1.default.join(process.cwd(), ".semlint", "rules");
112
123
  if (!node_fs_1.default.existsSync(rulesDir)) {
113
124
  node_fs_1.default.mkdirSync(rulesDir, { recursive: true });
package/dist/main.js CHANGED
@@ -7,6 +7,8 @@ exports.runSemlint = runSemlint;
7
7
  const picocolors_1 = __importDefault(require("picocolors"));
8
8
  const nanospinner_1 = require("nanospinner");
9
9
  const node_path_1 = __importDefault(require("node:path"));
10
+ const promises_1 = __importDefault(require("node:readline/promises"));
11
+ const node_process_1 = require("node:process");
10
12
  const package_json_1 = require("../package.json");
11
13
  const backend_1 = require("./backend");
12
14
  const config_1 = require("./config");
@@ -30,6 +32,64 @@ async function timedAsync(enabled, label, action) {
30
32
  (0, utils_1.debugLog)(enabled, `${label} in ${Date.now() - startedAt}ms`);
31
33
  return result;
32
34
  }
35
+ function formatFileList(title, files) {
36
+ if (files.length === 0) {
37
+ return `${title} (0):\n (none)\n`;
38
+ }
39
+ return `${title} (${files.length}):\n${files.map((file) => ` - ${file}`).join("\n")}\n`;
40
+ }
41
+ async function confirmDiffPreview(includedFiles, excludedFiles, autoAccept) {
42
+ const boxWidth = 80;
43
+ const boxBorder = picocolors_1.default.dim(`+${"-".repeat(boxWidth - 2)}+`);
44
+ // INFO : https://patorjk.com/software/taag/#p=display&f=Terrace&t=Semlint&x=none&v=4&h=4&w=80&we=false
45
+ const semlintAscii = [
46
+ " ░██████ ░██ ░██ ░██ ",
47
+ " ░██ ░██ ░██ ░██ ",
48
+ "░██ ░███████ ░█████████████ ░██ ░██░████████ ░████████ ",
49
+ " ░████████ ░██ ░██ ░██ ░██ ░██ ░██ ░██░██ ░██ ░██ ",
50
+ " ░██ ░█████████ ░██ ░██ ░██ ░██ ░██░██ ░██ ░██ ",
51
+ " ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██░██ ░██ ░██ ",
52
+ " ░██████ ░███████ ░██ ░██ ░██ ░██ ░██░██ ░██ ░████ ",
53
+ " ",
54
+ " "
55
+ ];
56
+ const boxLine = (text = "", style = (value) => value) => {
57
+ const visibleText = text.length > boxWidth - 4 ? `${text.slice(0, boxWidth - 7)}...` : text;
58
+ const padded = visibleText.padEnd(boxWidth - 4, " ");
59
+ return `${picocolors_1.default.dim("|")} ${style(padded)} ${picocolors_1.default.dim("|")}\n`;
60
+ };
61
+ process.stdout.write("\n");
62
+ process.stdout.write(`${picocolors_1.default.cyan(semlintAscii.join("\n"))}\n\n`);
63
+ process.stdout.write(`${picocolors_1.default.dim(`Semlint v${package_json_1.version} (alpha) - Use at your own risk.`)}\n`);
64
+ process.stdout.write(`${boxBorder}\n`);
65
+ process.stdout.write(boxLine("Diff preview", (value) => picocolors_1.default.bold(value)));
66
+ process.stdout.write(boxLine("Review which files are included before sending this diff to your agent.", picocolors_1.default.dim));
67
+ process.stdout.write(boxLine());
68
+ process.stdout.write(boxLine("! Security warning", (value) => picocolors_1.default.red(picocolors_1.default.bold(value))));
69
+ process.stdout.write(boxLine("Run `semlint-cli security` to review security guidance.", picocolors_1.default.dim));
70
+ process.stdout.write(`${boxBorder}\n\n`);
71
+ process.stdout.write(formatFileList(picocolors_1.default.bold("Included files"), includedFiles));
72
+ process.stdout.write("\n");
73
+ process.stdout.write(formatFileList(picocolors_1.default.bold("Excluded files"), excludedFiles));
74
+ process.stdout.write("\n");
75
+ if (autoAccept) {
76
+ process.stdout.write(picocolors_1.default.dim("Auto-accepted with --yes.\n\n"));
77
+ return true;
78
+ }
79
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
80
+ process.stderr.write(picocolors_1.default.red("Diff confirmation is required by default. Re-run with --yes (-y) to auto-accept in non-interactive environments.\n"));
81
+ return false;
82
+ }
83
+ const rl = promises_1.default.createInterface({ input: node_process_1.stdin, output: node_process_1.stdout });
84
+ try {
85
+ const answer = await rl.question(picocolors_1.default.yellow("Proceed with Semlint analysis? [y/N] "));
86
+ const normalized = answer.trim().toLowerCase();
87
+ return normalized === "y" || normalized === "yes";
88
+ }
89
+ finally {
90
+ rl.close();
91
+ }
92
+ }
33
93
  async function runSemlint(options) {
34
94
  const startedAt = Date.now();
35
95
  let spinner = null;
@@ -43,8 +103,10 @@ async function runSemlint(options) {
43
103
  const repoRoot = await timedAsync(config.debug, "Resolved git repo root", () => (0, git_1.getRepoRoot)());
44
104
  const scanRoot = repoRoot ?? process.cwd();
45
105
  (0, utils_1.debugLog)(config.debug, `Using diff/ignore scan root: ${scanRoot}`);
46
- const rawDiff = await timedAsync(config.debug, "Computed git diff", () => useLocalBranchDiff ? (0, git_1.getLocalBranchDiff)() : (0, git_1.getGitDiff)(config.base, config.head));
47
- const { filteredDiff: diff, excludedFiles } = timed(config.debug, "Filtered diff by ignore rules", () => (0, secrets_1.filterDiffByIgnoreRules)(rawDiff, scanRoot, config.security.ignoreFiles));
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));
48
110
  if (excludedFiles.length > 0) {
49
111
  (0, utils_1.debugLog)(config.debug, `Excluded ${excludedFiles.length} file(s) by ignore/security rules: ${excludedFiles.join(", ")}`);
50
112
  }
@@ -52,7 +114,7 @@ async function runSemlint(options) {
52
114
  const findings = timed(config.debug, "Scanned diff for secrets", () => (0, secrets_1.scanDiffForSecrets)(diff, config.security.allowPatterns, config.security.allowFiles));
53
115
  if (findings.length > 0) {
54
116
  process.stderr.write(picocolors_1.default.red("Secret guard blocked analysis: potential secrets were detected in the diff. Nothing was sent to the backend.\n"));
55
- process.stderr.write("Allow a known-safe file by adding a glob to security.allow_files in semlint.json (example: \"allow_files\": [\"src/test2.ts\"]).\n");
117
+ process.stderr.write("Allow a known-safe file by adding a glob to security.allow_files in semlint.json (example: \"allow_files\": [\"src/my-sensitive-file.ts\"]).\n");
56
118
  findings.slice(0, 20).forEach((finding) => {
57
119
  process.stderr.write(` ${finding.file}:${finding.line} ${finding.kind} sample=${finding.redactedSample}\n`);
58
120
  });
@@ -63,6 +125,11 @@ async function runSemlint(options) {
63
125
  }
64
126
  }
65
127
  const changedFiles = timed(config.debug, "Parsed changed files from diff", () => (0, filter_1.extractChangedFilesFromDiff)(diff));
128
+ const confirmed = await timedAsync(config.debug, "User confirmation", () => confirmDiffPreview(changedFiles, excludedFiles, options.autoAccept));
129
+ if (!confirmed) {
130
+ process.stderr.write("Aborted by user.\n");
131
+ return 2;
132
+ }
66
133
  (0, utils_1.debugLog)(config.debug, useLocalBranchDiff
67
134
  ? "Using local branch diff (staged + unstaged + untracked only)"
68
135
  : `Using explicit ref diff (${config.base}..${config.head})`);
@@ -70,7 +137,7 @@ async function runSemlint(options) {
70
137
  const backend = timed(config.debug, "Initialized backend runner", () => (0, backend_1.createBackendRunner)(config));
71
138
  const runnableRules = rules.filter((rule) => {
72
139
  const filterStartedAt = Date.now();
73
- const shouldRun = (0, filter_1.shouldRunRule)(rule, changedFiles, diff);
140
+ const shouldRun = (0, filter_1.shouldRunRule)(rule, changedFiles, diff, config.rulesIncludeGlobs, config.rulesExcludeGlobs);
74
141
  (0, utils_1.debugLog)(config.debug, `Rule ${rule.id}: filter check in ${Date.now() - filterStartedAt}ms`);
75
142
  if (!shouldRun) {
76
143
  (0, utils_1.debugLog)(config.debug, `Skipping rule ${rule.id}: filters did not match`);
package/dist/rules.js CHANGED
@@ -13,7 +13,7 @@ function assertNonEmptyString(value, fieldName, filePath) {
13
13
  }
14
14
  return value;
15
15
  }
16
- function assertStringArray(value, fieldName, filePath) {
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: assertStringArray(obj.include_globs, "include_globs", filePath),
40
- exclude_globs: assertStringArray(obj.exclude_globs, "exclude_globs", filePath),
41
- diff_regex: assertStringArray(obj.diff_regex, "diff_regex", filePath)
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
@@ -7,18 +7,21 @@ exports.filterDiffByIgnoreRules = filterDiffByIgnoreRules;
7
7
  exports.scanDiffForSecrets = scanDiffForSecrets;
8
8
  const node_fs_1 = __importDefault(require("node:fs"));
9
9
  const node_path_1 = __importDefault(require("node:path"));
10
+ const ignore_1 = __importDefault(require("ignore"));
10
11
  const picomatch_1 = __importDefault(require("picomatch"));
11
12
  const diff_1 = require("./diff");
12
13
  const BUILTIN_SENSITIVE_GLOBS = [
13
14
  ".env",
15
+ "*.env",
14
16
  ".env.*",
17
+ "*.env.*",
15
18
  "*.pem",
16
19
  "*.key",
17
20
  "id_rsa",
18
21
  "id_rsa.*",
19
- "**/secrets/**",
20
- "**/credentials/**",
21
- "**/*credentials*.json"
22
+ "secrets/",
23
+ "credentials/",
24
+ "*credentials*.json"
22
25
  ];
23
26
  const SECRET_KEYWORDS = [
24
27
  "password",
@@ -68,6 +71,10 @@ function readIgnorePatterns(cwd, ignoreFiles) {
68
71
  .filter((line) => line !== "" && !line.startsWith("#"));
69
72
  });
70
73
  }
74
+ function createIgnoreMatcher(patterns) {
75
+ const matcher = (0, ignore_1.default)().add(patterns);
76
+ return (filePath) => matcher.ignores(filePath);
77
+ }
71
78
  function redactSample(sample) {
72
79
  const compact = sample.trim();
73
80
  if (compact.length <= 6) {
@@ -86,8 +93,9 @@ function parseAllowMatchers(patterns) {
86
93
  });
87
94
  }
88
95
  function filterDiffByIgnoreRules(diff, cwd, ignoreFiles) {
89
- const ignorePatterns = [...readIgnorePatterns(cwd, ignoreFiles), ...BUILTIN_SENSITIVE_GLOBS];
90
- const ignoreMatcher = (0, picomatch_1.default)(ignorePatterns, { dot: true });
96
+ // Keep built-ins first so repo-specific negation patterns can carve out safe exceptions.
97
+ const ignorePatterns = [...BUILTIN_SENSITIVE_GLOBS, ...readIgnorePatterns(cwd, ignoreFiles)];
98
+ const ignoreMatcher = createIgnoreMatcher(ignorePatterns);
91
99
  const chunks = (0, diff_1.splitDiffIntoFileChunks)(diff);
92
100
  const excludedFiles = chunks
93
101
  .filter((chunk) => chunk.file !== "" && ignoreMatcher(chunk.file))
@@ -81,3 +81,77 @@ const secrets_1 = require("./secrets");
81
81
  const findings = (0, secrets_1.scanDiffForSecrets)(diff, [], ["src/test2.ts"]);
82
82
  strict_1.default.equal(findings.length, 0);
83
83
  });
84
+ (0, node_test_1.default)("filterDiffByIgnoreRules matches basename ignores in nested paths", () => {
85
+ const cwd = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "semlint-ignore-basename-"));
86
+ try {
87
+ node_fs_1.default.writeFileSync(node_path_1.default.join(cwd, ".semlintignore"), ".env\n*.env\n", "utf8");
88
+ const diff = [
89
+ "diff --git a/src/secret.env b/src/secret.env",
90
+ "--- a/src/secret.env",
91
+ "+++ b/src/secret.env",
92
+ "@@ -0,0 +1 @@",
93
+ "+API_KEY=abc",
94
+ "diff --git a/src/safe.ts b/src/safe.ts",
95
+ "--- a/src/safe.ts",
96
+ "+++ b/src/safe.ts",
97
+ "@@ -0,0 +1 @@",
98
+ "+const safe = true;"
99
+ ].join("\n");
100
+ const result = (0, secrets_1.filterDiffByIgnoreRules)(diff, cwd, [".semlintignore"]);
101
+ strict_1.default.deepEqual(result.excludedFiles, ["src/secret.env"]);
102
+ strict_1.default.match(result.filteredDiff, /src\/safe\.ts/);
103
+ strict_1.default.doesNotMatch(result.filteredDiff, /src\/secret\.env/);
104
+ }
105
+ finally {
106
+ node_fs_1.default.rmSync(cwd, { recursive: true, force: true });
107
+ }
108
+ });
109
+ (0, node_test_1.default)("filterDiffByIgnoreRules supports gitignore negation patterns", () => {
110
+ const cwd = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "semlint-ignore-negation-"));
111
+ try {
112
+ node_fs_1.default.writeFileSync(node_path_1.default.join(cwd, ".semlintignore"), "*.env\n!src/public.env\n", "utf8");
113
+ const diff = [
114
+ "diff --git a/src/secret.env b/src/secret.env",
115
+ "--- a/src/secret.env",
116
+ "+++ b/src/secret.env",
117
+ "@@ -0,0 +1 @@",
118
+ "+API_KEY=blocked",
119
+ "diff --git a/src/public.env b/src/public.env",
120
+ "--- a/src/public.env",
121
+ "+++ b/src/public.env",
122
+ "@@ -0,0 +1 @@",
123
+ "+SAFE=true"
124
+ ].join("\n");
125
+ const result = (0, secrets_1.filterDiffByIgnoreRules)(diff, cwd, [".semlintignore"]);
126
+ strict_1.default.deepEqual(result.excludedFiles, ["src/secret.env"]);
127
+ strict_1.default.match(result.filteredDiff, /src\/public\.env/);
128
+ strict_1.default.doesNotMatch(result.filteredDiff, /src\/secret\.env/);
129
+ }
130
+ finally {
131
+ node_fs_1.default.rmSync(cwd, { recursive: true, force: true });
132
+ }
133
+ });
134
+ (0, node_test_1.default)("filterDiffByIgnoreRules excludes *.env by builtin sensitive globs", () => {
135
+ const cwd = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "semlint-ignore-builtin-env-"));
136
+ try {
137
+ const diff = [
138
+ "diff --git a/src/secret.env b/src/secret.env",
139
+ "--- a/src/secret.env",
140
+ "+++ b/src/secret.env",
141
+ "@@ -0,0 +1 @@",
142
+ "+API_KEY=blocked",
143
+ "diff --git a/src/safe.ts b/src/safe.ts",
144
+ "--- a/src/safe.ts",
145
+ "+++ b/src/safe.ts",
146
+ "@@ -0,0 +1 @@",
147
+ "+const safe = true;"
148
+ ].join("\n");
149
+ const result = (0, secrets_1.filterDiffByIgnoreRules)(diff, cwd, [".semlintignore"]);
150
+ strict_1.default.deepEqual(result.excludedFiles, ["src/secret.env"]);
151
+ strict_1.default.match(result.filteredDiff, /src\/safe\.ts/);
152
+ strict_1.default.doesNotMatch(result.filteredDiff, /src\/secret\.env/);
153
+ }
154
+ finally {
155
+ node_fs_1.default.rmSync(cwd, { recursive: true, force: true });
156
+ }
157
+ });
@@ -0,0 +1,26 @@
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
+ exports.printSecurityGuide = printSecurityGuide;
7
+ const picocolors_1 = __importDefault(require("picocolors"));
8
+ const SECURITY_TEXT = [
9
+ picocolors_1.default.bold("Semlint security guide"),
10
+ "",
11
+ "Semlint applies security controls before backend execution:",
12
+ "- It filters diff paths using ignore files and built-in sensitive globs.",
13
+ "- It scans added lines for high-signal secret patterns.",
14
+ "- It blocks backend execution when potential secrets are found.",
15
+ "",
16
+ "Your responsibilities:",
17
+ "- Keep `.gitignore`, `.cursorignore`, `.semlintignore` (and configured `security.ignore_files`) up to date.",
18
+ "- Tune `security.allow_patterns` and `security.allow_files` only for known-safe cases.",
19
+ "- Review your agent native access and security policy.",
20
+ "",
21
+ "More details: README.md#security-responsibility-model"
22
+ ].join("\n");
23
+ function printSecurityGuide() {
24
+ process.stdout.write(`${SECURITY_TEXT}\n`);
25
+ return 0;
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "semlint-cli",
3
- "version": "0.1.7",
3
+ "version": "0.1.10",
4
4
  "description": "Semantic lint CLI — runs LLM-backed rules on your git diff and returns CI-friendly exit codes",
5
5
  "type": "commonjs",
6
6
  "main": "dist/main.js",
@@ -42,6 +42,7 @@
42
42
  },
43
43
  "packageManager": "pnpm@10.29.2",
44
44
  "dependencies": {
45
+ "ignore": "^7.0.5",
45
46
  "nanospinner": "^1.2.2",
46
47
  "picocolors": "^1.1.1",
47
48
  "picomatch": "^4.0.3"