semlint-cli 0.1.0 → 0.1.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/README.md +26 -1
- package/dist/cli.js +51 -5
- package/dist/filter.js +30 -4
- package/dist/init.js +95 -0
- package/dist/main.js +53 -5
- package/dist/reporter.js +35 -4
- package/package.json +4 -2
- package/rules/SEMLINT_NAMING_001.json +0 -12
- package/rules/SEMLINT_PATTERN_002.json +0 -12
- package/rules/SEMLINT_SWE_003.json +0 -12
package/README.md
CHANGED
|
@@ -21,6 +21,12 @@ pnpm build
|
|
|
21
21
|
semlint check
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
Scaffold a project config with automatic coding agent CLI detection:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
semlint init
|
|
28
|
+
```
|
|
29
|
+
|
|
24
30
|
If running from source:
|
|
25
31
|
|
|
26
32
|
```bash
|
|
@@ -38,6 +44,7 @@ pnpm check
|
|
|
38
44
|
- `--fail-on <error|warn|never>`: failure threshold (default `error`)
|
|
39
45
|
- `--batch`: run all selected rules in one backend call
|
|
40
46
|
- `--debug`: enable debug logs to stderr
|
|
47
|
+
- `init --force`: overwrite an existing `semlint.json`
|
|
41
48
|
|
|
42
49
|
Default diff behavior (without `--base`/`--head`) uses your local branch state:
|
|
43
50
|
|
|
@@ -92,9 +99,27 @@ Unknown fields are ignored.
|
|
|
92
99
|
}
|
|
93
100
|
```
|
|
94
101
|
|
|
102
|
+
## Config scaffolding and auto-detection
|
|
103
|
+
|
|
104
|
+
Run:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
semlint init
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
This creates `./semlint.json` and auto-detects installed coding agent CLIs in this priority order:
|
|
111
|
+
|
|
112
|
+
1. `cursor` -> backend `cursor-cli`
|
|
113
|
+
2. `claude` -> backend `claude-code`
|
|
114
|
+
3. `codex` -> backend `codex-cli`
|
|
115
|
+
|
|
116
|
+
If no known CLI is detected, Semlint falls back to `cursor-cli` + executable `cursor`.
|
|
117
|
+
|
|
118
|
+
Use `semlint init --force` to overwrite an existing config file. Init also creates `.semlint/rules/` and a starter rule `SEMLINT_EXAMPLE_001.json` (with a placeholder title and prompt) if they do not exist.
|
|
119
|
+
|
|
95
120
|
## Rule files
|
|
96
121
|
|
|
97
|
-
Rule JSON files are loaded from
|
|
122
|
+
Rule JSON files are loaded from `.semlint/rules/`. Run `semlint init` to create this folder and an example rule you can edit.
|
|
98
123
|
|
|
99
124
|
Required fields:
|
|
100
125
|
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
3
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
8
|
+
const init_1 = require("./init");
|
|
4
9
|
const main_1 = require("./main");
|
|
10
|
+
const HELP_TEXT = [
|
|
11
|
+
"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",
|
|
15
|
+
"",
|
|
16
|
+
"Commands:",
|
|
17
|
+
" check Run semantic lint rules against your git diff",
|
|
18
|
+
" init Create semlint.json, .semlint/rules/, and an example rule to edit",
|
|
19
|
+
"",
|
|
20
|
+
"Options:",
|
|
21
|
+
" -h, --help Show this help text"
|
|
22
|
+
].join("\n");
|
|
23
|
+
class HelpRequestedError extends Error {
|
|
24
|
+
constructor() {
|
|
25
|
+
super(HELP_TEXT);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
5
28
|
const FLAGS_WITH_VALUES = new Set([
|
|
6
29
|
"--backend",
|
|
7
30
|
"--model",
|
|
@@ -18,9 +41,26 @@ function isFailOn(value) {
|
|
|
18
41
|
return value === "error" || value === "warn" || value === "never";
|
|
19
42
|
}
|
|
20
43
|
function parseArgs(argv) {
|
|
44
|
+
if (argv.length === 0 || argv.includes("--help") || argv.includes("-h") || argv[0] === "help") {
|
|
45
|
+
throw new HelpRequestedError();
|
|
46
|
+
}
|
|
21
47
|
const [command, ...rest] = argv;
|
|
22
|
-
if (!command || command !== "check") {
|
|
23
|
-
throw new Error(
|
|
48
|
+
if (!command || (command !== "check" && command !== "init")) {
|
|
49
|
+
throw new Error(HELP_TEXT);
|
|
50
|
+
}
|
|
51
|
+
if (command === "init") {
|
|
52
|
+
const options = {
|
|
53
|
+
command: "init",
|
|
54
|
+
debug: false
|
|
55
|
+
};
|
|
56
|
+
for (const token of rest) {
|
|
57
|
+
if (token === "--force") {
|
|
58
|
+
options.force = true;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
throw new Error(`Unknown flag for init: ${token}`);
|
|
62
|
+
}
|
|
63
|
+
return options;
|
|
24
64
|
}
|
|
25
65
|
const options = {
|
|
26
66
|
command: "check",
|
|
@@ -81,13 +121,19 @@ function parseArgs(argv) {
|
|
|
81
121
|
async function main() {
|
|
82
122
|
try {
|
|
83
123
|
const options = parseArgs(process.argv.slice(2));
|
|
84
|
-
const exitCode = await (0, main_1.runSemlint)(options);
|
|
124
|
+
const exitCode = options.command === "init" ? (0, init_1.scaffoldConfig)(options.force) : await (0, main_1.runSemlint)(options);
|
|
85
125
|
process.exitCode = exitCode;
|
|
86
126
|
}
|
|
87
127
|
catch (error) {
|
|
88
128
|
const message = error instanceof Error ? error.message : String(error);
|
|
89
|
-
|
|
90
|
-
|
|
129
|
+
if (error instanceof HelpRequestedError) {
|
|
130
|
+
process.stderr.write(`${message}\n`);
|
|
131
|
+
process.exitCode = 0;
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
process.stderr.write(picocolors_1.default.red(`Error: ${message}\n`));
|
|
135
|
+
process.exitCode = 2;
|
|
136
|
+
}
|
|
91
137
|
}
|
|
92
138
|
}
|
|
93
139
|
void main();
|
package/dist/filter.js
CHANGED
|
@@ -131,14 +131,40 @@ function buildScopedDiff(rule, fullDiff, changedFiles) {
|
|
|
131
131
|
}
|
|
132
132
|
function buildRulePrompt(rule, diff) {
|
|
133
133
|
return [
|
|
134
|
+
"You are Semlint, an expert semantic code reviewer.",
|
|
135
|
+
"Analyze ONLY the modified code present in the DIFF below.",
|
|
136
|
+
"Return JSON only (no markdown, no prose, no code fences).",
|
|
137
|
+
"Output schema:",
|
|
138
|
+
"{",
|
|
139
|
+
" \"diagnostics\": [",
|
|
140
|
+
" {",
|
|
141
|
+
" \"rule_id\": string,",
|
|
142
|
+
" \"severity\": \"error\" | \"warn\" | \"info\",",
|
|
143
|
+
" \"message\": string,",
|
|
144
|
+
" \"file\": string,",
|
|
145
|
+
" \"line\": number,",
|
|
146
|
+
" \"column\"?: number,",
|
|
147
|
+
" \"end_line\"?: number,",
|
|
148
|
+
" \"end_column\"?: number,",
|
|
149
|
+
" \"evidence\"?: string,",
|
|
150
|
+
" \"confidence\"?: number",
|
|
151
|
+
" }",
|
|
152
|
+
" ]",
|
|
153
|
+
"}",
|
|
154
|
+
"Rules:",
|
|
155
|
+
"- If there are no findings, return {\"diagnostics\":[]}.",
|
|
156
|
+
"- Each diagnostic must reference a changed file from the DIFF.",
|
|
157
|
+
"- Use the provided RULE_ID exactly in every diagnostic.",
|
|
158
|
+
"- Keep messages concise and actionable.",
|
|
159
|
+
"",
|
|
134
160
|
`RULE_ID: ${rule.id}`,
|
|
135
161
|
`RULE_TITLE: ${rule.title}`,
|
|
136
162
|
`SEVERITY_DEFAULT: ${rule.effectiveSeverity}`,
|
|
137
163
|
"",
|
|
138
|
-
"DIFF:",
|
|
139
|
-
diff,
|
|
140
|
-
"",
|
|
141
164
|
"INSTRUCTIONS:",
|
|
142
|
-
rule.prompt
|
|
165
|
+
rule.prompt,
|
|
166
|
+
"",
|
|
167
|
+
"DIFF:",
|
|
168
|
+
diff
|
|
143
169
|
].join("\n");
|
|
144
170
|
}
|
package/dist/init.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
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.scaffoldConfig = scaffoldConfig;
|
|
7
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const node_child_process_1 = require("node:child_process");
|
|
11
|
+
function commandExists(command) {
|
|
12
|
+
const result = (0, node_child_process_1.spawnSync)(command, ["--version"], {
|
|
13
|
+
stdio: "ignore"
|
|
14
|
+
});
|
|
15
|
+
return result.status === 0 || result.status === 1;
|
|
16
|
+
}
|
|
17
|
+
function detectBackend() {
|
|
18
|
+
const candidates = [
|
|
19
|
+
{
|
|
20
|
+
executable: "cursor",
|
|
21
|
+
backend: "cursor-cli",
|
|
22
|
+
reason: "detected Cursor CLI"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
executable: "claude",
|
|
26
|
+
backend: "claude-code",
|
|
27
|
+
reason: "detected Claude Code CLI"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
executable: "codex",
|
|
31
|
+
backend: "codex-cli",
|
|
32
|
+
reason: "detected Codex CLI"
|
|
33
|
+
}
|
|
34
|
+
];
|
|
35
|
+
for (const candidate of candidates) {
|
|
36
|
+
if (commandExists(candidate.executable)) {
|
|
37
|
+
return candidate;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
backend: "cursor-cli",
|
|
42
|
+
executable: "cursor",
|
|
43
|
+
reason: "no known agent CLI detected, using default Cursor setup"
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function scaffoldConfig(force = false) {
|
|
47
|
+
const targetPath = node_path_1.default.join(process.cwd(), "semlint.json");
|
|
48
|
+
if (node_fs_1.default.existsSync(targetPath) && !force) {
|
|
49
|
+
process.stderr.write(`Refusing to overwrite existing ${targetPath}. Re-run with "semlint init --force" to replace it.\n`);
|
|
50
|
+
return 2;
|
|
51
|
+
}
|
|
52
|
+
const detected = detectBackend();
|
|
53
|
+
const scaffold = {
|
|
54
|
+
backend: detected.backend,
|
|
55
|
+
model: "auto",
|
|
56
|
+
budgets: {
|
|
57
|
+
timeout_ms: 120000
|
|
58
|
+
},
|
|
59
|
+
output: {
|
|
60
|
+
format: "text"
|
|
61
|
+
},
|
|
62
|
+
execution: {
|
|
63
|
+
batch: false
|
|
64
|
+
},
|
|
65
|
+
rules: {
|
|
66
|
+
disable: [],
|
|
67
|
+
severity_overrides: {}
|
|
68
|
+
},
|
|
69
|
+
backends: {
|
|
70
|
+
[detected.backend]: {
|
|
71
|
+
executable: detected.executable
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
node_fs_1.default.writeFileSync(targetPath, `${JSON.stringify(scaffold, null, 2)}\n`, "utf8");
|
|
76
|
+
process.stdout.write(picocolors_1.default.green(`Created ${targetPath}\n`));
|
|
77
|
+
process.stdout.write(picocolors_1.default.cyan(`Backend setup: ${detected.backend} (${detected.reason})\n`));
|
|
78
|
+
const rulesDir = node_path_1.default.join(process.cwd(), ".semlint", "rules");
|
|
79
|
+
if (!node_fs_1.default.existsSync(rulesDir)) {
|
|
80
|
+
node_fs_1.default.mkdirSync(rulesDir, { recursive: true });
|
|
81
|
+
process.stdout.write(picocolors_1.default.green(`Created ${node_path_1.default.join(".semlint", "rules")}/\n`));
|
|
82
|
+
}
|
|
83
|
+
const exampleRulePath = node_path_1.default.join(rulesDir, "SEMLINT_EXAMPLE_001.json");
|
|
84
|
+
if (!node_fs_1.default.existsSync(exampleRulePath)) {
|
|
85
|
+
const exampleRule = {
|
|
86
|
+
id: "SEMLINT_EXAMPLE_001",
|
|
87
|
+
title: "My first rule",
|
|
88
|
+
severity_default: "warn",
|
|
89
|
+
prompt: "Describe what the agent should check in the changed code. Example: flag when new functions lack JSDoc, or when error handling is missing."
|
|
90
|
+
};
|
|
91
|
+
node_fs_1.default.writeFileSync(exampleRulePath, `${JSON.stringify(exampleRule, null, 2)}\n`, "utf8");
|
|
92
|
+
process.stdout.write(picocolors_1.default.green(`Created ${node_path_1.default.join(".semlint", "rules", "SEMLINT_EXAMPLE_001.json")} `) + picocolors_1.default.dim(`(edit the title and prompt to define your rule)\n`));
|
|
93
|
+
}
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
package/dist/main.js
CHANGED
|
@@ -4,6 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.runSemlint = runSemlint;
|
|
7
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
8
|
+
const nanospinner_1 = require("nanospinner");
|
|
7
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
8
10
|
const backend_1 = require("./backend");
|
|
9
11
|
const config_1 = require("./config");
|
|
@@ -15,7 +17,7 @@ const rules_1 = require("./rules");
|
|
|
15
17
|
const VERSION = "0.1.0";
|
|
16
18
|
function debugLog(enabled, message) {
|
|
17
19
|
if (enabled) {
|
|
18
|
-
process.stderr.write(`[debug] ${message}\n`);
|
|
20
|
+
process.stderr.write(`${picocolors_1.default.gray(`[debug] ${message}`)}\n`);
|
|
19
21
|
}
|
|
20
22
|
}
|
|
21
23
|
function timed(enabled, label, action) {
|
|
@@ -41,10 +43,33 @@ function buildBatchPrompt(rules, diff) {
|
|
|
41
43
|
].join("\n"))
|
|
42
44
|
.join("\n\n---\n\n");
|
|
43
45
|
return [
|
|
46
|
+
"You are Semlint, an expert semantic code reviewer.",
|
|
44
47
|
"BATCH_MODE: true",
|
|
45
48
|
"Evaluate all rules below against the DIFF in one pass.",
|
|
46
|
-
"
|
|
47
|
-
"
|
|
49
|
+
"Analyze ONLY the modified code present in the DIFF below.",
|
|
50
|
+
"Return JSON only (no markdown, no prose, no code fences).",
|
|
51
|
+
"Output schema:",
|
|
52
|
+
"{",
|
|
53
|
+
" \"diagnostics\": [",
|
|
54
|
+
" {",
|
|
55
|
+
" \"rule_id\": string,",
|
|
56
|
+
" \"severity\": \"error\" | \"warn\" | \"info\",",
|
|
57
|
+
" \"message\": string,",
|
|
58
|
+
" \"file\": string,",
|
|
59
|
+
" \"line\": number,",
|
|
60
|
+
" \"column\"?: number,",
|
|
61
|
+
" \"end_line\"?: number,",
|
|
62
|
+
" \"end_column\"?: number,",
|
|
63
|
+
" \"evidence\"?: string,",
|
|
64
|
+
" \"confidence\"?: number",
|
|
65
|
+
" }",
|
|
66
|
+
" ]",
|
|
67
|
+
"}",
|
|
68
|
+
"Rules:",
|
|
69
|
+
"- If there are no findings, return {\"diagnostics\":[]}.",
|
|
70
|
+
"- Each diagnostic must reference a changed file from the DIFF.",
|
|
71
|
+
"- rule_id must match one of the RULE_ID values listed below.",
|
|
72
|
+
"- Keep messages concise and actionable.",
|
|
48
73
|
"",
|
|
49
74
|
"RULES:",
|
|
50
75
|
ruleBlocks,
|
|
@@ -55,9 +80,10 @@ function buildBatchPrompt(rules, diff) {
|
|
|
55
80
|
}
|
|
56
81
|
async function runSemlint(options) {
|
|
57
82
|
const startedAt = Date.now();
|
|
83
|
+
let spinner = null;
|
|
58
84
|
try {
|
|
59
85
|
const config = timed(options.debug, "Loaded effective config", () => (0, config_1.loadEffectiveConfig)(options));
|
|
60
|
-
const rulesDir = node_path_1.default.join(process.cwd(), "rules");
|
|
86
|
+
const rulesDir = node_path_1.default.join(process.cwd(), ".semlint", "rules");
|
|
61
87
|
const rules = timed(config.debug, "Loaded and validated rules", () => (0, rules_1.loadRules)(rulesDir, config.rulesDisable, config.severityOverrides));
|
|
62
88
|
debugLog(config.debug, `Loaded ${rules.length} rule(s)`);
|
|
63
89
|
debugLog(config.debug, `Rule IDs: ${rules.map((rule) => rule.id).join(", ")}`);
|
|
@@ -82,6 +108,17 @@ async function runSemlint(options) {
|
|
|
82
108
|
const diagnostics = [];
|
|
83
109
|
const rulesRun = runnableRules.length;
|
|
84
110
|
let backendErrors = 0;
|
|
111
|
+
if (config.format !== "json" && rulesRun > 0) {
|
|
112
|
+
process.stdout.write(`${picocolors_1.default.bold("Running rules:")}\n`);
|
|
113
|
+
for (const rule of runnableRules) {
|
|
114
|
+
process.stdout.write(` ${picocolors_1.default.cyan(rule.id)} ${picocolors_1.default.dim(rule.title)}\n`);
|
|
115
|
+
}
|
|
116
|
+
process.stdout.write("\n");
|
|
117
|
+
}
|
|
118
|
+
spinner =
|
|
119
|
+
config.format !== "json" && rulesRun > 0
|
|
120
|
+
? (0, nanospinner_1.createSpinner)(`Analyzing ${changedFiles.length} file${changedFiles.length === 1 ? "" : "s"} with ${config.backend} in ${config.batchMode ? "batch" : "parallel"} mode...`).start()
|
|
121
|
+
: null;
|
|
85
122
|
if (config.batchMode && runnableRules.length > 0) {
|
|
86
123
|
debugLog(config.debug, `Running ${runnableRules.length} rule(s) in batch mode`);
|
|
87
124
|
const combinedDiff = timed(config.debug, "Batch: combined scoped diff build", () => runnableRules
|
|
@@ -161,12 +198,20 @@ async function runSemlint(options) {
|
|
|
161
198
|
}
|
|
162
199
|
const sorted = timed(config.debug, "Sorted diagnostics", () => (0, diagnostics_1.sortDiagnostics)(diagnostics));
|
|
163
200
|
const durationMs = Date.now() - startedAt;
|
|
201
|
+
if (spinner) {
|
|
202
|
+
if (backendErrors > 0) {
|
|
203
|
+
spinner.error({ text: "Analysis completed with backend errors" });
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
spinner.success({ text: "Analysis complete" });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
164
209
|
const outputStartedAt = Date.now();
|
|
165
210
|
if (config.format === "json") {
|
|
166
211
|
process.stdout.write(`${(0, reporter_1.formatJsonOutput)(VERSION, sorted, { rulesRun, durationMs, backendErrors })}\n`);
|
|
167
212
|
}
|
|
168
213
|
else {
|
|
169
|
-
process.stdout.write(`${(0, reporter_1.formatTextOutput)(sorted)}\n`);
|
|
214
|
+
process.stdout.write(`${(0, reporter_1.formatTextOutput)(sorted, { rulesRun, durationMs, backendErrors })}\n`);
|
|
170
215
|
}
|
|
171
216
|
debugLog(config.debug, `Rendered output in ${Date.now() - outputStartedAt}ms`);
|
|
172
217
|
debugLog(config.debug, `Total run duration ${durationMs}ms`);
|
|
@@ -179,6 +224,9 @@ async function runSemlint(options) {
|
|
|
179
224
|
return 0;
|
|
180
225
|
}
|
|
181
226
|
catch (error) {
|
|
227
|
+
if (spinner) {
|
|
228
|
+
spinner.error({ text: "Analysis failed" });
|
|
229
|
+
}
|
|
182
230
|
const message = error instanceof Error ? error.message : String(error);
|
|
183
231
|
process.stderr.write(`${message}\n`);
|
|
184
232
|
return 2;
|
package/dist/reporter.js
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.formatJsonOutput = formatJsonOutput;
|
|
4
7
|
exports.formatTextOutput = formatTextOutput;
|
|
8
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
9
|
+
function formatSeverity(severity) {
|
|
10
|
+
if (severity === "error")
|
|
11
|
+
return picocolors_1.default.red(severity);
|
|
12
|
+
if (severity === "warn")
|
|
13
|
+
return picocolors_1.default.yellow(severity);
|
|
14
|
+
if (severity === "info")
|
|
15
|
+
return picocolors_1.default.cyan(severity);
|
|
16
|
+
return severity;
|
|
17
|
+
}
|
|
5
18
|
function formatJsonOutput(version, diagnostics, stats) {
|
|
6
19
|
const payload = {
|
|
7
20
|
tool: {
|
|
@@ -17,7 +30,7 @@ function formatJsonOutput(version, diagnostics, stats) {
|
|
|
17
30
|
};
|
|
18
31
|
return JSON.stringify(payload, null, 2);
|
|
19
32
|
}
|
|
20
|
-
function formatTextOutput(diagnostics) {
|
|
33
|
+
function formatTextOutput(diagnostics, stats) {
|
|
21
34
|
const lines = [];
|
|
22
35
|
let currentFile = "";
|
|
23
36
|
for (const diagnostic of diagnostics) {
|
|
@@ -26,10 +39,10 @@ function formatTextOutput(diagnostics) {
|
|
|
26
39
|
lines.push("");
|
|
27
40
|
}
|
|
28
41
|
currentFile = diagnostic.file;
|
|
29
|
-
lines.push(currentFile);
|
|
42
|
+
lines.push(picocolors_1.default.underline(currentFile));
|
|
30
43
|
}
|
|
31
44
|
const column = diagnostic.column ?? 1;
|
|
32
|
-
lines.push(` ${diagnostic.line}:${column} ${diagnostic.severity} ${diagnostic.rule_id} ${diagnostic.message}`);
|
|
45
|
+
lines.push(` ${picocolors_1.default.dim(`${diagnostic.line}:${column}`)} ${formatSeverity(diagnostic.severity)} ${picocolors_1.default.gray(diagnostic.rule_id)} ${diagnostic.message}`);
|
|
33
46
|
}
|
|
34
47
|
const errors = diagnostics.filter((d) => d.severity === "error").length;
|
|
35
48
|
const warnings = diagnostics.filter((d) => d.severity === "warn").length;
|
|
@@ -37,6 +50,24 @@ function formatTextOutput(diagnostics) {
|
|
|
37
50
|
if (lines.length > 0) {
|
|
38
51
|
lines.push("");
|
|
39
52
|
}
|
|
40
|
-
|
|
53
|
+
const summary = `✖ ${problems} problems (${errors} errors, ${warnings} warnings)`;
|
|
54
|
+
const timeInfo = ` in ${(stats.durationMs / 1000).toFixed(1)}s`;
|
|
55
|
+
if (problems === 0) {
|
|
56
|
+
if (stats.rulesRun === 0) {
|
|
57
|
+
lines.push(picocolors_1.default.green(`✔ 0 problems (no rules matched changed files)`));
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
lines.push(picocolors_1.default.green(`✔ 0 problems (0 errors, 0 warnings)`));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else if (errors > 0) {
|
|
64
|
+
lines.push(picocolors_1.default.red(summary) + picocolors_1.default.gray(timeInfo));
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
lines.push(picocolors_1.default.yellow(summary) + picocolors_1.default.gray(timeInfo));
|
|
68
|
+
}
|
|
69
|
+
if (problems === 0 && stats.durationMs > 0) {
|
|
70
|
+
lines[lines.length - 1] += picocolors_1.default.gray(timeInfo);
|
|
71
|
+
}
|
|
41
72
|
return lines.join("\n");
|
|
42
73
|
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "semlint-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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",
|
|
7
7
|
"bin": {
|
|
8
|
-
"semlint": "dist/cli.js"
|
|
8
|
+
"semlint-cli": "dist/cli.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist",
|
|
@@ -40,6 +40,8 @@
|
|
|
40
40
|
},
|
|
41
41
|
"packageManager": "pnpm@10.29.2",
|
|
42
42
|
"dependencies": {
|
|
43
|
+
"nanospinner": "^1.2.2",
|
|
44
|
+
"picocolors": "^1.1.1",
|
|
43
45
|
"picomatch": "^4.0.3"
|
|
44
46
|
},
|
|
45
47
|
"devDependencies": {
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "SEMLINT_NAMING_001",
|
|
3
|
-
"title": "Ambient naming convention consistency",
|
|
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
|
-
],
|
|
11
|
-
"prompt": "You are Semlint. Review ONLY the modified code in the provided DIFF and verify naming is consistent with the ambient naming conventions already used in surrounding code. Focus on identifier styles such as camelCase for variables/functions, PascalCase for classes/types/interfaces, and ALL_CAPS for constants where that convention is clearly established nearby. Flag only clear inconsistencies in newly added or renamed identifiers. Ignore untouched legacy naming unless directly impacted by the change. Return valid JSON only with shape {\"diagnostics\":[...]} and each diagnostic must include: rule_id, severity, message, file, line."
|
|
12
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "SEMLINT_PATTERN_002",
|
|
3
|
-
"title": "Ambient pattern is respected",
|
|
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": "You are Semlint. Review ONLY the modified code in the provided DIFF and check whether ambient implementation patterns are respected. Compare new or changed code against nearby established patterns for control flow, async handling, error propagation, data transformation, module boundaries, and function/class structure. Flag clear regressions where the proposed change deviates from consistent local patterns without obvious justification. Ignore untouched legacy code and acceptable intentional improvements. Return valid JSON only with shape {\"diagnostics\":[...]} and each diagnostic must include: rule_id, severity, message, file, line."
|
|
12
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "SEMLINT_SWE_003",
|
|
3
|
-
"title": "Obvious SWE mistakes",
|
|
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
|
-
],
|
|
11
|
-
"prompt": "You are Semlint. Review ONLY the modified code in the DIFF and find obvious software-engineering mistakes in the proposed change. Focus on clear issues such as dead code, side-effect misuse, swallowed errors, unsafe any-casts, accidental debug leftovers, contradictory conditions, and obvious maintainability hazards that are likely unintended (these are only examples, there are many more). Do not nitpick style or architecture unless the issue is clearly harmful. Report only high-signal findings tied to changed lines. Return valid JSON only with shape {\"diagnostics\":[...]} and each diagnostic must include: rule_id, severity, message, file, line."
|
|
12
|
-
}
|