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.
Files changed (175) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +510 -0
  3. package/dist/advisor/advisor.d.ts +50 -0
  4. package/dist/advisor/advisor.js +178 -0
  5. package/dist/agents/base.d.ts +62 -0
  6. package/dist/agents/base.js +215 -0
  7. package/dist/agents/orchestrator.d.ts +46 -0
  8. package/dist/agents/orchestrator.js +192 -0
  9. package/dist/agents/team.d.ts +51 -0
  10. package/dist/agents/team.js +210 -0
  11. package/dist/api/models.d.ts +23 -0
  12. package/dist/api/models.js +514 -0
  13. package/dist/api/ollama.d.ts +153 -0
  14. package/dist/api/ollama.js +751 -0
  15. package/dist/api/openrouter.d.ts +55 -0
  16. package/dist/api/openrouter.js +223 -0
  17. package/dist/commands/commands.d.ts +43 -0
  18. package/dist/commands/commands.js +308 -0
  19. package/dist/config/config.d.ts +8 -0
  20. package/dist/config/config.js +311 -0
  21. package/dist/context/compaction.d.ts +54 -0
  22. package/dist/context/compaction.js +251 -0
  23. package/dist/context/custom-agents.d.ts +107 -0
  24. package/dist/context/custom-agents.js +397 -0
  25. package/dist/context/custom-tools.d.ts +120 -0
  26. package/dist/context/custom-tools.js +564 -0
  27. package/dist/context/git-checkpoint.d.ts +52 -0
  28. package/dist/context/git-checkpoint.js +240 -0
  29. package/dist/context/neuro-md.d.ts +48 -0
  30. package/dist/context/neuro-md.js +202 -0
  31. package/dist/context/neuroignore.d.ts +102 -0
  32. package/dist/context/neuroignore.js +441 -0
  33. package/dist/context/repo-map.d.ts +38 -0
  34. package/dist/context/repo-map.js +220 -0
  35. package/dist/context/skill-standard.d.ts +262 -0
  36. package/dist/context/skill-standard.js +1156 -0
  37. package/dist/context/skill-system.d.ts +75 -0
  38. package/dist/context/skill-system.js +578 -0
  39. package/dist/context/tree-sitter.d.ts +99 -0
  40. package/dist/context/tree-sitter.js +1956 -0
  41. package/dist/core/acp.d.ts +325 -0
  42. package/dist/core/acp.js +1498 -0
  43. package/dist/core/api-server.d.ts +143 -0
  44. package/dist/core/api-server.js +550 -0
  45. package/dist/core/approval.d.ts +81 -0
  46. package/dist/core/approval.js +432 -0
  47. package/dist/core/auto-compact.d.ts +127 -0
  48. package/dist/core/auto-compact.js +436 -0
  49. package/dist/core/auto-mode.d.ts +232 -0
  50. package/dist/core/auto-mode.js +831 -0
  51. package/dist/core/background-session.d.ts +166 -0
  52. package/dist/core/background-session.js +696 -0
  53. package/dist/core/cicd.d.ts +257 -0
  54. package/dist/core/cicd.js +1443 -0
  55. package/dist/core/cloud-sync.d.ts +156 -0
  56. package/dist/core/cloud-sync.js +582 -0
  57. package/dist/core/code-review.d.ts +132 -0
  58. package/dist/core/code-review.js +1191 -0
  59. package/dist/core/completion.d.ts +49 -0
  60. package/dist/core/completion.js +384 -0
  61. package/dist/core/context.d.ts +38 -0
  62. package/dist/core/context.js +144 -0
  63. package/dist/core/diff-preview.d.ts +35 -0
  64. package/dist/core/diff-preview.js +173 -0
  65. package/dist/core/doom-loop.d.ts +51 -0
  66. package/dist/core/doom-loop.js +179 -0
  67. package/dist/core/engine.d.ts +183 -0
  68. package/dist/core/engine.js +942 -0
  69. package/dist/core/extended-thinking.d.ts +103 -0
  70. package/dist/core/extended-thinking.js +269 -0
  71. package/dist/core/fallback.d.ts +54 -0
  72. package/dist/core/fallback.js +104 -0
  73. package/dist/core/git-worktree.d.ts +139 -0
  74. package/dist/core/git-worktree.js +614 -0
  75. package/dist/core/headless.d.ts +30 -0
  76. package/dist/core/headless.js +101 -0
  77. package/dist/core/i18n.d.ts +83 -0
  78. package/dist/core/i18n.js +739 -0
  79. package/dist/core/linting.d.ts +129 -0
  80. package/dist/core/linting.js +699 -0
  81. package/dist/core/model-router.d.ts +109 -0
  82. package/dist/core/model-router.js +581 -0
  83. package/dist/core/multi-model.d.ts +243 -0
  84. package/dist/core/multi-model.js +1099 -0
  85. package/dist/core/multi-session.d.ts +144 -0
  86. package/dist/core/multi-session.js +442 -0
  87. package/dist/core/multimodal.d.ts +125 -0
  88. package/dist/core/multimodal.js +286 -0
  89. package/dist/core/observability.d.ts +93 -0
  90. package/dist/core/observability.js +737 -0
  91. package/dist/core/os-sandbox.d.ts +122 -0
  92. package/dist/core/os-sandbox.js +1193 -0
  93. package/dist/core/outcome-grading.d.ts +228 -0
  94. package/dist/core/outcome-grading.js +1123 -0
  95. package/dist/core/output-styles.d.ts +57 -0
  96. package/dist/core/output-styles.js +382 -0
  97. package/dist/core/parallel-agents.d.ts +183 -0
  98. package/dist/core/parallel-agents.js +563 -0
  99. package/dist/core/plugin-bundle.d.ts +236 -0
  100. package/dist/core/plugin-bundle.js +887 -0
  101. package/dist/core/plugin-sdk.d.ts +139 -0
  102. package/dist/core/plugin-sdk.js +273 -0
  103. package/dist/core/prompt-cache.d.ts +163 -0
  104. package/dist/core/prompt-cache.js +599 -0
  105. package/dist/core/sandbox.d.ts +127 -0
  106. package/dist/core/sandbox.js +369 -0
  107. package/dist/core/scheduled-tasks.d.ts +151 -0
  108. package/dist/core/scheduled-tasks.js +502 -0
  109. package/dist/core/security-scanner.d.ts +160 -0
  110. package/dist/core/security-scanner.js +1494 -0
  111. package/dist/core/session.d.ts +83 -0
  112. package/dist/core/session.js +269 -0
  113. package/dist/core/shell-completion.d.ts +51 -0
  114. package/dist/core/shell-completion.js +674 -0
  115. package/dist/core/smart-monitor.d.ts +146 -0
  116. package/dist/core/smart-monitor.js +1199 -0
  117. package/dist/core/spec-driven.d.ts +233 -0
  118. package/dist/core/spec-driven.js +1485 -0
  119. package/dist/core/spending-warnings.d.ts +123 -0
  120. package/dist/core/spending-warnings.js +456 -0
  121. package/dist/core/sub-agent.d.ts +298 -0
  122. package/dist/core/sub-agent.js +1023 -0
  123. package/dist/core/telemetry.d.ts +157 -0
  124. package/dist/core/telemetry.js +412 -0
  125. package/dist/core/terminal-ux.d.ts +134 -0
  126. package/dist/core/terminal-ux.js +649 -0
  127. package/dist/core/testing.d.ts +146 -0
  128. package/dist/core/testing.js +1200 -0
  129. package/dist/core/types.d.ts +439 -0
  130. package/dist/core/types.js +6 -0
  131. package/dist/core/undo-redo.d.ts +112 -0
  132. package/dist/core/undo-redo.js +290 -0
  133. package/dist/core/updater.d.ts +159 -0
  134. package/dist/core/updater.js +608 -0
  135. package/dist/core/vim-mode.d.ts +151 -0
  136. package/dist/core/vim-mode.js +771 -0
  137. package/dist/core/voice.d.ts +137 -0
  138. package/dist/core/voice.js +538 -0
  139. package/dist/core/web-dashboard.d.ts +109 -0
  140. package/dist/core/web-dashboard.js +484 -0
  141. package/dist/hooks/hooks.d.ts +74 -0
  142. package/dist/hooks/hooks.js +160 -0
  143. package/dist/hooks/llm-evaluator.d.ts +165 -0
  144. package/dist/hooks/llm-evaluator.js +560 -0
  145. package/dist/index.d.ts +3 -0
  146. package/dist/index.js +1186 -0
  147. package/dist/lsp/lsp-manager.d.ts +63 -0
  148. package/dist/lsp/lsp-manager.js +351 -0
  149. package/dist/mcp/client.d.ts +133 -0
  150. package/dist/mcp/client.js +684 -0
  151. package/dist/mcp/mcp-apps.d.ts +70 -0
  152. package/dist/mcp/mcp-apps.js +1007 -0
  153. package/dist/tools/bash.d.ts +5 -0
  154. package/dist/tools/bash.js +195 -0
  155. package/dist/tools/browser.d.ts +92 -0
  156. package/dist/tools/browser.js +1570 -0
  157. package/dist/tools/extended.d.ts +6 -0
  158. package/dist/tools/extended.js +191 -0
  159. package/dist/tools/file.d.ts +10 -0
  160. package/dist/tools/file.js +382 -0
  161. package/dist/tools/github.d.ts +389 -0
  162. package/dist/tools/github.js +759 -0
  163. package/dist/tools/index.d.ts +9 -0
  164. package/dist/tools/index.js +40 -0
  165. package/dist/tools/memory.d.ts +6 -0
  166. package/dist/tools/memory.js +197 -0
  167. package/dist/tools/registry.d.ts +29 -0
  168. package/dist/tools/registry.js +64 -0
  169. package/dist/tools/web.d.ts +6 -0
  170. package/dist/tools/web.js +150 -0
  171. package/dist/ui/renderer.d.ts +97 -0
  172. package/dist/ui/renderer.js +279 -0
  173. package/dist/ui/theme.d.ts +27 -0
  174. package/dist/ui/theme.js +106 -0
  175. 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