@thegitai/cli 1.0.0-beta.1
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 +20 -0
- package/README.md +30 -0
- package/dist/bin/ai.js +438 -0
- package/dist/parsers/tree-sitter-c-sharp.wasm +0 -0
- package/dist/parsers/tree-sitter-c.wasm +0 -0
- package/dist/parsers/tree-sitter-cpp.wasm +0 -0
- package/dist/parsers/tree-sitter-css.wasm +0 -0
- package/dist/parsers/tree-sitter-go.wasm +0 -0
- package/dist/parsers/tree-sitter-html.wasm +0 -0
- package/dist/parsers/tree-sitter-java.wasm +0 -0
- package/dist/parsers/tree-sitter-javascript.wasm +0 -0
- package/dist/parsers/tree-sitter-objc.wasm +0 -0
- package/dist/parsers/tree-sitter-php.wasm +0 -0
- package/dist/parsers/tree-sitter-python.wasm +0 -0
- package/dist/parsers/tree-sitter-ruby.wasm +0 -0
- package/dist/parsers/tree-sitter-rust.wasm +0 -0
- package/dist/parsers/tree-sitter-tsx.wasm +0 -0
- package/dist/parsers/tree-sitter-typescript.wasm +0 -0
- package/dist/src/agent-mode.js +142 -0
- package/dist/src/api/auth.js +81 -0
- package/dist/src/api/browser-login.js +184 -0
- package/dist/src/api/chat.js +346 -0
- package/dist/src/api/contracts.js +1 -0
- package/dist/src/api/http.js +44 -0
- package/dist/src/api/index.js +11 -0
- package/dist/src/api/models.js +110 -0
- package/dist/src/api/sessions.js +72 -0
- package/dist/src/artifact-policy.js +207 -0
- package/dist/src/client-state.js +14 -0
- package/dist/src/core/clipboard.js +208 -0
- package/dist/src/core/open-url.js +32 -0
- package/dist/src/edit-journal.js +133 -0
- package/dist/src/executor.js +924 -0
- package/dist/src/extractors/cpp.js +18 -0
- package/dist/src/extractors/csharp.js +16 -0
- package/dist/src/extractors/css.js +12 -0
- package/dist/src/extractors/go.js +27 -0
- package/dist/src/extractors/index.js +52 -0
- package/dist/src/extractors/java.js +14 -0
- package/dist/src/extractors/javascript.js +33 -0
- package/dist/src/extractors/objc.js +14 -0
- package/dist/src/extractors/php.js +20 -0
- package/dist/src/extractors/python.js +11 -0
- package/dist/src/extractors/ruby.js +13 -0
- package/dist/src/extractors/rust.js +17 -0
- package/dist/src/extractors/utils.js +58 -0
- package/dist/src/help-text.js +125 -0
- package/dist/src/markdown-renderer.js +112 -0
- package/dist/src/patcher.js +279 -0
- package/dist/src/project-index.js +221 -0
- package/dist/src/repo-map-languages.js +100 -0
- package/dist/src/runtime-mode.js +35 -0
- package/dist/src/scanner.js +362 -0
- package/dist/src/secret-preview.js +137 -0
- package/dist/src/session-exit.js +17 -0
- package/dist/src/session-safety.js +1012 -0
- package/dist/src/session-store.js +266 -0
- package/dist/src/session.js +93 -0
- package/dist/src/tool-executor.js +188 -0
- package/dist/src/tools/code-intel.js +472 -0
- package/dist/src/tools/delete-file.js +27 -0
- package/dist/src/tools/exec-utils.js +17 -0
- package/dist/src/tools/find-symbol.js +70 -0
- package/dist/src/tools/get-diagnostics.js +22 -0
- package/dist/src/tools/grep-code.js +331 -0
- package/dist/src/tools/hover-symbol.js +95 -0
- package/dist/src/tools/index.js +73 -0
- package/dist/src/tools/list-checkpoints.js +11 -0
- package/dist/src/tools/list-directories.js +16 -0
- package/dist/src/tools/list-files.js +13 -0
- package/dist/src/tools/list-session-edits.js +9 -0
- package/dist/src/tools/list-symbols.js +55 -0
- package/dist/src/tools/patch-file.js +88 -0
- package/dist/src/tools/path-listing.js +83 -0
- package/dist/src/tools/read-document.js +111 -0
- package/dist/src/tools/read-file.js +109 -0
- package/dist/src/tools/restore-checkpoint.js +100 -0
- package/dist/src/tools/ripgrep.js +29 -0
- package/dist/src/tools/run-command.js +94 -0
- package/dist/src/tools/run-node-script.js +210 -0
- package/dist/src/tools/search-code.js +37 -0
- package/dist/src/tools/shell-diagnostics.js +707 -0
- package/dist/src/tools/signature-help.js +118 -0
- package/dist/src/tools/str-replace.js +193 -0
- package/dist/src/tools/types.js +1 -0
- package/dist/src/tools/undo-edit.js +202 -0
- package/dist/src/tools/write-file.js +59 -0
- package/dist/src/tree-sitter-runtime.js +135 -0
- package/dist/src/types.js +1 -0
- package/dist/src/ui/paste-collapse.js +22 -0
- package/dist/src/ui/prompt-history-store.js +96 -0
- package/dist/src/ui/repl.js +2238 -0
- package/dist/src/ui/tui/bridge.js +175 -0
- package/dist/src/ui/tui/build-frame.js +718 -0
- package/dist/src/ui/tui/markdown-render.js +455 -0
- package/dist/src/ui/tui/shell-input.js +488 -0
- package/dist/src/ui/tui/text.js +30 -0
- package/dist/src/ui/tui/types.js +1 -0
- package/dist/src/usage.js +47 -0
- package/dist/src/utils.js +38 -0
- package/package.json +38 -0
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { builtinModules } from 'node:module';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
const CHECK_TIMEOUT = 60000;
|
|
6
|
+
const MAX_RAW_OUTPUT = 512 * 1024;
|
|
7
|
+
const FILE_DIAGNOSTICS_CACHE_TTL_MS = 5000;
|
|
8
|
+
const PROJECT_DIAGNOSTICS_CACHE_TTL_MS = 30000;
|
|
9
|
+
const EXT_TO_LANGUAGE = {
|
|
10
|
+
'.rs': 'rust',
|
|
11
|
+
'.go': 'go',
|
|
12
|
+
'.py': 'python',
|
|
13
|
+
'.ts': 'typescript',
|
|
14
|
+
'.tsx': 'typescript',
|
|
15
|
+
'.mts': 'typescript',
|
|
16
|
+
'.cts': 'typescript',
|
|
17
|
+
'.js': 'javascript',
|
|
18
|
+
'.jsx': 'javascript',
|
|
19
|
+
'.mjs': 'javascript',
|
|
20
|
+
'.cjs': 'javascript',
|
|
21
|
+
};
|
|
22
|
+
function detectLanguageFromFile(filePath) {
|
|
23
|
+
return EXT_TO_LANGUAGE[path.extname(filePath).toLowerCase()] ?? null;
|
|
24
|
+
}
|
|
25
|
+
function detectLanguageFromProject(rootDir) {
|
|
26
|
+
if (existsSync(path.join(rootDir, 'Cargo.toml')))
|
|
27
|
+
return 'rust';
|
|
28
|
+
if (existsSync(path.join(rootDir, 'go.mod')))
|
|
29
|
+
return 'go';
|
|
30
|
+
if (existsSync(path.join(rootDir, 'tsconfig.json')))
|
|
31
|
+
return 'typescript';
|
|
32
|
+
if (existsSync(path.join(rootDir, 'package.json')))
|
|
33
|
+
return 'javascript';
|
|
34
|
+
if (existsSync(path.join(rootDir, 'setup.py')) ||
|
|
35
|
+
existsSync(path.join(rootDir, 'pyproject.toml')))
|
|
36
|
+
return 'python';
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
function commandExists(cmd) {
|
|
40
|
+
try {
|
|
41
|
+
execFileSync(cmd, ['--version'], {
|
|
42
|
+
timeout: 5000,
|
|
43
|
+
stdio: 'pipe',
|
|
44
|
+
encoding: 'utf-8',
|
|
45
|
+
});
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function runCheck(cmd, args, rootDir) {
|
|
53
|
+
try {
|
|
54
|
+
const stdout = execFileSync(cmd, args, {
|
|
55
|
+
cwd: rootDir,
|
|
56
|
+
encoding: 'utf-8',
|
|
57
|
+
maxBuffer: MAX_RAW_OUTPUT,
|
|
58
|
+
timeout: CHECK_TIMEOUT,
|
|
59
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
60
|
+
});
|
|
61
|
+
return { stdout, stderr: '', exitCode: 0 };
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
return {
|
|
65
|
+
stdout: err.stdout ?? '',
|
|
66
|
+
stderr: err.stderr ?? '',
|
|
67
|
+
exitCode: err.status ?? 1,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function parseRustDiagnostics(stdout, rootDir) {
|
|
72
|
+
const diagnostics = [];
|
|
73
|
+
for (const line of stdout.split('\n')) {
|
|
74
|
+
if (!line.trim())
|
|
75
|
+
continue;
|
|
76
|
+
let msg;
|
|
77
|
+
try {
|
|
78
|
+
msg = JSON.parse(line);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (msg.reason !== 'compiler-message')
|
|
84
|
+
continue;
|
|
85
|
+
const cm = msg.message;
|
|
86
|
+
if (!cm?.spans?.length)
|
|
87
|
+
continue;
|
|
88
|
+
const severity = cm.level === 'error'
|
|
89
|
+
? 'error'
|
|
90
|
+
: cm.level === 'warning'
|
|
91
|
+
? 'warning'
|
|
92
|
+
: cm.level === 'note'
|
|
93
|
+
? 'info'
|
|
94
|
+
: 'hint';
|
|
95
|
+
for (const span of cm.spans) {
|
|
96
|
+
if (!span.is_primary)
|
|
97
|
+
continue;
|
|
98
|
+
let filePath = span.file_name ?? '';
|
|
99
|
+
if (path.isAbsolute(filePath)) {
|
|
100
|
+
filePath = path.relative(rootDir, filePath);
|
|
101
|
+
}
|
|
102
|
+
diagnostics.push({
|
|
103
|
+
filePath,
|
|
104
|
+
severity: severity,
|
|
105
|
+
message: cm.message,
|
|
106
|
+
source: 'rustc',
|
|
107
|
+
code: cm.code?.code ?? undefined,
|
|
108
|
+
startLine: span.line_start ?? 1,
|
|
109
|
+
startColumn: span.column_start ?? 1,
|
|
110
|
+
endLine: span.line_end ?? span.line_start ?? 1,
|
|
111
|
+
endColumn: span.column_end ?? span.column_start ?? 1,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return diagnostics;
|
|
116
|
+
}
|
|
117
|
+
function runRustCheck(rootDir) {
|
|
118
|
+
if (!existsSync(path.join(rootDir, 'Cargo.toml')))
|
|
119
|
+
return null;
|
|
120
|
+
if (!commandExists('cargo'))
|
|
121
|
+
return null;
|
|
122
|
+
const result = runCheck('cargo', ['check', '--message-format=json', '--quiet'], rootDir);
|
|
123
|
+
return parseRustDiagnostics(`${result.stdout}\n${result.stderr}`, rootDir);
|
|
124
|
+
}
|
|
125
|
+
const GO_DIAG_RE = /^(.+?):(\d+):(\d+):\s*(.+)$/;
|
|
126
|
+
function parseGoDiagnostics(output, rootDir) {
|
|
127
|
+
const diagnostics = [];
|
|
128
|
+
for (const line of output.split('\n')) {
|
|
129
|
+
const m = line.match(GO_DIAG_RE);
|
|
130
|
+
if (!m)
|
|
131
|
+
continue;
|
|
132
|
+
let filePath = m[1];
|
|
133
|
+
if (filePath.startsWith('./'))
|
|
134
|
+
filePath = filePath.slice(2);
|
|
135
|
+
if (path.isAbsolute(filePath))
|
|
136
|
+
filePath = path.relative(rootDir, filePath);
|
|
137
|
+
const lineNum = parseInt(m[2], 10);
|
|
138
|
+
const col = parseInt(m[3], 10);
|
|
139
|
+
diagnostics.push({
|
|
140
|
+
filePath,
|
|
141
|
+
severity: 'error',
|
|
142
|
+
message: m[4].trim(),
|
|
143
|
+
source: 'go',
|
|
144
|
+
startLine: lineNum,
|
|
145
|
+
startColumn: col,
|
|
146
|
+
endLine: lineNum,
|
|
147
|
+
endColumn: col,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return diagnostics;
|
|
151
|
+
}
|
|
152
|
+
function runGoCheck(rootDir) {
|
|
153
|
+
if (!existsSync(path.join(rootDir, 'go.mod')))
|
|
154
|
+
return null;
|
|
155
|
+
if (!commandExists('go'))
|
|
156
|
+
return null;
|
|
157
|
+
const result = runCheck('go', ['build', './...'], rootDir);
|
|
158
|
+
return parseGoDiagnostics(`${result.stdout}\n${result.stderr}`, rootDir);
|
|
159
|
+
}
|
|
160
|
+
function parseRuffDiagnostics(stdout, rootDir) {
|
|
161
|
+
let items;
|
|
162
|
+
try {
|
|
163
|
+
items = JSON.parse(stdout);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
if (!Array.isArray(items))
|
|
169
|
+
return [];
|
|
170
|
+
return items.map((item) => {
|
|
171
|
+
let filePath = item.filename ?? '';
|
|
172
|
+
if (path.isAbsolute(filePath))
|
|
173
|
+
filePath = path.relative(rootDir, filePath);
|
|
174
|
+
return {
|
|
175
|
+
filePath,
|
|
176
|
+
severity: 'warning',
|
|
177
|
+
message: item.message ?? '',
|
|
178
|
+
source: 'ruff',
|
|
179
|
+
code: item.code ?? undefined,
|
|
180
|
+
startLine: item.location?.row ?? 1,
|
|
181
|
+
startColumn: item.location?.column ?? 1,
|
|
182
|
+
endLine: item.end_location?.row ?? item.location?.row ?? 1,
|
|
183
|
+
endColumn: item.end_location?.column ?? item.location?.column ?? 1,
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
const PYCOMPILE_RE = /File "(.+?)", line (\d+)/;
|
|
188
|
+
const PYCOMPILE_ERR_RE = /^(\w*Error):\s*(.+)$/m;
|
|
189
|
+
function parsePyCompileDiagnostics(stderr, rootDir, filePath) {
|
|
190
|
+
const locMatch = stderr.match(PYCOMPILE_RE);
|
|
191
|
+
const errMatch = stderr.match(PYCOMPILE_ERR_RE);
|
|
192
|
+
if (!locMatch && !errMatch)
|
|
193
|
+
return [];
|
|
194
|
+
let diagPath = filePath;
|
|
195
|
+
if (locMatch?.[1]) {
|
|
196
|
+
diagPath = path.isAbsolute(locMatch[1])
|
|
197
|
+
? path.relative(rootDir, locMatch[1])
|
|
198
|
+
: locMatch[1];
|
|
199
|
+
}
|
|
200
|
+
const lineNum = locMatch ? parseInt(locMatch[2], 10) : 1;
|
|
201
|
+
const message = errMatch
|
|
202
|
+
? `${errMatch[1]}: ${errMatch[2]}`
|
|
203
|
+
: (stderr.trim().split('\n').pop() ?? 'Syntax error');
|
|
204
|
+
return [
|
|
205
|
+
{
|
|
206
|
+
filePath: diagPath,
|
|
207
|
+
severity: 'error',
|
|
208
|
+
message,
|
|
209
|
+
source: 'python',
|
|
210
|
+
startLine: lineNum,
|
|
211
|
+
startColumn: 1,
|
|
212
|
+
endLine: lineNum,
|
|
213
|
+
endColumn: 1,
|
|
214
|
+
},
|
|
215
|
+
];
|
|
216
|
+
}
|
|
217
|
+
function runPythonCheck(rootDir, filePath) {
|
|
218
|
+
if (commandExists('ruff')) {
|
|
219
|
+
const target = filePath || '.';
|
|
220
|
+
const result = runCheck('ruff', ['check', '--output-format=json', '--', target], rootDir);
|
|
221
|
+
return parseRuffDiagnostics(result.stdout, rootDir);
|
|
222
|
+
}
|
|
223
|
+
if (filePath) {
|
|
224
|
+
const result = runCheck('python', ['-m', 'py_compile', filePath], rootDir);
|
|
225
|
+
if (result.exitCode !== 0) {
|
|
226
|
+
return parsePyCompileDiagnostics(result.stderr, rootDir, filePath);
|
|
227
|
+
}
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
const TSC_DIAG_RE = /^(.+?)\((\d+),(\d+)\):\s*(\w+)\s+(TS\d+):\s*(.+)$/;
|
|
233
|
+
function parseTscDiagnostics(output, rootDir) {
|
|
234
|
+
const diagnostics = [];
|
|
235
|
+
for (const line of output.split('\n')) {
|
|
236
|
+
const m = line.match(TSC_DIAG_RE);
|
|
237
|
+
if (!m)
|
|
238
|
+
continue;
|
|
239
|
+
let filePath = m[1];
|
|
240
|
+
if (path.isAbsolute(filePath))
|
|
241
|
+
filePath = path.relative(rootDir, filePath);
|
|
242
|
+
diagnostics.push({
|
|
243
|
+
filePath,
|
|
244
|
+
severity: m[4] === 'error' ? 'error' : 'warning',
|
|
245
|
+
message: m[6].trim(),
|
|
246
|
+
source: 'tsc',
|
|
247
|
+
code: m[5],
|
|
248
|
+
startLine: parseInt(m[2], 10),
|
|
249
|
+
startColumn: parseInt(m[3], 10),
|
|
250
|
+
endLine: parseInt(m[2], 10),
|
|
251
|
+
endColumn: parseInt(m[3], 10),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
return diagnostics;
|
|
255
|
+
}
|
|
256
|
+
function runTypeScriptCheck(rootDir) {
|
|
257
|
+
if (!existsSync(path.join(rootDir, 'tsconfig.json')))
|
|
258
|
+
return null;
|
|
259
|
+
const result = runCheck('npx', ['tsc', '--noEmit', '--pretty', 'false'], rootDir);
|
|
260
|
+
return parseTscDiagnostics(`${result.stdout}\n${result.stderr}`, rootDir);
|
|
261
|
+
}
|
|
262
|
+
const JS_IMPORT_RE = /import\s+(?:type\s+)?([^'"]+?)\s+from\s+['"]([^'"]+)['"]/g;
|
|
263
|
+
const JS_SIDE_EFFECT_IMPORT_RE = /import\s+['"]([^'"]+)['"]/g;
|
|
264
|
+
function resolveJavaScriptImport(rootDir, fromFile, specifier) {
|
|
265
|
+
if (!specifier.startsWith('.') && !specifier.startsWith('/'))
|
|
266
|
+
return null;
|
|
267
|
+
const fromDir = path.dirname(path.join(rootDir, fromFile));
|
|
268
|
+
const base = specifier.startsWith('/')
|
|
269
|
+
? path.join(rootDir, specifier)
|
|
270
|
+
: path.resolve(fromDir, specifier);
|
|
271
|
+
const candidates = [
|
|
272
|
+
base,
|
|
273
|
+
`${base}.js`,
|
|
274
|
+
`${base}.mjs`,
|
|
275
|
+
`${base}.cjs`,
|
|
276
|
+
path.join(base, 'index.js'),
|
|
277
|
+
];
|
|
278
|
+
for (const candidate of candidates) {
|
|
279
|
+
if (existsSync(candidate))
|
|
280
|
+
return path.relative(rootDir, candidate);
|
|
281
|
+
}
|
|
282
|
+
return path.relative(rootDir, base);
|
|
283
|
+
}
|
|
284
|
+
function exportedNames(content) {
|
|
285
|
+
const names = new Set();
|
|
286
|
+
for (const match of content.matchAll(/\bexport\s+(?:async\s+)?(?:function|class|const|let|var)\s+([A-Za-z_$][\w$]*)/g)) {
|
|
287
|
+
names.add(match[1]);
|
|
288
|
+
}
|
|
289
|
+
for (const match of content.matchAll(/\bexport\s*\{([^}]+)\}/g)) {
|
|
290
|
+
const list = match[1] ?? '';
|
|
291
|
+
for (const raw of list.split(',')) {
|
|
292
|
+
const name = raw.trim().split(/\s+as\s+/i).pop()?.trim();
|
|
293
|
+
if (name)
|
|
294
|
+
names.add(name);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (/\bexport\s+default\b/.test(content))
|
|
298
|
+
names.add('default');
|
|
299
|
+
return names;
|
|
300
|
+
}
|
|
301
|
+
function packageDeps(rootDir) {
|
|
302
|
+
try {
|
|
303
|
+
const pkg = JSON.parse(readFileSync(path.join(rootDir, 'package.json'), 'utf-8'));
|
|
304
|
+
return new Set([
|
|
305
|
+
...Object.keys(pkg.dependencies ?? {}),
|
|
306
|
+
...Object.keys(pkg.devDependencies ?? {}),
|
|
307
|
+
...Object.keys(pkg.peerDependencies ?? {}),
|
|
308
|
+
...Object.keys(pkg.optionalDependencies ?? {}),
|
|
309
|
+
]);
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return new Set();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function packageName(specifier) {
|
|
316
|
+
if (specifier.startsWith('@')) {
|
|
317
|
+
const parts = specifier.split('/');
|
|
318
|
+
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : specifier;
|
|
319
|
+
}
|
|
320
|
+
return specifier.split('/')[0] ?? specifier;
|
|
321
|
+
}
|
|
322
|
+
function runJavaScriptStaticCheck(rootDir, filePath) {
|
|
323
|
+
const absPath = path.join(rootDir, filePath);
|
|
324
|
+
let content = '';
|
|
325
|
+
try {
|
|
326
|
+
content = readFileSync(absPath, 'utf-8');
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
return [];
|
|
330
|
+
}
|
|
331
|
+
const deps = packageDeps(rootDir);
|
|
332
|
+
const diagnostics = [];
|
|
333
|
+
const builtin = new Set([
|
|
334
|
+
...builtinModules,
|
|
335
|
+
...builtinModules.map((name) => `node:${name}`),
|
|
336
|
+
]);
|
|
337
|
+
const imports = [];
|
|
338
|
+
for (const match of content.matchAll(JS_IMPORT_RE)) {
|
|
339
|
+
const importClause = (match[1] ?? '').trim();
|
|
340
|
+
const specifier = match[2] ?? '';
|
|
341
|
+
const namedImports = importClause.match(/\{([^}]+)\}/)?.[1] ?? '';
|
|
342
|
+
const names = namedImports
|
|
343
|
+
? namedImports
|
|
344
|
+
.split(',')
|
|
345
|
+
.map((raw) => raw.trim().split(/\s+as\s+/i)[0]?.trim())
|
|
346
|
+
.filter(Boolean)
|
|
347
|
+
: [];
|
|
348
|
+
const defaultImport = importClause.replace(/\{[^}]+\}/, '').split(',')[0]?.trim();
|
|
349
|
+
if (defaultImport &&
|
|
350
|
+
defaultImport !== '*' &&
|
|
351
|
+
!defaultImport.startsWith('* ')) {
|
|
352
|
+
names.unshift('default');
|
|
353
|
+
}
|
|
354
|
+
const line = content.slice(0, match.index ?? 0).split('\n').length;
|
|
355
|
+
imports.push({ specifier, names, line });
|
|
356
|
+
}
|
|
357
|
+
for (const match of content.matchAll(JS_SIDE_EFFECT_IMPORT_RE)) {
|
|
358
|
+
const specifier = match[1] ?? '';
|
|
359
|
+
const line = content.slice(0, match.index ?? 0).split('\n').length;
|
|
360
|
+
imports.push({ specifier, names: [], line });
|
|
361
|
+
}
|
|
362
|
+
for (const item of imports) {
|
|
363
|
+
if (!item.specifier.startsWith('.') && !item.specifier.startsWith('/')) {
|
|
364
|
+
const pkg = packageName(item.specifier);
|
|
365
|
+
if (!builtin.has(pkg) && !builtin.has(item.specifier) && !deps.has(pkg)) {
|
|
366
|
+
diagnostics.push({
|
|
367
|
+
filePath,
|
|
368
|
+
severity: 'error',
|
|
369
|
+
message: `Package "${pkg}" is imported but not listed in package.json dependencies.`,
|
|
370
|
+
source: 'node-import',
|
|
371
|
+
startLine: item.line,
|
|
372
|
+
startColumn: 1,
|
|
373
|
+
endLine: item.line,
|
|
374
|
+
endColumn: 1,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
const resolved = resolveJavaScriptImport(rootDir, filePath, item.specifier);
|
|
380
|
+
if (!resolved || !existsSync(path.join(rootDir, resolved))) {
|
|
381
|
+
diagnostics.push({
|
|
382
|
+
filePath,
|
|
383
|
+
severity: 'error',
|
|
384
|
+
message: `Imported module "${item.specifier}" does not resolve.`,
|
|
385
|
+
source: 'node-import',
|
|
386
|
+
startLine: item.line,
|
|
387
|
+
startColumn: 1,
|
|
388
|
+
endLine: item.line,
|
|
389
|
+
endColumn: 1,
|
|
390
|
+
});
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
let importedContent = '';
|
|
394
|
+
try {
|
|
395
|
+
importedContent = readFileSync(path.join(rootDir, resolved), 'utf-8');
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
const exports = exportedNames(importedContent);
|
|
401
|
+
for (const name of item.names) {
|
|
402
|
+
if (exports.has(name))
|
|
403
|
+
continue;
|
|
404
|
+
diagnostics.push({
|
|
405
|
+
filePath,
|
|
406
|
+
severity: 'error',
|
|
407
|
+
message: `Module "${item.specifier}" does not export "${name}".`,
|
|
408
|
+
source: 'node-import',
|
|
409
|
+
startLine: item.line,
|
|
410
|
+
startColumn: 1,
|
|
411
|
+
endLine: item.line,
|
|
412
|
+
endColumn: 1,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return diagnostics;
|
|
417
|
+
}
|
|
418
|
+
function runJavaScriptCheck(rootDir, filePath) {
|
|
419
|
+
if (existsSync(path.join(rootDir, 'tsconfig.json'))) {
|
|
420
|
+
return runTypeScriptCheck(rootDir);
|
|
421
|
+
}
|
|
422
|
+
if (filePath) {
|
|
423
|
+
const result = runCheck('node', ['--check', filePath], rootDir);
|
|
424
|
+
const staticDiagnostics = runJavaScriptStaticCheck(rootDir, filePath);
|
|
425
|
+
if (result.exitCode !== 0) {
|
|
426
|
+
const msg = (result.stderr || result.stdout).trim();
|
|
427
|
+
const lineMatch = msg.match(/:(\d+)\b/);
|
|
428
|
+
const lineNum = lineMatch ? parseInt(lineMatch[1], 10) : 1;
|
|
429
|
+
return [
|
|
430
|
+
{
|
|
431
|
+
filePath,
|
|
432
|
+
severity: 'error',
|
|
433
|
+
message: msg.split('\n').pop() ?? 'Syntax error',
|
|
434
|
+
source: 'node',
|
|
435
|
+
startLine: lineNum,
|
|
436
|
+
startColumn: 1,
|
|
437
|
+
endLine: lineNum,
|
|
438
|
+
endColumn: 1,
|
|
439
|
+
},
|
|
440
|
+
...staticDiagnostics,
|
|
441
|
+
];
|
|
442
|
+
}
|
|
443
|
+
return staticDiagnostics;
|
|
444
|
+
}
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
function countBySeverity(diagnostics) {
|
|
448
|
+
let errors = 0;
|
|
449
|
+
let warnings = 0;
|
|
450
|
+
for (const d of diagnostics) {
|
|
451
|
+
if (d.severity === 'error')
|
|
452
|
+
errors++;
|
|
453
|
+
else if (d.severity === 'warning')
|
|
454
|
+
warnings++;
|
|
455
|
+
}
|
|
456
|
+
return { errors, warnings };
|
|
457
|
+
}
|
|
458
|
+
function formatShellDiagnosticsOutput(diagnostics, otherFileErrorCount = 0) {
|
|
459
|
+
let output;
|
|
460
|
+
if (!diagnostics.length) {
|
|
461
|
+
output = 'Diagnostics: clean (via build check)';
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
const lines = diagnostics.map((d) => {
|
|
465
|
+
const source = d.source ? ` ${d.source}` : '';
|
|
466
|
+
const code = d.code != null ? ` (${d.code})` : '';
|
|
467
|
+
return `- ${d.severity.toUpperCase()} ${d.filePath}:${d.startLine}:${d.startColumn}${source}${code} ${d.message}`;
|
|
468
|
+
});
|
|
469
|
+
output = `Diagnostics via build check (${diagnostics.length})\n${lines.join('\n')}`;
|
|
470
|
+
}
|
|
471
|
+
if (otherFileErrorCount > 0) {
|
|
472
|
+
output += `\n\n${otherFileErrorCount} additional error(s) in other project files — call get_diagnostics without a filePath to see the full project picture before declaring success.`;
|
|
473
|
+
}
|
|
474
|
+
return output;
|
|
475
|
+
}
|
|
476
|
+
const shellDiagnosticsCache = new Map();
|
|
477
|
+
const FILE_EXT_AFFECTS_LANGUAGES = new Map([
|
|
478
|
+
['.ts', ['typescript', 'javascript']],
|
|
479
|
+
['.tsx', ['typescript', 'javascript']],
|
|
480
|
+
['.mts', ['typescript', 'javascript']],
|
|
481
|
+
['.cts', ['typescript', 'javascript']],
|
|
482
|
+
['.js', ['typescript', 'javascript']],
|
|
483
|
+
['.jsx', ['typescript', 'javascript']],
|
|
484
|
+
['.mjs', ['typescript', 'javascript']],
|
|
485
|
+
['.cjs', ['typescript', 'javascript']],
|
|
486
|
+
['.rs', ['rust']],
|
|
487
|
+
['.go', ['go']],
|
|
488
|
+
['.py', ['python']],
|
|
489
|
+
]);
|
|
490
|
+
const BUILD_MANIFEST_AFFECTS_LANGUAGES = new Map([
|
|
491
|
+
['tsconfig.json', ['typescript', 'javascript']],
|
|
492
|
+
['jsconfig.json', ['typescript', 'javascript']],
|
|
493
|
+
['package.json', ['typescript', 'javascript']],
|
|
494
|
+
['package-lock.json', ['typescript', 'javascript']],
|
|
495
|
+
['yarn.lock', ['typescript', 'javascript']],
|
|
496
|
+
['pnpm-lock.yaml', ['typescript', 'javascript']],
|
|
497
|
+
['Cargo.toml', ['rust']],
|
|
498
|
+
['Cargo.lock', ['rust']],
|
|
499
|
+
['go.mod', ['go']],
|
|
500
|
+
['go.sum', ['go']],
|
|
501
|
+
['pyproject.toml', ['python']],
|
|
502
|
+
['setup.py', ['python']],
|
|
503
|
+
['setup.cfg', ['python']],
|
|
504
|
+
['requirements.txt', ['python']],
|
|
505
|
+
['Pipfile', ['python']],
|
|
506
|
+
['Pipfile.lock', ['python']],
|
|
507
|
+
]);
|
|
508
|
+
function projectCacheKey(rootDir, language) {
|
|
509
|
+
return `${rootDir}::project::${language}`;
|
|
510
|
+
}
|
|
511
|
+
function fileCacheKey(rootDir, filePath) {
|
|
512
|
+
return `${rootDir}::file::${filePath}`;
|
|
513
|
+
}
|
|
514
|
+
function cloneDiagnostics(diagnostics) {
|
|
515
|
+
return diagnostics.map((diagnostic) => ({ ...diagnostic }));
|
|
516
|
+
}
|
|
517
|
+
export function invalidateShellDiagnosticsCache(rootDir, filePath) {
|
|
518
|
+
const prefix = `${rootDir}::`;
|
|
519
|
+
if (!filePath) {
|
|
520
|
+
for (const key of shellDiagnosticsCache.keys()) {
|
|
521
|
+
if (key.startsWith(prefix)) {
|
|
522
|
+
shellDiagnosticsCache.delete(key);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const basename = path.basename(filePath);
|
|
528
|
+
const manifestAffected = BUILD_MANIFEST_AFFECTS_LANGUAGES.get(basename);
|
|
529
|
+
if (manifestAffected) {
|
|
530
|
+
for (const language of manifestAffected) {
|
|
531
|
+
shellDiagnosticsCache.delete(projectCacheKey(rootDir, language));
|
|
532
|
+
}
|
|
533
|
+
shellDiagnosticsCache.delete(fileCacheKey(rootDir, filePath));
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
537
|
+
const affected = FILE_EXT_AFFECTS_LANGUAGES.get(ext);
|
|
538
|
+
if (!affected)
|
|
539
|
+
return;
|
|
540
|
+
for (const language of affected) {
|
|
541
|
+
shellDiagnosticsCache.delete(projectCacheKey(rootDir, language));
|
|
542
|
+
}
|
|
543
|
+
shellDiagnosticsCache.delete(fileCacheKey(rootDir, filePath));
|
|
544
|
+
}
|
|
545
|
+
export function inspectShellDiagnosticsCache() {
|
|
546
|
+
return { keys: [...shellDiagnosticsCache.keys()] };
|
|
547
|
+
}
|
|
548
|
+
function commandScopeFor(language, rootDir, filePath) {
|
|
549
|
+
switch (language) {
|
|
550
|
+
case 'rust':
|
|
551
|
+
case 'go':
|
|
552
|
+
case 'typescript':
|
|
553
|
+
return 'project';
|
|
554
|
+
case 'javascript':
|
|
555
|
+
return existsSync(path.join(rootDir, 'tsconfig.json')) ? 'project' : 'file';
|
|
556
|
+
case 'python':
|
|
557
|
+
return filePath ? 'file' : 'project';
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
export function buildDeferredShellDiagnostics(trigger, filePath) {
|
|
561
|
+
if (trigger === 'run_command' || trigger === 'run_node_script') {
|
|
562
|
+
return {
|
|
563
|
+
provider: 'shell',
|
|
564
|
+
deferred: true,
|
|
565
|
+
trigger,
|
|
566
|
+
filePath: filePath ?? undefined,
|
|
567
|
+
output: `${trigger} did not rerun project diagnostics automatically. ` +
|
|
568
|
+
`Use the command output for direct verification when applicable, and call get_diagnostics${filePath ? ` for ${filePath}` : ''} only if the command changed build-relevant files or you need project-wide compiler feedback.`,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
return {
|
|
572
|
+
provider: 'shell',
|
|
573
|
+
deferred: true,
|
|
574
|
+
trigger,
|
|
575
|
+
filePath: filePath ?? undefined,
|
|
576
|
+
output: `${trigger} updated files without rerunning project diagnostics automatically. ` +
|
|
577
|
+
`Batch related edits first, then call get_diagnostics (without a filePath for project-wide errors${filePath ? `, or with filePath="${filePath}" for this file only` : ''}) before declaring success.`,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
function buildUnavailableResult(filePath, command, reason) {
|
|
581
|
+
return {
|
|
582
|
+
ok: true,
|
|
583
|
+
provider: 'shell',
|
|
584
|
+
available: false,
|
|
585
|
+
filePath: filePath ?? undefined,
|
|
586
|
+
diagnostics: [],
|
|
587
|
+
diagnosticCount: 0,
|
|
588
|
+
errorCount: 0,
|
|
589
|
+
warningCount: 0,
|
|
590
|
+
command,
|
|
591
|
+
output: reason,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
function applyFilePathFilter(diagnostics, filePath) {
|
|
595
|
+
if (!filePath) {
|
|
596
|
+
return { focused: diagnostics, otherFileErrorCount: 0 };
|
|
597
|
+
}
|
|
598
|
+
const normalizedTarget = filePath.replace(/^\.\//, '');
|
|
599
|
+
const focused = [];
|
|
600
|
+
let otherFileErrorCount = 0;
|
|
601
|
+
for (const diagnostic of diagnostics) {
|
|
602
|
+
const normalizedDiag = diagnostic.filePath.replace(/^\.\//, '');
|
|
603
|
+
if (normalizedDiag === normalizedTarget ||
|
|
604
|
+
normalizedDiag.endsWith(`/${normalizedTarget}`)) {
|
|
605
|
+
focused.push(diagnostic);
|
|
606
|
+
}
|
|
607
|
+
else if (diagnostic.severity === 'error') {
|
|
608
|
+
otherFileErrorCount += 1;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return { focused, otherFileErrorCount };
|
|
612
|
+
}
|
|
613
|
+
function runDiagnosticsCommand(language, rootDir, filePath) {
|
|
614
|
+
switch (language) {
|
|
615
|
+
case 'rust':
|
|
616
|
+
return {
|
|
617
|
+
command: 'cargo check --message-format=json --quiet',
|
|
618
|
+
diagnostics: runRustCheck(rootDir),
|
|
619
|
+
};
|
|
620
|
+
case 'go':
|
|
621
|
+
return {
|
|
622
|
+
command: 'go build ./...',
|
|
623
|
+
diagnostics: runGoCheck(rootDir),
|
|
624
|
+
};
|
|
625
|
+
case 'python':
|
|
626
|
+
return {
|
|
627
|
+
command: commandExists('ruff')
|
|
628
|
+
? `ruff check ${filePath || '.'}`
|
|
629
|
+
: `python -m py_compile ${filePath || ''}`,
|
|
630
|
+
diagnostics: runPythonCheck(rootDir, filePath),
|
|
631
|
+
};
|
|
632
|
+
case 'typescript':
|
|
633
|
+
return {
|
|
634
|
+
command: 'npx tsc --noEmit',
|
|
635
|
+
diagnostics: runTypeScriptCheck(rootDir),
|
|
636
|
+
};
|
|
637
|
+
case 'javascript':
|
|
638
|
+
return {
|
|
639
|
+
command: existsSync(path.join(rootDir, 'tsconfig.json'))
|
|
640
|
+
? 'npx tsc --noEmit'
|
|
641
|
+
: `node --check ${filePath || ''}`,
|
|
642
|
+
diagnostics: runJavaScriptCheck(rootDir, filePath),
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
export function runShellDiagnostics(rootDir, filePath, limit = 50) {
|
|
647
|
+
const language = filePath
|
|
648
|
+
? detectLanguageFromFile(filePath)
|
|
649
|
+
: detectLanguageFromProject(rootDir);
|
|
650
|
+
if (!language) {
|
|
651
|
+
return buildUnavailableResult(filePath, '', `No build check command available${filePath ? ` for ${filePath}` : ''}.`);
|
|
652
|
+
}
|
|
653
|
+
const scope = commandScopeFor(language, rootDir, filePath);
|
|
654
|
+
const cacheKey = scope === 'project'
|
|
655
|
+
? projectCacheKey(rootDir, language)
|
|
656
|
+
: fileCacheKey(rootDir, filePath);
|
|
657
|
+
const now = Date.now();
|
|
658
|
+
const cached = shellDiagnosticsCache.get(cacheKey);
|
|
659
|
+
let rawDiagnostics;
|
|
660
|
+
let command;
|
|
661
|
+
let available;
|
|
662
|
+
if (cached && cached.expiresAt > now && cached.scope === scope) {
|
|
663
|
+
rawDiagnostics = cached.diagnostics;
|
|
664
|
+
command = cached.command;
|
|
665
|
+
available = cached.available;
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
const outcome = runDiagnosticsCommand(language, rootDir, filePath);
|
|
669
|
+
command = outcome.command;
|
|
670
|
+
if (outcome.diagnostics === null) {
|
|
671
|
+
available = false;
|
|
672
|
+
rawDiagnostics = [];
|
|
673
|
+
}
|
|
674
|
+
else {
|
|
675
|
+
available = true;
|
|
676
|
+
rawDiagnostics = outcome.diagnostics;
|
|
677
|
+
}
|
|
678
|
+
shellDiagnosticsCache.set(cacheKey, {
|
|
679
|
+
expiresAt: now +
|
|
680
|
+
(scope === 'project'
|
|
681
|
+
? PROJECT_DIAGNOSTICS_CACHE_TTL_MS
|
|
682
|
+
: FILE_DIAGNOSTICS_CACHE_TTL_MS),
|
|
683
|
+
scope,
|
|
684
|
+
command,
|
|
685
|
+
available,
|
|
686
|
+
diagnostics: rawDiagnostics,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
if (!available) {
|
|
690
|
+
return buildUnavailableResult(filePath, command, `No build check command available${filePath ? ` for ${filePath}` : ''} (no project configuration found).`);
|
|
691
|
+
}
|
|
692
|
+
const { focused, otherFileErrorCount } = applyFilePathFilter(rawDiagnostics, filePath);
|
|
693
|
+
const limited = focused.slice(0, limit);
|
|
694
|
+
const counts = countBySeverity(limited);
|
|
695
|
+
return {
|
|
696
|
+
ok: true,
|
|
697
|
+
provider: 'shell',
|
|
698
|
+
available: true,
|
|
699
|
+
filePath: filePath || undefined,
|
|
700
|
+
diagnostics: cloneDiagnostics(limited),
|
|
701
|
+
diagnosticCount: focused.length,
|
|
702
|
+
errorCount: counts.errors,
|
|
703
|
+
warningCount: counts.warnings,
|
|
704
|
+
command,
|
|
705
|
+
output: formatShellDiagnosticsOutput(limited, otherFileErrorCount),
|
|
706
|
+
};
|
|
707
|
+
}
|