neuro-cli 4.1.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/LICENSE +21 -0
- package/README.md +510 -0
- package/dist/advisor/advisor.d.ts +50 -0
- package/dist/advisor/advisor.js +178 -0
- package/dist/agents/base.d.ts +62 -0
- package/dist/agents/base.js +215 -0
- package/dist/agents/orchestrator.d.ts +46 -0
- package/dist/agents/orchestrator.js +192 -0
- package/dist/agents/team.d.ts +51 -0
- package/dist/agents/team.js +210 -0
- package/dist/api/models.d.ts +23 -0
- package/dist/api/models.js +514 -0
- package/dist/api/ollama.d.ts +153 -0
- package/dist/api/ollama.js +751 -0
- package/dist/api/openrouter.d.ts +55 -0
- package/dist/api/openrouter.js +223 -0
- package/dist/commands/commands.d.ts +43 -0
- package/dist/commands/commands.js +308 -0
- package/dist/config/config.d.ts +8 -0
- package/dist/config/config.js +311 -0
- package/dist/context/compaction.d.ts +54 -0
- package/dist/context/compaction.js +251 -0
- package/dist/context/custom-agents.d.ts +107 -0
- package/dist/context/custom-agents.js +397 -0
- package/dist/context/custom-tools.d.ts +120 -0
- package/dist/context/custom-tools.js +564 -0
- package/dist/context/git-checkpoint.d.ts +52 -0
- package/dist/context/git-checkpoint.js +240 -0
- package/dist/context/neuro-md.d.ts +48 -0
- package/dist/context/neuro-md.js +202 -0
- package/dist/context/neuroignore.d.ts +102 -0
- package/dist/context/neuroignore.js +441 -0
- package/dist/context/repo-map.d.ts +38 -0
- package/dist/context/repo-map.js +220 -0
- package/dist/context/skill-standard.d.ts +262 -0
- package/dist/context/skill-standard.js +1156 -0
- package/dist/context/skill-system.d.ts +75 -0
- package/dist/context/skill-system.js +578 -0
- package/dist/context/tree-sitter.d.ts +99 -0
- package/dist/context/tree-sitter.js +1956 -0
- package/dist/core/acp.d.ts +325 -0
- package/dist/core/acp.js +1498 -0
- package/dist/core/api-server.d.ts +143 -0
- package/dist/core/api-server.js +550 -0
- package/dist/core/approval.d.ts +81 -0
- package/dist/core/approval.js +432 -0
- package/dist/core/auto-compact.d.ts +127 -0
- package/dist/core/auto-compact.js +436 -0
- package/dist/core/auto-mode.d.ts +232 -0
- package/dist/core/auto-mode.js +831 -0
- package/dist/core/background-session.d.ts +166 -0
- package/dist/core/background-session.js +696 -0
- package/dist/core/cicd.d.ts +257 -0
- package/dist/core/cicd.js +1443 -0
- package/dist/core/cloud-sync.d.ts +156 -0
- package/dist/core/cloud-sync.js +582 -0
- package/dist/core/code-review.d.ts +132 -0
- package/dist/core/code-review.js +1191 -0
- package/dist/core/completion.d.ts +49 -0
- package/dist/core/completion.js +384 -0
- package/dist/core/context.d.ts +38 -0
- package/dist/core/context.js +144 -0
- package/dist/core/diff-preview.d.ts +35 -0
- package/dist/core/diff-preview.js +173 -0
- package/dist/core/doom-loop.d.ts +51 -0
- package/dist/core/doom-loop.js +179 -0
- package/dist/core/engine.d.ts +183 -0
- package/dist/core/engine.js +942 -0
- package/dist/core/extended-thinking.d.ts +103 -0
- package/dist/core/extended-thinking.js +269 -0
- package/dist/core/fallback.d.ts +54 -0
- package/dist/core/fallback.js +104 -0
- package/dist/core/git-worktree.d.ts +139 -0
- package/dist/core/git-worktree.js +614 -0
- package/dist/core/headless.d.ts +30 -0
- package/dist/core/headless.js +101 -0
- package/dist/core/i18n.d.ts +83 -0
- package/dist/core/i18n.js +739 -0
- package/dist/core/linting.d.ts +129 -0
- package/dist/core/linting.js +699 -0
- package/dist/core/model-router.d.ts +109 -0
- package/dist/core/model-router.js +581 -0
- package/dist/core/multi-model.d.ts +243 -0
- package/dist/core/multi-model.js +1099 -0
- package/dist/core/multi-session.d.ts +144 -0
- package/dist/core/multi-session.js +442 -0
- package/dist/core/multimodal.d.ts +125 -0
- package/dist/core/multimodal.js +286 -0
- package/dist/core/observability.d.ts +93 -0
- package/dist/core/observability.js +737 -0
- package/dist/core/os-sandbox.d.ts +122 -0
- package/dist/core/os-sandbox.js +1193 -0
- package/dist/core/outcome-grading.d.ts +228 -0
- package/dist/core/outcome-grading.js +1123 -0
- package/dist/core/output-styles.d.ts +57 -0
- package/dist/core/output-styles.js +382 -0
- package/dist/core/parallel-agents.d.ts +183 -0
- package/dist/core/parallel-agents.js +563 -0
- package/dist/core/plugin-bundle.d.ts +236 -0
- package/dist/core/plugin-bundle.js +887 -0
- package/dist/core/plugin-sdk.d.ts +139 -0
- package/dist/core/plugin-sdk.js +273 -0
- package/dist/core/prompt-cache.d.ts +163 -0
- package/dist/core/prompt-cache.js +599 -0
- package/dist/core/sandbox.d.ts +127 -0
- package/dist/core/sandbox.js +369 -0
- package/dist/core/scheduled-tasks.d.ts +151 -0
- package/dist/core/scheduled-tasks.js +502 -0
- package/dist/core/security-scanner.d.ts +160 -0
- package/dist/core/security-scanner.js +1494 -0
- package/dist/core/session.d.ts +83 -0
- package/dist/core/session.js +269 -0
- package/dist/core/shell-completion.d.ts +51 -0
- package/dist/core/shell-completion.js +674 -0
- package/dist/core/smart-monitor.d.ts +146 -0
- package/dist/core/smart-monitor.js +1199 -0
- package/dist/core/spec-driven.d.ts +233 -0
- package/dist/core/spec-driven.js +1485 -0
- package/dist/core/spending-warnings.d.ts +123 -0
- package/dist/core/spending-warnings.js +456 -0
- package/dist/core/sub-agent.d.ts +298 -0
- package/dist/core/sub-agent.js +1023 -0
- package/dist/core/telemetry.d.ts +157 -0
- package/dist/core/telemetry.js +412 -0
- package/dist/core/terminal-ux.d.ts +134 -0
- package/dist/core/terminal-ux.js +649 -0
- package/dist/core/testing.d.ts +146 -0
- package/dist/core/testing.js +1200 -0
- package/dist/core/types.d.ts +439 -0
- package/dist/core/types.js +6 -0
- package/dist/core/undo-redo.d.ts +112 -0
- package/dist/core/undo-redo.js +290 -0
- package/dist/core/updater.d.ts +159 -0
- package/dist/core/updater.js +608 -0
- package/dist/core/vim-mode.d.ts +151 -0
- package/dist/core/vim-mode.js +771 -0
- package/dist/core/voice.d.ts +137 -0
- package/dist/core/voice.js +538 -0
- package/dist/core/web-dashboard.d.ts +109 -0
- package/dist/core/web-dashboard.js +484 -0
- package/dist/hooks/hooks.d.ts +74 -0
- package/dist/hooks/hooks.js +160 -0
- package/dist/hooks/llm-evaluator.d.ts +165 -0
- package/dist/hooks/llm-evaluator.js +560 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1186 -0
- package/dist/lsp/lsp-manager.d.ts +63 -0
- package/dist/lsp/lsp-manager.js +351 -0
- package/dist/mcp/client.d.ts +133 -0
- package/dist/mcp/client.js +684 -0
- package/dist/mcp/mcp-apps.d.ts +70 -0
- package/dist/mcp/mcp-apps.js +1007 -0
- package/dist/tools/bash.d.ts +5 -0
- package/dist/tools/bash.js +195 -0
- package/dist/tools/browser.d.ts +92 -0
- package/dist/tools/browser.js +1570 -0
- package/dist/tools/extended.d.ts +6 -0
- package/dist/tools/extended.js +191 -0
- package/dist/tools/file.d.ts +10 -0
- package/dist/tools/file.js +382 -0
- package/dist/tools/github.d.ts +389 -0
- package/dist/tools/github.js +759 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.js +40 -0
- package/dist/tools/memory.d.ts +6 -0
- package/dist/tools/memory.js +197 -0
- package/dist/tools/registry.d.ts +29 -0
- package/dist/tools/registry.js +64 -0
- package/dist/tools/web.d.ts +6 -0
- package/dist/tools/web.js +150 -0
- package/dist/ui/renderer.d.ts +97 -0
- package/dist/ui/renderer.js +279 -0
- package/dist/ui/theme.d.ts +27 -0
- package/dist/ui/theme.js +106 -0
- package/package.json +83 -0
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// NeuroCLI - Linting Integration
|
|
3
|
+
// Automatic linter detection, execution, and auto-fix
|
|
4
|
+
// Supports ESLint, Prettier, Ruff, Pylint, Flake8,
|
|
5
|
+
// golangci-lint, Clippy, and standard linters per language
|
|
6
|
+
// ============================================================
|
|
7
|
+
import { join, resolve, extname, relative } from 'path';
|
|
8
|
+
import { existsSync } from 'fs';
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
// -----------------------------------------------------------
|
|
11
|
+
// Linter definitions
|
|
12
|
+
// -----------------------------------------------------------
|
|
13
|
+
const LINTERS = [
|
|
14
|
+
// JavaScript / TypeScript
|
|
15
|
+
{
|
|
16
|
+
name: 'eslint',
|
|
17
|
+
language: 'javascript',
|
|
18
|
+
configFiles: [
|
|
19
|
+
'.eslintrc',
|
|
20
|
+
'.eslintrc.js',
|
|
21
|
+
'.eslintrc.cjs',
|
|
22
|
+
'.eslintrc.mjs',
|
|
23
|
+
'.eslintrc.json',
|
|
24
|
+
'.eslintrc.yaml',
|
|
25
|
+
'.eslintrc.yml',
|
|
26
|
+
],
|
|
27
|
+
commands: {
|
|
28
|
+
lint: ['npx', 'eslint', '--format', 'json'],
|
|
29
|
+
fix: ['npx', 'eslint', '--fix', '--format', 'json'],
|
|
30
|
+
},
|
|
31
|
+
parseOutput: (stdout, _stderr, rootDir) => {
|
|
32
|
+
const issues = [];
|
|
33
|
+
try {
|
|
34
|
+
const results = JSON.parse(stdout);
|
|
35
|
+
for (const fileResult of results) {
|
|
36
|
+
for (const msg of fileResult.messages) {
|
|
37
|
+
issues.push({
|
|
38
|
+
file: relative(rootDir, fileResult.filePath),
|
|
39
|
+
line: msg.line,
|
|
40
|
+
column: msg.column,
|
|
41
|
+
severity: msg.severity === 2 ? 'error' : msg.severity === 1 ? 'warning' : 'info',
|
|
42
|
+
rule: msg.ruleId ?? 'unknown',
|
|
43
|
+
message: msg.message,
|
|
44
|
+
fixable: msg.fix !== undefined,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Parse error — return empty
|
|
51
|
+
}
|
|
52
|
+
return issues;
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
// Python - Ruff
|
|
56
|
+
{
|
|
57
|
+
name: 'ruff',
|
|
58
|
+
language: 'python',
|
|
59
|
+
configFiles: ['ruff.toml', 'pyproject.toml'],
|
|
60
|
+
commands: {
|
|
61
|
+
lint: ['ruff', 'check', '--output-format', 'json'],
|
|
62
|
+
fix: ['ruff', 'check', '--fix', '--output-format', 'json'],
|
|
63
|
+
format: ['ruff', 'format'],
|
|
64
|
+
},
|
|
65
|
+
parseOutput: (stdout, _stderr, rootDir) => {
|
|
66
|
+
const issues = [];
|
|
67
|
+
try {
|
|
68
|
+
const results = JSON.parse(stdout);
|
|
69
|
+
for (const item of results) {
|
|
70
|
+
issues.push({
|
|
71
|
+
file: relative(rootDir, item.filename),
|
|
72
|
+
line: item.location.row,
|
|
73
|
+
column: item.location.column,
|
|
74
|
+
severity: 'warning',
|
|
75
|
+
rule: item.code,
|
|
76
|
+
message: item.message,
|
|
77
|
+
fixable: item.fix !== undefined,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Parse error
|
|
83
|
+
}
|
|
84
|
+
return issues;
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
// Python - Pylint
|
|
88
|
+
{
|
|
89
|
+
name: 'pylint',
|
|
90
|
+
language: 'python',
|
|
91
|
+
configFiles: ['.pylintrc', 'pyproject.toml', 'setup.cfg'],
|
|
92
|
+
commands: {
|
|
93
|
+
lint: ['pylint', '--output-format=json'],
|
|
94
|
+
fix: undefined,
|
|
95
|
+
},
|
|
96
|
+
parseOutput: (stdout, _stderr, rootDir) => {
|
|
97
|
+
const issues = [];
|
|
98
|
+
try {
|
|
99
|
+
const results = JSON.parse(stdout);
|
|
100
|
+
for (const item of results) {
|
|
101
|
+
const sev = item.type === 'error' || item.type === 'fatal'
|
|
102
|
+
? 'error'
|
|
103
|
+
: item.type === 'warning'
|
|
104
|
+
? 'warning'
|
|
105
|
+
: 'info';
|
|
106
|
+
issues.push({
|
|
107
|
+
file: relative(rootDir, item.path),
|
|
108
|
+
line: item.line,
|
|
109
|
+
column: item.column + 1,
|
|
110
|
+
severity: sev,
|
|
111
|
+
rule: item.symbol,
|
|
112
|
+
message: item.message,
|
|
113
|
+
fixable: false,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Parse error
|
|
119
|
+
}
|
|
120
|
+
return issues;
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
// Python - Flake8
|
|
124
|
+
{
|
|
125
|
+
name: 'flake8',
|
|
126
|
+
language: 'python',
|
|
127
|
+
configFiles: ['.flake8', 'setup.cfg', 'tox.ini'],
|
|
128
|
+
commands: {
|
|
129
|
+
lint: ['flake8', '--format=json'],
|
|
130
|
+
},
|
|
131
|
+
parseOutput: (stdout, _stderr, rootDir) => {
|
|
132
|
+
const issues = [];
|
|
133
|
+
try {
|
|
134
|
+
const data = JSON.parse(stdout);
|
|
135
|
+
for (const [filePath, violations] of Object.entries(data)) {
|
|
136
|
+
for (const v of violations) {
|
|
137
|
+
issues.push({
|
|
138
|
+
file: relative(rootDir, filePath),
|
|
139
|
+
line: v.row,
|
|
140
|
+
column: v.col,
|
|
141
|
+
severity: v.code.startsWith('E') || v.code.startsWith('F') ? 'error' : 'warning',
|
|
142
|
+
rule: v.code,
|
|
143
|
+
message: v.message,
|
|
144
|
+
fixable: false,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// Try line-by-line parsing as fallback
|
|
151
|
+
const lines = stdout.split('\n');
|
|
152
|
+
const pattern = /^(.+):(\d+):(\d+):\s+(\w+)\s+(.+)$/;
|
|
153
|
+
for (const line of lines) {
|
|
154
|
+
const match = line.match(pattern);
|
|
155
|
+
if (match) {
|
|
156
|
+
issues.push({
|
|
157
|
+
file: relative(rootDir, match[1]),
|
|
158
|
+
line: parseInt(match[2], 10),
|
|
159
|
+
column: parseInt(match[3], 10),
|
|
160
|
+
severity: match[4].startsWith('E') || match[4].startsWith('F') ? 'error' : 'warning',
|
|
161
|
+
rule: match[4],
|
|
162
|
+
message: match[5],
|
|
163
|
+
fixable: false,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return issues;
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
// Go - golangci-lint
|
|
172
|
+
{
|
|
173
|
+
name: 'golangci-lint',
|
|
174
|
+
language: 'go',
|
|
175
|
+
configFiles: ['.golangci.yml', '.golangci.yaml', '.golangci.json', '.golangci.toml'],
|
|
176
|
+
commands: {
|
|
177
|
+
lint: ['golangci-lint', 'run', '--out-format=json'],
|
|
178
|
+
fix: ['golangci-lint', 'run', '--fix', '--out-format=json'],
|
|
179
|
+
},
|
|
180
|
+
parseOutput: (stdout, _stderr, rootDir) => {
|
|
181
|
+
const issues = [];
|
|
182
|
+
try {
|
|
183
|
+
const data = JSON.parse(stdout);
|
|
184
|
+
for (const issue of data.Issues ?? []) {
|
|
185
|
+
issues.push({
|
|
186
|
+
file: relative(rootDir, issue.Pos.Filename),
|
|
187
|
+
line: issue.Pos.Line,
|
|
188
|
+
column: issue.Pos.Column,
|
|
189
|
+
severity: issue.Severity === 'error' ? 'error' : 'warning',
|
|
190
|
+
rule: issue.FromLinter,
|
|
191
|
+
message: issue.Text,
|
|
192
|
+
fixable: false,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// Parse error
|
|
198
|
+
}
|
|
199
|
+
return issues;
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
// Rust - Clippy
|
|
203
|
+
{
|
|
204
|
+
name: 'clippy',
|
|
205
|
+
language: 'rust',
|
|
206
|
+
configFiles: ['clippy.toml', '.clippy.toml'],
|
|
207
|
+
commands: {
|
|
208
|
+
lint: ['cargo', 'clippy', '--message-format=json'],
|
|
209
|
+
fix: ['cargo', 'clippy', '--fix', '--allow-dirty', '--message-format=json'],
|
|
210
|
+
},
|
|
211
|
+
parseOutput: (stdout, _stderr, rootDir) => {
|
|
212
|
+
const issues = [];
|
|
213
|
+
const lines = stdout.split('\n');
|
|
214
|
+
for (const line of lines) {
|
|
215
|
+
try {
|
|
216
|
+
const msg = JSON.parse(line);
|
|
217
|
+
if (msg.reason === 'compiler-message' && msg.message) {
|
|
218
|
+
const span = msg.message.spans?.[0];
|
|
219
|
+
issues.push({
|
|
220
|
+
file: span ? relative(rootDir, span.file_name) : 'unknown',
|
|
221
|
+
line: span?.line_start ?? 0,
|
|
222
|
+
column: span?.column_start ?? 0,
|
|
223
|
+
severity: msg.message.level === 'error' ? 'error' : 'warning',
|
|
224
|
+
rule: msg.message.code?.code ?? 'clippy',
|
|
225
|
+
message: msg.message.rendered?.split('\n')[0] ?? '',
|
|
226
|
+
fixable: false,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
// Not a JSON line — skip
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return issues;
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
];
|
|
238
|
+
// Formatter definitions
|
|
239
|
+
const FORMATTERS = [
|
|
240
|
+
{
|
|
241
|
+
name: 'prettier',
|
|
242
|
+
configFiles: [
|
|
243
|
+
'.prettierrc',
|
|
244
|
+
'.prettierrc.js',
|
|
245
|
+
'.prettierrc.cjs',
|
|
246
|
+
'.prettierrc.mjs',
|
|
247
|
+
'.prettierrc.json',
|
|
248
|
+
'.prettierrc.yaml',
|
|
249
|
+
'.prettierrc.yml',
|
|
250
|
+
'prettier.config.js',
|
|
251
|
+
'prettier.config.cjs',
|
|
252
|
+
'prettier.config.mjs',
|
|
253
|
+
],
|
|
254
|
+
command: ['npx', 'prettier', '--write'],
|
|
255
|
+
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json', '.css', '.scss', '.less', '.html', '.md', '.yaml', '.yml', '.graphql'],
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
name: 'ruff-format',
|
|
259
|
+
configFiles: ['ruff.toml', 'pyproject.toml'],
|
|
260
|
+
command: ['ruff', 'format'],
|
|
261
|
+
extensions: ['.py', '.pyi'],
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
name: 'gofmt',
|
|
265
|
+
configFiles: [],
|
|
266
|
+
command: ['gofmt', '-w'],
|
|
267
|
+
extensions: ['.go'],
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: 'rustfmt',
|
|
271
|
+
configFiles: ['rustfmt.toml', '.rustfmt.toml'],
|
|
272
|
+
command: ['cargo', 'fmt'],
|
|
273
|
+
extensions: ['.rs'],
|
|
274
|
+
},
|
|
275
|
+
];
|
|
276
|
+
// Language → extension mapping for standard linters
|
|
277
|
+
const LANGUAGE_EXTENSIONS = {
|
|
278
|
+
javascript: ['.js', '.jsx', '.mjs', '.cjs'],
|
|
279
|
+
typescript: ['.ts', '.tsx', '.mts', '.cts'],
|
|
280
|
+
python: ['.py', '.pyi'],
|
|
281
|
+
go: ['.go'],
|
|
282
|
+
rust: ['.rs'],
|
|
283
|
+
java: ['.java'],
|
|
284
|
+
c: ['.c', '.h'],
|
|
285
|
+
cpp: ['.cpp', '.cc', '.cxx', '.hpp', '.hxx'],
|
|
286
|
+
ruby: ['.rb'],
|
|
287
|
+
shell: ['.sh', '.bash'],
|
|
288
|
+
};
|
|
289
|
+
// Default configuration
|
|
290
|
+
const DEFAULT_LINTER_CONFIG = {
|
|
291
|
+
enabled: true,
|
|
292
|
+
autoRunOnChange: false,
|
|
293
|
+
autoFix: false,
|
|
294
|
+
failOnError: true,
|
|
295
|
+
timeout: 60_000,
|
|
296
|
+
excludePatterns: [
|
|
297
|
+
'node_modules/**',
|
|
298
|
+
'.git/**',
|
|
299
|
+
'dist/**',
|
|
300
|
+
'build/**',
|
|
301
|
+
'__pycache__/**',
|
|
302
|
+
'target/**',
|
|
303
|
+
'.next/**',
|
|
304
|
+
'vendor/**',
|
|
305
|
+
],
|
|
306
|
+
};
|
|
307
|
+
// -----------------------------------------------------------
|
|
308
|
+
// LintingIntegration class
|
|
309
|
+
// -----------------------------------------------------------
|
|
310
|
+
export class LintingIntegration {
|
|
311
|
+
config;
|
|
312
|
+
projectRoot;
|
|
313
|
+
detectedLinters = new Map();
|
|
314
|
+
detectedFormatter = null;
|
|
315
|
+
cachedIssues = [];
|
|
316
|
+
callbacks = [];
|
|
317
|
+
lintersDetected = false;
|
|
318
|
+
constructor(projectRoot, config) {
|
|
319
|
+
this.projectRoot = projectRoot ?? process.cwd();
|
|
320
|
+
this.config = { ...DEFAULT_LINTER_CONFIG, ...config };
|
|
321
|
+
}
|
|
322
|
+
// ----------------------------------------------------------
|
|
323
|
+
// Public API
|
|
324
|
+
// ----------------------------------------------------------
|
|
325
|
+
/**
|
|
326
|
+
* Run linter on a specific file or the entire project.
|
|
327
|
+
* Returns a LintResult with all detected issues.
|
|
328
|
+
*/
|
|
329
|
+
async runLint(filePath, fix) {
|
|
330
|
+
if (!this.config.enabled) {
|
|
331
|
+
return this.emptyResult('disabled');
|
|
332
|
+
}
|
|
333
|
+
this.ensureDetected();
|
|
334
|
+
if (this.detectedLinters.size === 0) {
|
|
335
|
+
return this.emptyResult('none');
|
|
336
|
+
}
|
|
337
|
+
const shouldFix = fix ?? this.config.autoFix;
|
|
338
|
+
const startTime = Date.now();
|
|
339
|
+
let allIssues = [];
|
|
340
|
+
let totalFixed = 0;
|
|
341
|
+
let totalFiles = 0;
|
|
342
|
+
let primaryLinter = '';
|
|
343
|
+
for (const [name, linter] of this.detectedLinters) {
|
|
344
|
+
// Determine which command to use
|
|
345
|
+
let commandParts;
|
|
346
|
+
if (shouldFix && linter.commands.fix) {
|
|
347
|
+
commandParts = [...linter.commands.fix];
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
commandParts = [...linter.commands.lint];
|
|
351
|
+
}
|
|
352
|
+
// Append file path if provided
|
|
353
|
+
if (filePath) {
|
|
354
|
+
commandParts.push(resolve(this.projectRoot, filePath));
|
|
355
|
+
}
|
|
356
|
+
// Add exclude patterns
|
|
357
|
+
for (const pattern of this.config.excludePatterns) {
|
|
358
|
+
if (name === 'eslint') {
|
|
359
|
+
commandParts.push('--ignore-pattern', pattern);
|
|
360
|
+
}
|
|
361
|
+
else if (name === 'ruff') {
|
|
362
|
+
// Ruff uses different exclude syntax
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
const { stdout, stderr, exitCode } = await this.execCommand(commandParts, this.projectRoot, this.config.timeout);
|
|
367
|
+
const issues = linter.parseOutput(stdout, stderr, this.projectRoot);
|
|
368
|
+
allIssues = allIssues.concat(issues);
|
|
369
|
+
totalFiles += this.countLintedFiles(issues);
|
|
370
|
+
if (!primaryLinter)
|
|
371
|
+
primaryLinter = name;
|
|
372
|
+
// If fixing, count how many were auto-fixed
|
|
373
|
+
if (shouldFix) {
|
|
374
|
+
const preFixCount = this.cachedIssues.filter(i => i.fixable).length;
|
|
375
|
+
totalFixed += Math.max(0, preFixCount - issues.length);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
// Linter may exit with non-zero when issues found — that's expected
|
|
380
|
+
if (error instanceof Error && 'stdout' in error) {
|
|
381
|
+
const execErr = error;
|
|
382
|
+
if (execErr.stdout) {
|
|
383
|
+
const issues = linter.parseOutput(execErr.stdout, execErr.stderr ?? '', this.projectRoot);
|
|
384
|
+
allIssues = allIssues.concat(issues);
|
|
385
|
+
totalFiles += this.countLintedFiles(issues);
|
|
386
|
+
if (!primaryLinter)
|
|
387
|
+
primaryLinter = name;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// Silently skip linters that fail to execute
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
// Filter by exclude patterns
|
|
394
|
+
allIssues = this.filterExcludedIssues(allIssues);
|
|
395
|
+
const duration = Date.now() - startTime;
|
|
396
|
+
this.cachedIssues = allIssues;
|
|
397
|
+
const result = {
|
|
398
|
+
success: allIssues.filter(i => i.severity === 'error').length === 0,
|
|
399
|
+
issues: allIssues,
|
|
400
|
+
fixed: totalFixed,
|
|
401
|
+
totalFiles,
|
|
402
|
+
duration,
|
|
403
|
+
linter: primaryLinter || 'unknown',
|
|
404
|
+
};
|
|
405
|
+
this.emit(result);
|
|
406
|
+
return result;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Detect which linters are configured in the project root.
|
|
410
|
+
* Returns an array of detected linter names.
|
|
411
|
+
*/
|
|
412
|
+
detectLinter(projectRoot) {
|
|
413
|
+
const root = projectRoot ?? this.projectRoot;
|
|
414
|
+
const detected = [];
|
|
415
|
+
for (const linter of LINTERS) {
|
|
416
|
+
const hasConfig = linter.configFiles.some(cfg => existsSync(join(root, cfg)));
|
|
417
|
+
// Also check if the linter binary is available
|
|
418
|
+
const hasBinary = this.isCommandAvailable(linter.commands.lint[0]);
|
|
419
|
+
if (hasConfig || hasBinary) {
|
|
420
|
+
detected.push(linter.name);
|
|
421
|
+
this.detectedLinters.set(linter.name, linter);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// If no linters found but has package.json, assume ESLint for JS projects
|
|
425
|
+
if (detected.length === 0 && existsSync(join(root, 'package.json'))) {
|
|
426
|
+
const defaultEslint = LINTERS.find(l => l.name === 'eslint');
|
|
427
|
+
this.detectedLinters.set('eslint', defaultEslint);
|
|
428
|
+
detected.push('eslint');
|
|
429
|
+
}
|
|
430
|
+
this.lintersDetected = true;
|
|
431
|
+
return detected;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Auto-fix linting issues for a specific file or the entire project.
|
|
435
|
+
*/
|
|
436
|
+
async fixIssues(filePath) {
|
|
437
|
+
return this.runLint(filePath, true);
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Get all current lint issues, optionally filtered by file.
|
|
441
|
+
* If no cached issues exist, runs the linter first.
|
|
442
|
+
*/
|
|
443
|
+
async getIssues(filePath) {
|
|
444
|
+
if (this.cachedIssues.length === 0) {
|
|
445
|
+
await this.runLint(filePath);
|
|
446
|
+
}
|
|
447
|
+
if (filePath) {
|
|
448
|
+
const absPath = resolve(this.projectRoot, filePath);
|
|
449
|
+
return this.cachedIssues.filter(i => resolve(this.projectRoot, i.file) === absPath);
|
|
450
|
+
}
|
|
451
|
+
return [...this.cachedIssues];
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Format a file using the project's configured formatter.
|
|
455
|
+
* Returns true if formatting succeeded.
|
|
456
|
+
*/
|
|
457
|
+
async formatFile(filePath) {
|
|
458
|
+
this.ensureDetected();
|
|
459
|
+
const formatter = this.detectFormatter(this.projectRoot);
|
|
460
|
+
if (!formatter) {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
const ext = extname(filePath);
|
|
464
|
+
if (formatter.extensions.length > 0 && !formatter.extensions.includes(ext)) {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
const commandParts = [...formatter.command, resolve(this.projectRoot, filePath)];
|
|
468
|
+
try {
|
|
469
|
+
await this.execCommand(commandParts, this.projectRoot, this.config.timeout);
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Detect the configured formatter for the project.
|
|
478
|
+
* Returns the FormatterInfo or null if none found.
|
|
479
|
+
*/
|
|
480
|
+
detectFormatter(projectRoot) {
|
|
481
|
+
const root = projectRoot ?? this.projectRoot;
|
|
482
|
+
for (const formatter of FORMATTERS) {
|
|
483
|
+
const hasConfig = formatter.configFiles.some(cfg => existsSync(join(root, cfg)));
|
|
484
|
+
const hasBinary = this.isCommandAvailable(formatter.command[0]);
|
|
485
|
+
if (hasConfig || hasBinary) {
|
|
486
|
+
this.detectedFormatter = formatter;
|
|
487
|
+
return formatter;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Register a callback for lint results.
|
|
494
|
+
*/
|
|
495
|
+
onLintResult(callback) {
|
|
496
|
+
this.callbacks.push(callback);
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Remove a previously registered callback.
|
|
500
|
+
*/
|
|
501
|
+
offLintResult(callback) {
|
|
502
|
+
this.callbacks = this.callbacks.filter(cb => cb !== callback);
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Get the current linter configuration.
|
|
506
|
+
*/
|
|
507
|
+
getConfig() {
|
|
508
|
+
return { ...this.config };
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Update the linter configuration.
|
|
512
|
+
*/
|
|
513
|
+
updateConfig(updates) {
|
|
514
|
+
Object.assign(this.config, updates);
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Clear the cached issues.
|
|
518
|
+
*/
|
|
519
|
+
clearCache() {
|
|
520
|
+
this.cachedIssues = [];
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Get the list of detected linter names.
|
|
524
|
+
*/
|
|
525
|
+
getDetectedLinters() {
|
|
526
|
+
this.ensureDetected();
|
|
527
|
+
return Array.from(this.detectedLinters.keys());
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Get the detected formatter name, or null.
|
|
531
|
+
*/
|
|
532
|
+
getDetectedFormatter() {
|
|
533
|
+
return this.detectedFormatter?.name ?? null;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Determine the primary language of a file based on its extension.
|
|
537
|
+
*/
|
|
538
|
+
getLanguageForFile(filePath) {
|
|
539
|
+
const ext = extname(filePath).toLowerCase();
|
|
540
|
+
for (const [lang, extensions] of Object.entries(LANGUAGE_EXTENSIONS)) {
|
|
541
|
+
if (extensions.includes(ext))
|
|
542
|
+
return lang;
|
|
543
|
+
}
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Check if a specific linter is available and configured.
|
|
548
|
+
*/
|
|
549
|
+
isLinterAvailable(linterName) {
|
|
550
|
+
this.ensureDetected();
|
|
551
|
+
return this.detectedLinters.has(linterName);
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Print a summary of lint results.
|
|
555
|
+
*/
|
|
556
|
+
printSummary(result) {
|
|
557
|
+
const errors = result.issues.filter(i => i.severity === 'error').length;
|
|
558
|
+
const warnings = result.issues.filter(i => i.severity === 'warning').length;
|
|
559
|
+
const infos = result.issues.filter(i => i.severity === 'info').length;
|
|
560
|
+
console.log('');
|
|
561
|
+
console.log('--- Lint Summary ---');
|
|
562
|
+
console.log(` Linter: ${result.linter}`);
|
|
563
|
+
console.log(` Duration: ${result.duration}ms`);
|
|
564
|
+
console.log(` Files: ${result.totalFiles}`);
|
|
565
|
+
console.log(` Errors: ${errors}`);
|
|
566
|
+
console.log(` Warnings: ${warnings}`);
|
|
567
|
+
console.log(` Info: ${infos}`);
|
|
568
|
+
if (result.fixed > 0) {
|
|
569
|
+
console.log(` Auto-fixed:${result.fixed}`);
|
|
570
|
+
}
|
|
571
|
+
console.log(` Status: ${result.success ? 'PASS' : 'FAIL'}`);
|
|
572
|
+
console.log('');
|
|
573
|
+
// Group issues by file
|
|
574
|
+
const byFile = new Map();
|
|
575
|
+
for (const issue of result.issues) {
|
|
576
|
+
const arr = byFile.get(issue.file) ?? [];
|
|
577
|
+
arr.push(issue);
|
|
578
|
+
byFile.set(issue.file, arr);
|
|
579
|
+
}
|
|
580
|
+
if (byFile.size > 0) {
|
|
581
|
+
for (const [file, issues] of byFile) {
|
|
582
|
+
console.log(` ${file}:`);
|
|
583
|
+
for (const issue of issues.slice(0, 20)) {
|
|
584
|
+
const sevLabel = issue.severity === 'error'
|
|
585
|
+
? 'E'
|
|
586
|
+
: issue.severity === 'warning'
|
|
587
|
+
? 'W'
|
|
588
|
+
: 'I';
|
|
589
|
+
console.log(` ${sevLabel} ${issue.line}:${issue.column} ${issue.rule} — ${issue.message}${issue.fixable ? ' [fixable]' : ''}`);
|
|
590
|
+
}
|
|
591
|
+
if (issues.length > 20) {
|
|
592
|
+
console.log(` ... and ${issues.length - 20} more`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
console.log('');
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
// ----------------------------------------------------------
|
|
599
|
+
// Private helpers
|
|
600
|
+
// ----------------------------------------------------------
|
|
601
|
+
ensureDetected() {
|
|
602
|
+
if (!this.lintersDetected) {
|
|
603
|
+
this.detectLinter(this.projectRoot);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
emit(result) {
|
|
607
|
+
for (const cb of this.callbacks) {
|
|
608
|
+
try {
|
|
609
|
+
cb(result);
|
|
610
|
+
}
|
|
611
|
+
catch {
|
|
612
|
+
// Callback errors should not interrupt the flow
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
emptyResult(reason) {
|
|
617
|
+
return {
|
|
618
|
+
success: reason !== 'fail',
|
|
619
|
+
issues: [],
|
|
620
|
+
fixed: 0,
|
|
621
|
+
totalFiles: 0,
|
|
622
|
+
duration: 0,
|
|
623
|
+
linter: reason,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
countLintedFiles(issues) {
|
|
627
|
+
const uniqueFiles = new Set(issues.map(i => i.file));
|
|
628
|
+
return uniqueFiles.size;
|
|
629
|
+
}
|
|
630
|
+
filterExcludedIssues(issues) {
|
|
631
|
+
if (this.config.excludePatterns.length === 0)
|
|
632
|
+
return issues;
|
|
633
|
+
return issues.filter(issue => {
|
|
634
|
+
for (const pattern of this.config.excludePatterns) {
|
|
635
|
+
const globRegex = this.globToRegex(pattern);
|
|
636
|
+
if (globRegex.test(issue.file))
|
|
637
|
+
return false;
|
|
638
|
+
}
|
|
639
|
+
return true;
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
globToRegex(pattern) {
|
|
643
|
+
const escaped = pattern
|
|
644
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
645
|
+
.replace(/\*\*/g, '{{GLOBSTAR}}')
|
|
646
|
+
.replace(/\*/g, '[^/]*')
|
|
647
|
+
.replace(/\?/g, '[^/]')
|
|
648
|
+
.replace(/\{\{GLOBSTAR\}\}/g, '.*');
|
|
649
|
+
return new RegExp('^' + escaped + '$');
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Execute a command and return its output.
|
|
653
|
+
* Rejects on spawn errors; resolves with stdout/stderr even on non-zero exit.
|
|
654
|
+
*/
|
|
655
|
+
execCommand(commandParts, cwd, timeout) {
|
|
656
|
+
return new Promise((resolve, reject) => {
|
|
657
|
+
const [cmd, ...args] = commandParts;
|
|
658
|
+
try {
|
|
659
|
+
const result = execSync(`${cmd} ${args.map(a => `"${a}"`).join(' ')}`, {
|
|
660
|
+
cwd,
|
|
661
|
+
timeout,
|
|
662
|
+
encoding: 'utf-8',
|
|
663
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
664
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
665
|
+
});
|
|
666
|
+
resolve({ stdout: result, stderr: '', exitCode: 0 });
|
|
667
|
+
}
|
|
668
|
+
catch (error) {
|
|
669
|
+
const execError = error;
|
|
670
|
+
// Non-zero exit code from linter is normal (issues found)
|
|
671
|
+
if (execError.stdout || execError.stderr) {
|
|
672
|
+
resolve({
|
|
673
|
+
stdout: typeof execError.stdout === 'string' ? execError.stdout : (execError.stdout?.toString('utf-8') ?? ''),
|
|
674
|
+
stderr: typeof execError.stderr === 'string' ? execError.stderr : (execError.stderr?.toString('utf-8') ?? ''),
|
|
675
|
+
exitCode: execError.status ?? 1,
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
reject(error);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Check if a command-line tool is available on the system PATH.
|
|
686
|
+
*/
|
|
687
|
+
isCommandAvailable(command) {
|
|
688
|
+
try {
|
|
689
|
+
const isWin = process.platform === 'win32';
|
|
690
|
+
const checkCmd = isWin ? `where ${command}` : `which ${command} 2>/dev/null`;
|
|
691
|
+
execSync(checkCmd, { encoding: 'utf-8', timeout: 5_000, stdio: 'pipe' });
|
|
692
|
+
return true;
|
|
693
|
+
}
|
|
694
|
+
catch {
|
|
695
|
+
return false;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
//# sourceMappingURL=linting.js.map
|