@wispbit/local 1.0.25
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/dist/build.d.ts +3 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/cli.js +3668 -0
- package/dist/cli.js.map +7 -0
- package/dist/index.js +3868 -0
- package/dist/index.js.map +7 -0
- package/dist/package.json +72 -0
- package/dist/src/cli.d.ts +16 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/config.d.ts +6 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/environment/Config.d.ts +69 -0
- package/dist/src/environment/Config.d.ts.map +1 -0
- package/dist/src/environment/Environment.d.ts +23 -0
- package/dist/src/environment/Environment.d.ts.map +1 -0
- package/dist/src/environment/Sandbox.d.ts +48 -0
- package/dist/src/environment/Sandbox.d.ts.map +1 -0
- package/dist/src/environment/Storage.d.ts +84 -0
- package/dist/src/environment/Storage.d.ts.map +1 -0
- package/dist/src/index.d.ts +16 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/languages.d.ts +36 -0
- package/dist/src/languages.d.ts.map +1 -0
- package/dist/src/providers/AstGrepAstProvider.d.ts +44 -0
- package/dist/src/providers/AstGrepAstProvider.d.ts.map +1 -0
- package/dist/src/providers/LanguageBackend.d.ts +74 -0
- package/dist/src/providers/LanguageBackend.d.ts.map +1 -0
- package/dist/src/providers/RuleProvider.d.ts +46 -0
- package/dist/src/providers/RuleProvider.d.ts.map +1 -0
- package/dist/src/providers/ScipIntelligenceProvider.d.ts +84 -0
- package/dist/src/providers/ScipIntelligenceProvider.d.ts.map +1 -0
- package/dist/src/providers/ViolationValidationProvider.d.ts +42 -0
- package/dist/src/providers/ViolationValidationProvider.d.ts.map +1 -0
- package/dist/src/providers/WispbitRuleProvider.d.ts +45 -0
- package/dist/src/providers/WispbitRuleProvider.d.ts.map +1 -0
- package/dist/src/providers/WispbitViolationValidationProvider.d.ts +15 -0
- package/dist/src/providers/WispbitViolationValidationProvider.d.ts.map +1 -0
- package/dist/src/schemas.d.ts +1771 -0
- package/dist/src/schemas.d.ts.map +1 -0
- package/dist/src/steps/ExecutionEventEmitter.d.ts +156 -0
- package/dist/src/steps/ExecutionEventEmitter.d.ts.map +1 -0
- package/dist/src/steps/FileExecutionContext.d.ts +85 -0
- package/dist/src/steps/FileExecutionContext.d.ts.map +1 -0
- package/dist/src/steps/FileFilterStep.d.ts +35 -0
- package/dist/src/steps/FileFilterStep.d.ts.map +1 -0
- package/dist/src/steps/FileFilterStep.test.d.ts +2 -0
- package/dist/src/steps/FileFilterStep.test.d.ts.map +1 -0
- package/dist/src/steps/FindMatchesStep.d.ts +41 -0
- package/dist/src/steps/FindMatchesStep.d.ts.map +1 -0
- package/dist/src/steps/FindMatchesStep.test.d.ts +2 -0
- package/dist/src/steps/FindMatchesStep.test.d.ts.map +1 -0
- package/dist/src/steps/GotoDefinitionStep.d.ts +86 -0
- package/dist/src/steps/GotoDefinitionStep.d.ts.map +1 -0
- package/dist/src/steps/LLMStep.d.ts +50 -0
- package/dist/src/steps/LLMStep.d.ts.map +1 -0
- package/dist/src/steps/RuleExecutor.d.ts +35 -0
- package/dist/src/steps/RuleExecutor.d.ts.map +1 -0
- package/dist/src/steps/RuleExecutor.test.d.ts +2 -0
- package/dist/src/steps/RuleExecutor.test.d.ts.map +1 -0
- package/dist/src/test/TestExecutor.d.ts +33 -0
- package/dist/src/test/TestExecutor.d.ts.map +1 -0
- package/dist/src/test/rules.test.d.ts +2 -0
- package/dist/src/test/rules.test.d.ts.map +1 -0
- package/dist/src/types.d.ts +200 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/utils/asciiFrames.d.ts +5 -0
- package/dist/src/utils/asciiFrames.d.ts.map +1 -0
- package/dist/src/utils/formatters.d.ts +55 -0
- package/dist/src/utils/formatters.d.ts.map +1 -0
- package/dist/src/utils/generateTreeDump.d.ts +19 -0
- package/dist/src/utils/generateTreeDump.d.ts.map +1 -0
- package/dist/src/utils/git.d.ts +39 -0
- package/dist/src/utils/git.d.ts.map +1 -0
- package/dist/src/utils/hashString.d.ts +2 -0
- package/dist/src/utils/hashString.d.ts.map +1 -0
- package/dist/src/utils/readTextAtRange.d.ts +10 -0
- package/dist/src/utils/readTextAtRange.d.ts.map +1 -0
- package/dist/src/utils/snapshotComparison.d.ts +16 -0
- package/dist/src/utils/snapshotComparison.d.ts.map +1 -0
- package/dist/src/utils/startupScreen.d.ts +5 -0
- package/dist/src/utils/startupScreen.d.ts.map +1 -0
- package/dist/src/utils/validateRule.d.ts +16 -0
- package/dist/src/utils/validateRule.d.ts.map +1 -0
- package/dist/src/version.d.ts +3 -0
- package/dist/src/version.d.ts.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/vitest.config.d.mts +3 -0
- package/dist/vitest.config.d.mts.map +1 -0
- package/package.json +90 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,3668 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import * as fs5 from "fs/promises";
|
|
5
|
+
import * as os3 from "os";
|
|
6
|
+
import * as path10 from "path";
|
|
7
|
+
import Big from "big.js";
|
|
8
|
+
import chalk4 from "chalk";
|
|
9
|
+
import dotenv from "dotenv";
|
|
10
|
+
import meow from "meow";
|
|
11
|
+
import semver from "semver";
|
|
12
|
+
|
|
13
|
+
// src/version.ts
|
|
14
|
+
import { readFileSync } from "fs";
|
|
15
|
+
import { dirname, join } from "path";
|
|
16
|
+
import { fileURLToPath } from "url";
|
|
17
|
+
import latestVersion from "latest-version";
|
|
18
|
+
function getCurrentVersion() {
|
|
19
|
+
const filename = fileURLToPath(import.meta.url ? import.meta.url : __filename);
|
|
20
|
+
const __dirname = dirname(filename);
|
|
21
|
+
const packageJsonPath = join(__dirname, "../package.json");
|
|
22
|
+
try {
|
|
23
|
+
const file = readFileSync(packageJsonPath, "utf8");
|
|
24
|
+
const packageJson = JSON.parse(file);
|
|
25
|
+
return packageJson.version;
|
|
26
|
+
} catch {
|
|
27
|
+
return "0.0.0";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function getLatestVersion() {
|
|
31
|
+
try {
|
|
32
|
+
const latestCliVersion = await latestVersion("@wispbit/local");
|
|
33
|
+
return latestCliVersion;
|
|
34
|
+
} catch {
|
|
35
|
+
return getCurrentVersion();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/environment/Config.ts
|
|
40
|
+
var Config = class _Config {
|
|
41
|
+
config;
|
|
42
|
+
apiKey = null;
|
|
43
|
+
baseUrl = null;
|
|
44
|
+
constructor(config) {
|
|
45
|
+
this.config = {
|
|
46
|
+
...config,
|
|
47
|
+
ignoredGlobs: config.ignoredGlobs || []
|
|
48
|
+
};
|
|
49
|
+
this.apiKey = config.apiKey || null;
|
|
50
|
+
this.baseUrl = config.baseUrl || null;
|
|
51
|
+
}
|
|
52
|
+
getIgnoredGlobs() {
|
|
53
|
+
return this.config.ignoredGlobs || [];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Get the Wispbit API key
|
|
57
|
+
* @returns The API key or null if not set
|
|
58
|
+
*/
|
|
59
|
+
getApiKey() {
|
|
60
|
+
return this.apiKey;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get the Wispbit API base URL
|
|
64
|
+
* @returns The base URL
|
|
65
|
+
*/
|
|
66
|
+
getBaseUrl() {
|
|
67
|
+
return this.baseUrl;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Get the local PowerLint version
|
|
71
|
+
* @returns The current PowerLint version
|
|
72
|
+
*/
|
|
73
|
+
getLocalVersion() {
|
|
74
|
+
return getCurrentVersion();
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Get the schema version
|
|
78
|
+
* @returns The schema version
|
|
79
|
+
*/
|
|
80
|
+
getSchemaVersion() {
|
|
81
|
+
return "v1";
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Validate API key with Wispbit API
|
|
85
|
+
*/
|
|
86
|
+
static async validateApiKey(apiKey, repositoryUrl, baseUrl) {
|
|
87
|
+
const response = await fetch(`${baseUrl}/plv1/initialize`, {
|
|
88
|
+
method: "POST",
|
|
89
|
+
headers: {
|
|
90
|
+
"Content-Type": "application/json",
|
|
91
|
+
Authorization: `Bearer ${apiKey}`
|
|
92
|
+
},
|
|
93
|
+
body: JSON.stringify({
|
|
94
|
+
repository_url: repositoryUrl,
|
|
95
|
+
powerlint_version: getCurrentVersion(),
|
|
96
|
+
schema_version: "v1"
|
|
97
|
+
})
|
|
98
|
+
});
|
|
99
|
+
return response.ok;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Initialize configuration without network validation (for testing)
|
|
103
|
+
* @param options Optional configuration options
|
|
104
|
+
* @returns Config instance
|
|
105
|
+
*/
|
|
106
|
+
static initializeWithoutNetwork(options = {}) {
|
|
107
|
+
const finalBaseUrl = options.baseUrl || process.env.WISPBIT_API_BASE_URL || "https://api.wispbit.com";
|
|
108
|
+
const finalApiKey = options.apiKey || process.env.WISPBIT_API_KEY || null;
|
|
109
|
+
const ignoredGlobs = options.ignoredGlobs || [];
|
|
110
|
+
return new _Config({
|
|
111
|
+
ignoredGlobs,
|
|
112
|
+
apiKey: finalApiKey || void 0,
|
|
113
|
+
baseUrl: finalBaseUrl
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Initialize configuration by validating API key and repository URL with Wispbit
|
|
118
|
+
* @param environment Environment instance to get repository URL
|
|
119
|
+
* @param apiKey Optional API key to use for initialization. If not provided, will use environment variable
|
|
120
|
+
* @returns Promise<Config | null> - Config if valid, null if API key missing/invalid
|
|
121
|
+
*/
|
|
122
|
+
static async initialize(environment, {
|
|
123
|
+
apiKey,
|
|
124
|
+
baseUrl
|
|
125
|
+
}) {
|
|
126
|
+
var _a;
|
|
127
|
+
const finalBaseUrl = baseUrl || process.env.WISPBIT_API_BASE_URL || "https://api.wispbit.com";
|
|
128
|
+
const finalApiKey = apiKey || process.env.WISPBIT_API_KEY || null;
|
|
129
|
+
if (!finalApiKey) {
|
|
130
|
+
console.log("No API key found");
|
|
131
|
+
return { failed: true, error: "INVALID_API_KEY" };
|
|
132
|
+
}
|
|
133
|
+
const repositoryUrl = await environment.getRepositoryUrl();
|
|
134
|
+
const isValidApiKey = await _Config.validateApiKey(finalApiKey, repositoryUrl, finalBaseUrl);
|
|
135
|
+
if (!isValidApiKey) {
|
|
136
|
+
return { failed: true, error: "INVALID_API_KEY" };
|
|
137
|
+
}
|
|
138
|
+
const response = await fetch(`${finalBaseUrl}/plv1/initialize`, {
|
|
139
|
+
method: "POST",
|
|
140
|
+
headers: {
|
|
141
|
+
"Content-Type": "application/json",
|
|
142
|
+
Authorization: `Bearer ${finalApiKey}`
|
|
143
|
+
},
|
|
144
|
+
body: JSON.stringify({
|
|
145
|
+
repository_url: repositoryUrl,
|
|
146
|
+
powerlint_version: getCurrentVersion(),
|
|
147
|
+
schema_version: "v1"
|
|
148
|
+
})
|
|
149
|
+
});
|
|
150
|
+
const result = await response.json();
|
|
151
|
+
if (result.invalid_api_key) {
|
|
152
|
+
return { failed: true, error: "INVALID_API_KEY" };
|
|
153
|
+
}
|
|
154
|
+
if (!result.is_valid_repository) {
|
|
155
|
+
return { failed: true, error: "INVALID_REPOSITORY" };
|
|
156
|
+
}
|
|
157
|
+
const ignoredGlobs = ((_a = result.config) == null ? void 0 : _a.ignored_globs) || [];
|
|
158
|
+
const config = new _Config({ ignoredGlobs, apiKey: finalApiKey, baseUrl: finalBaseUrl });
|
|
159
|
+
return config;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Check if PowerLint is configured (has valid API key)
|
|
163
|
+
*/
|
|
164
|
+
isConfigured() {
|
|
165
|
+
return this.getApiKey() !== null;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// src/utils/git.ts
|
|
170
|
+
import { exec, execSync } from "child_process";
|
|
171
|
+
import { promisify } from "util";
|
|
172
|
+
|
|
173
|
+
// src/utils/hashString.ts
|
|
174
|
+
import { createHash } from "crypto";
|
|
175
|
+
function hashString(str) {
|
|
176
|
+
return createHash("sha256").update(str).digest("hex");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/utils/git.ts
|
|
180
|
+
var execPromise = promisify(exec);
|
|
181
|
+
function findGitRoot() {
|
|
182
|
+
const stdout = execSync("git rev-parse --show-toplevel", { encoding: "utf-8" });
|
|
183
|
+
return stdout.trim();
|
|
184
|
+
}
|
|
185
|
+
async function getRepositoryUrl(repoRoot, remoteName = "origin") {
|
|
186
|
+
var _a;
|
|
187
|
+
const { stdout } = await execPromise(`git remote show ${remoteName}`, {
|
|
188
|
+
cwd: repoRoot
|
|
189
|
+
});
|
|
190
|
+
const fetchUrlLine = stdout.split("\n").find((line) => line.includes("Fetch URL:"));
|
|
191
|
+
if (fetchUrlLine) {
|
|
192
|
+
return ((_a = fetchUrlLine.split("Fetch URL:").pop()) == null ? void 0 : _a.trim()) || null;
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
async function getDefaultBranch(repoRoot, remoteName = "origin") {
|
|
197
|
+
var _a;
|
|
198
|
+
const { stdout } = await execPromise(`git remote show ${remoteName}`, {
|
|
199
|
+
cwd: repoRoot
|
|
200
|
+
});
|
|
201
|
+
const headBranchLine = stdout.split("\n").find((line) => line.includes("HEAD branch"));
|
|
202
|
+
if (headBranchLine) {
|
|
203
|
+
return ((_a = headBranchLine.split(":").pop()) == null ? void 0 : _a.trim()) || null;
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
async function getChangedFiles(repoRoot, base) {
|
|
208
|
+
var _a, _b;
|
|
209
|
+
const { stdout: currentBranchOutput } = await execPromise("git rev-parse --abbrev-ref HEAD", {
|
|
210
|
+
cwd: repoRoot
|
|
211
|
+
});
|
|
212
|
+
const currentBranch = currentBranchOutput.trim();
|
|
213
|
+
const defaultBranch = await getDefaultBranch(repoRoot);
|
|
214
|
+
const compareTo = base ?? (defaultBranch ? `origin/${defaultBranch}` : "HEAD^");
|
|
215
|
+
let currentCommit;
|
|
216
|
+
try {
|
|
217
|
+
const { stdout: remoteCommitOutput } = await execPromise(
|
|
218
|
+
`git rev-parse origin/${currentBranch}`,
|
|
219
|
+
{ cwd: repoRoot }
|
|
220
|
+
);
|
|
221
|
+
currentCommit = remoteCommitOutput.trim();
|
|
222
|
+
} catch (error) {
|
|
223
|
+
const { stdout: currentCommitOutput } = await execPromise("git rev-parse HEAD", {
|
|
224
|
+
cwd: repoRoot
|
|
225
|
+
});
|
|
226
|
+
currentCommit = currentCommitOutput.trim();
|
|
227
|
+
}
|
|
228
|
+
let mergeBase;
|
|
229
|
+
try {
|
|
230
|
+
const { stdout: mergeBaseOutput } = await execPromise(
|
|
231
|
+
`git merge-base ${currentBranch} ${compareTo}`,
|
|
232
|
+
{ cwd: repoRoot }
|
|
233
|
+
);
|
|
234
|
+
mergeBase = mergeBaseOutput.trim();
|
|
235
|
+
} catch (error) {
|
|
236
|
+
mergeBase = "HEAD^";
|
|
237
|
+
}
|
|
238
|
+
const { stdout: statusOutput } = await execPromise("git status --porcelain", {
|
|
239
|
+
cwd: repoRoot
|
|
240
|
+
});
|
|
241
|
+
const statusLines = statusOutput.split("\n").filter(Boolean);
|
|
242
|
+
const fileStatuses = /* @__PURE__ */ new Map();
|
|
243
|
+
statusLines.forEach((line) => {
|
|
244
|
+
const statusCode = line.substring(0, 2).trim();
|
|
245
|
+
const filename = line.substring(3);
|
|
246
|
+
fileStatuses.set(filename, statusCode);
|
|
247
|
+
});
|
|
248
|
+
const { stdout: diffOutput } = await execPromise(`git diff ${mergeBase} --name-only`, {
|
|
249
|
+
cwd: repoRoot
|
|
250
|
+
});
|
|
251
|
+
const allFiles = diffOutput.split("\n").filter(Boolean);
|
|
252
|
+
const { stdout: deletedFilesOutput } = await execPromise("git ls-files --deleted", {
|
|
253
|
+
cwd: repoRoot
|
|
254
|
+
});
|
|
255
|
+
const deletedFiles = deletedFilesOutput.split("\n").filter(Boolean);
|
|
256
|
+
allFiles.push(...deletedFiles.filter((file) => !allFiles.includes(file)));
|
|
257
|
+
const { stdout: untrackedOutput } = await execPromise(
|
|
258
|
+
"git ls-files --others --exclude-standard",
|
|
259
|
+
{
|
|
260
|
+
cwd: repoRoot
|
|
261
|
+
}
|
|
262
|
+
);
|
|
263
|
+
const untrackedFiles = untrackedOutput.split("\n").filter(Boolean);
|
|
264
|
+
allFiles.push(...untrackedFiles.filter((file) => !allFiles.includes(file)));
|
|
265
|
+
const nonDeletedFiles = [];
|
|
266
|
+
const deletedFilesSet = new Set(deletedFiles);
|
|
267
|
+
const fileIsDeleted = /* @__PURE__ */ new Map();
|
|
268
|
+
for (const file of allFiles) {
|
|
269
|
+
const isDeleted = deletedFilesSet.has(file) || ((_a = fileStatuses.get(file)) == null ? void 0 : _a.includes("D")) || ((_b = fileStatuses.get(file)) == null ? void 0 : _b.includes("R"));
|
|
270
|
+
fileIsDeleted.set(file, Boolean(isDeleted));
|
|
271
|
+
if (!isDeleted) {
|
|
272
|
+
nonDeletedFiles.push(file);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const fileStats = /* @__PURE__ */ new Map();
|
|
276
|
+
if (nonDeletedFiles.length > 0) {
|
|
277
|
+
const { stdout: batchNumstatOutput } = await execPromise(
|
|
278
|
+
`git diff ${mergeBase} --numstat -- ${nonDeletedFiles.map((f) => `'${f.replace(/'/g, "'\\''")}'`).join(" ")}`,
|
|
279
|
+
{ cwd: repoRoot }
|
|
280
|
+
);
|
|
281
|
+
const numstatLines = batchNumstatOutput.split("\n").filter(Boolean);
|
|
282
|
+
numstatLines.forEach((line) => {
|
|
283
|
+
const parts = line.split(" ");
|
|
284
|
+
if (parts.length >= 3) {
|
|
285
|
+
const [additionsStr, deletionsStr, filename] = parts;
|
|
286
|
+
fileStats.set(filename, {
|
|
287
|
+
additions: parseInt(additionsStr) || 0,
|
|
288
|
+
deletions: parseInt(deletionsStr) || 0
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
const fileDiffs = /* @__PURE__ */ new Map();
|
|
294
|
+
if (nonDeletedFiles.length > 0) {
|
|
295
|
+
const { stdout: batchDiffOutput } = await execPromise(
|
|
296
|
+
`git diff ${mergeBase} -- ${nonDeletedFiles.map((f) => `'${f.replace(/'/g, "'\\''")}'`).join(" ")}`,
|
|
297
|
+
{ cwd: repoRoot }
|
|
298
|
+
);
|
|
299
|
+
const diffSections = batchDiffOutput.split(/^diff --git /m).filter(Boolean);
|
|
300
|
+
diffSections.forEach((section) => {
|
|
301
|
+
const lines = section.split("\n");
|
|
302
|
+
const firstLine = lines[0];
|
|
303
|
+
const match = firstLine.match(/a\/(.+?) b\//);
|
|
304
|
+
if (match) {
|
|
305
|
+
const filename = match[1];
|
|
306
|
+
const diffContent = lines.slice(1).filter((line) => {
|
|
307
|
+
return !(line.startsWith("index ") || line.startsWith("--- ") || line.startsWith("+++ "));
|
|
308
|
+
}).join("\n");
|
|
309
|
+
fileDiffs.set(filename, diffContent);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
const deletedFileContents = /* @__PURE__ */ new Map();
|
|
314
|
+
const actualDeletedFiles = allFiles.filter((file) => fileIsDeleted.get(file));
|
|
315
|
+
if (actualDeletedFiles.length > 0) {
|
|
316
|
+
const deletedFilePromises = actualDeletedFiles.map(async (file) => {
|
|
317
|
+
const { stdout: lastContent } = await execPromise(
|
|
318
|
+
`git show '${mergeBase}:${file.replace(/'/g, "'\\''")}'`,
|
|
319
|
+
{
|
|
320
|
+
cwd: repoRoot
|
|
321
|
+
}
|
|
322
|
+
);
|
|
323
|
+
return { file, content: lastContent };
|
|
324
|
+
});
|
|
325
|
+
const results = await Promise.allSettled(deletedFilePromises);
|
|
326
|
+
results.forEach((result, index) => {
|
|
327
|
+
if (result.status === "fulfilled") {
|
|
328
|
+
deletedFileContents.set(actualDeletedFiles[index], result.value.content);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
const fileChanges = [];
|
|
333
|
+
for (const file of allFiles) {
|
|
334
|
+
const isDeleted = fileIsDeleted.get(file);
|
|
335
|
+
let additions = 0;
|
|
336
|
+
let deletions = 0;
|
|
337
|
+
let diffOutput2 = "";
|
|
338
|
+
if (isDeleted) {
|
|
339
|
+
const lastContent = deletedFileContents.get(file);
|
|
340
|
+
if (lastContent) {
|
|
341
|
+
deletions = lastContent.split("\n").length;
|
|
342
|
+
diffOutput2 = lastContent.split("\n").map((line) => `-${line}`).join("\n");
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
const stats = fileStats.get(file);
|
|
346
|
+
if (stats) {
|
|
347
|
+
additions = stats.additions;
|
|
348
|
+
deletions = stats.deletions;
|
|
349
|
+
}
|
|
350
|
+
diffOutput2 = fileDiffs.get(file) || "";
|
|
351
|
+
}
|
|
352
|
+
const status = isDeleted ? "removed" : additions > 0 && deletions === 0 ? "added" : "modified";
|
|
353
|
+
fileChanges.push({
|
|
354
|
+
filename: file,
|
|
355
|
+
status,
|
|
356
|
+
patch: diffOutput2,
|
|
357
|
+
additions,
|
|
358
|
+
deletions,
|
|
359
|
+
sha: hashString(diffOutput2)
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
files: fileChanges,
|
|
364
|
+
currentBranch,
|
|
365
|
+
currentCommit,
|
|
366
|
+
diffCommit: mergeBase,
|
|
367
|
+
diffBranch: compareTo
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// src/environment/Environment.ts
|
|
372
|
+
var Environment = class {
|
|
373
|
+
workspaceRoot;
|
|
374
|
+
repositoryUrl;
|
|
375
|
+
constructor(config) {
|
|
376
|
+
this.repositoryUrl = (config == null ? void 0 : config.repositoryUrl) || null;
|
|
377
|
+
this.workspaceRoot = (config == null ? void 0 : config.workspaceRoot) || findGitRoot();
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Get the workspace root directory
|
|
381
|
+
*/
|
|
382
|
+
getWorkspaceRoot() {
|
|
383
|
+
return this.workspaceRoot;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Get the remote repository URL from Git config
|
|
387
|
+
* @param remoteName Name of the remote (default: origin)
|
|
388
|
+
* @returns The remote repository URL or null if not found
|
|
389
|
+
*/
|
|
390
|
+
async getRepositoryUrl(remoteName = "origin") {
|
|
391
|
+
if (this.repositoryUrl) {
|
|
392
|
+
return this.repositoryUrl;
|
|
393
|
+
}
|
|
394
|
+
const repositoryUrl = await getRepositoryUrl(this.workspaceRoot, remoteName);
|
|
395
|
+
if (!repositoryUrl) {
|
|
396
|
+
throw new Error(
|
|
397
|
+
"Could not determine repository URL. Make sure you're in a Git repository with a remote origin."
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
return repositoryUrl;
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
// src/environment/Storage.ts
|
|
405
|
+
import * as fs from "fs/promises";
|
|
406
|
+
import os from "os";
|
|
407
|
+
import path from "path";
|
|
408
|
+
import Keyv from "keyv";
|
|
409
|
+
import { KeyvFile } from "keyv-file";
|
|
410
|
+
var Storage = class {
|
|
411
|
+
environment;
|
|
412
|
+
cacheStore;
|
|
413
|
+
constructor(environment) {
|
|
414
|
+
this.environment = environment;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Get the base storage directory for powerlint
|
|
418
|
+
* This replaces the getConfigDirectory functionality
|
|
419
|
+
*/
|
|
420
|
+
getStorageDirectory() {
|
|
421
|
+
return path.join(os.homedir(), ".powerlint");
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Get the base directory for indexes
|
|
425
|
+
*/
|
|
426
|
+
getIndexDirectory() {
|
|
427
|
+
return path.join(
|
|
428
|
+
this.getStorageDirectory(),
|
|
429
|
+
"indexes",
|
|
430
|
+
hashString(this.environment.getWorkspaceRoot())
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Get the base directory for caches
|
|
435
|
+
*/
|
|
436
|
+
getCacheDirectory() {
|
|
437
|
+
return path.join(
|
|
438
|
+
this.getStorageDirectory(),
|
|
439
|
+
"cache",
|
|
440
|
+
hashString(this.environment.getWorkspaceRoot())
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Ensure a directory exists, creating it if necessary
|
|
445
|
+
*/
|
|
446
|
+
async ensureDirectory(dirPath) {
|
|
447
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Get the file path for a specific index
|
|
451
|
+
* Ensures the index directory exists
|
|
452
|
+
*/
|
|
453
|
+
async getIndexFilePath(language, fileName) {
|
|
454
|
+
const indexDir = this.getIndexDirectory();
|
|
455
|
+
await this.ensureDirectory(indexDir);
|
|
456
|
+
const file = fileName || `${language.toLowerCase()}-index.scip`;
|
|
457
|
+
return path.join(indexDir, `${language.toLowerCase()}-${file}`);
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Check if an index exists for a language
|
|
461
|
+
*/
|
|
462
|
+
async indexExists(language, fileName) {
|
|
463
|
+
const indexPath = await this.getIndexFilePath(language, fileName);
|
|
464
|
+
try {
|
|
465
|
+
await fs.access(indexPath);
|
|
466
|
+
return true;
|
|
467
|
+
} catch {
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Read an index file for a language
|
|
473
|
+
*/
|
|
474
|
+
async readIndex(language, fileName) {
|
|
475
|
+
const indexPath = await this.getIndexFilePath(language, fileName);
|
|
476
|
+
try {
|
|
477
|
+
await fs.access(indexPath);
|
|
478
|
+
return await fs.readFile(indexPath);
|
|
479
|
+
} catch {
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Save an index file for a language
|
|
485
|
+
*/
|
|
486
|
+
async saveIndex(language, data, fileName) {
|
|
487
|
+
const indexPath = await this.getIndexFilePath(language, fileName);
|
|
488
|
+
await fs.writeFile(indexPath, data);
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Get the cache file path
|
|
492
|
+
*/
|
|
493
|
+
getCacheFilePath() {
|
|
494
|
+
return path.join(this.getCacheDirectory(), "cache.json");
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Purge all storage (cache and indexes)
|
|
498
|
+
* @returns Object with success status and details about what was purged
|
|
499
|
+
*/
|
|
500
|
+
async purgeStorage() {
|
|
501
|
+
let deletedCount = 0;
|
|
502
|
+
const storagePath = this.getStorageDirectory();
|
|
503
|
+
try {
|
|
504
|
+
await fs.access(storagePath);
|
|
505
|
+
await fs.rm(storagePath, { recursive: true, force: true });
|
|
506
|
+
deletedCount++;
|
|
507
|
+
} catch {
|
|
508
|
+
}
|
|
509
|
+
return {
|
|
510
|
+
success: true,
|
|
511
|
+
deletedCount
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Purge only cache data
|
|
516
|
+
*/
|
|
517
|
+
async purgeCache() {
|
|
518
|
+
let deletedCount = 0;
|
|
519
|
+
const cachePath = this.getCacheDirectory();
|
|
520
|
+
try {
|
|
521
|
+
await fs.access(cachePath);
|
|
522
|
+
await fs.rm(cachePath, { recursive: true, force: true });
|
|
523
|
+
deletedCount++;
|
|
524
|
+
} catch {
|
|
525
|
+
}
|
|
526
|
+
return {
|
|
527
|
+
success: true,
|
|
528
|
+
deletedCount
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Get or initialize the cache store
|
|
533
|
+
*/
|
|
534
|
+
getCacheStore() {
|
|
535
|
+
if (!this.cacheStore) {
|
|
536
|
+
const cacheDir = this.getCacheDirectory();
|
|
537
|
+
this.cacheStore = new Keyv({
|
|
538
|
+
store: new KeyvFile({
|
|
539
|
+
filename: path.join(cacheDir, "cache.json")
|
|
540
|
+
})
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
return this.cacheStore;
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Save data to cache
|
|
547
|
+
*/
|
|
548
|
+
async saveCache(ruleId, cacheKey, data) {
|
|
549
|
+
const cache = this.getCacheStore();
|
|
550
|
+
const key = `${ruleId}:${cacheKey}`;
|
|
551
|
+
await cache.set(key, data);
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Read data from cache
|
|
555
|
+
*/
|
|
556
|
+
async readCache(ruleId, cacheKey) {
|
|
557
|
+
const cache = this.getCacheStore();
|
|
558
|
+
const key = `${ruleId}:${cacheKey}`;
|
|
559
|
+
const data = await cache.get(key);
|
|
560
|
+
return data || null;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Purge only index data
|
|
564
|
+
*/
|
|
565
|
+
async purgeIndexes() {
|
|
566
|
+
let deletedCount = 0;
|
|
567
|
+
const indexPath = this.getIndexDirectory();
|
|
568
|
+
try {
|
|
569
|
+
await fs.access(indexPath);
|
|
570
|
+
await fs.rm(indexPath, { recursive: true, force: true });
|
|
571
|
+
deletedCount++;
|
|
572
|
+
} catch {
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
success: true,
|
|
576
|
+
deletedCount
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
// src/providers/WispbitRuleProvider.ts
|
|
582
|
+
import { z } from "zod";
|
|
583
|
+
var GetRulesSchema = z.object({
|
|
584
|
+
repository_url: z.string(),
|
|
585
|
+
rule_ids: z.array(z.string()).optional(),
|
|
586
|
+
schema_version: z.string(),
|
|
587
|
+
powerlint_version: z.string()
|
|
588
|
+
});
|
|
589
|
+
var WispbitRuleProvider = class {
|
|
590
|
+
config;
|
|
591
|
+
environment;
|
|
592
|
+
constructor(config, environment) {
|
|
593
|
+
this.config = config;
|
|
594
|
+
this.environment = environment;
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Make a request to the Wispbit API
|
|
598
|
+
*/
|
|
599
|
+
async makeApiRequest(endpoint, data) {
|
|
600
|
+
const baseUrl = this.config.getBaseUrl();
|
|
601
|
+
const apiKey = this.config.getApiKey();
|
|
602
|
+
const url = `${baseUrl}${endpoint}`;
|
|
603
|
+
const response = await fetch(url, {
|
|
604
|
+
method: "POST",
|
|
605
|
+
headers: {
|
|
606
|
+
"Content-Type": "application/json",
|
|
607
|
+
Authorization: `Bearer ${apiKey}`
|
|
608
|
+
},
|
|
609
|
+
body: JSON.stringify(data)
|
|
610
|
+
});
|
|
611
|
+
if (!response.ok) {
|
|
612
|
+
throw new Error(`Wispbit API request failed: ${response.status} ${response.statusText}`);
|
|
613
|
+
}
|
|
614
|
+
return await response.json();
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Get the repository URL for API requests
|
|
618
|
+
*/
|
|
619
|
+
async getRepositoryUrl() {
|
|
620
|
+
const repoUrl = await this.environment.getRepositoryUrl();
|
|
621
|
+
if (!repoUrl) {
|
|
622
|
+
throw new Error(
|
|
623
|
+
"Could not determine repository URL. Make sure you're in a Git repository with a remote origin."
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
return repoUrl;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Load a specific rule by ID from Wispbit Cloud
|
|
630
|
+
*/
|
|
631
|
+
async loadRuleById(ruleId) {
|
|
632
|
+
const rules = await this.fetchRules([ruleId]);
|
|
633
|
+
if (rules.length === 0) {
|
|
634
|
+
throw new Error(`Rule with ID '${ruleId}' not found`);
|
|
635
|
+
}
|
|
636
|
+
return rules[0];
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Load all rules from Wispbit Cloud for the configured repository
|
|
640
|
+
*/
|
|
641
|
+
async loadAllRules() {
|
|
642
|
+
return await this.fetchRules();
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Fetch rules from Wispbit Cloud API
|
|
646
|
+
*/
|
|
647
|
+
async fetchRules(ruleIds) {
|
|
648
|
+
const repositoryUrl = await this.getRepositoryUrl();
|
|
649
|
+
const requestData = {
|
|
650
|
+
repository_url: repositoryUrl,
|
|
651
|
+
rule_ids: ruleIds,
|
|
652
|
+
schema_version: this.config.getSchemaVersion(),
|
|
653
|
+
powerlint_version: this.config.getLocalVersion()
|
|
654
|
+
};
|
|
655
|
+
GetRulesSchema.parse(requestData);
|
|
656
|
+
const response = await this.makeApiRequest("/plv1/get-rules", requestData);
|
|
657
|
+
if (!Array.isArray(response.rules)) {
|
|
658
|
+
throw new Error("Invalid response format from Wispbit API: expected rules array");
|
|
659
|
+
}
|
|
660
|
+
const rules = response.rules;
|
|
661
|
+
return rules.map((rule) => ({
|
|
662
|
+
id: rule.id,
|
|
663
|
+
internalId: rule.internalId,
|
|
664
|
+
config: {
|
|
665
|
+
message: rule.message,
|
|
666
|
+
severity: rule.severity,
|
|
667
|
+
steps: rule.schema
|
|
668
|
+
},
|
|
669
|
+
prompt: rule.prompt,
|
|
670
|
+
testCases: []
|
|
671
|
+
}));
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Create a new rule in Wispbit Cloud
|
|
675
|
+
*/
|
|
676
|
+
async createRule(_rule) {
|
|
677
|
+
await Promise.resolve();
|
|
678
|
+
throw new Error("Creating rules in Wispbit Cloud is not yet implemented");
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Update an existing rule in Wispbit Cloud
|
|
682
|
+
*/
|
|
683
|
+
async updateRule(_ruleId, _rule) {
|
|
684
|
+
await Promise.resolve();
|
|
685
|
+
throw new Error("Updating rules in Wispbit Cloud is not yet implemented");
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Delete a rule from Wispbit Cloud
|
|
689
|
+
*/
|
|
690
|
+
async deleteRule(_ruleId) {
|
|
691
|
+
await Promise.resolve();
|
|
692
|
+
throw new Error("Deleting rules from Wispbit Cloud is not yet implemented");
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
// src/steps/ExecutionEventEmitter.ts
|
|
697
|
+
import { EventEmitter } from "events";
|
|
698
|
+
var ExecutionEventEmitter = class extends EventEmitter {
|
|
699
|
+
rulesStartTime = 0;
|
|
700
|
+
indexingStartTimes = /* @__PURE__ */ new Map();
|
|
701
|
+
fileDiscoveryStartTime = 0;
|
|
702
|
+
constructor() {
|
|
703
|
+
super();
|
|
704
|
+
this.setMaxListeners(20);
|
|
705
|
+
}
|
|
706
|
+
// Type-safe event emission
|
|
707
|
+
emit(event, data) {
|
|
708
|
+
return super.emit(event, data);
|
|
709
|
+
}
|
|
710
|
+
// Type-safe event listening
|
|
711
|
+
on(event, listener) {
|
|
712
|
+
return super.on(event, listener);
|
|
713
|
+
}
|
|
714
|
+
once(event, listener) {
|
|
715
|
+
return super.once(event, listener);
|
|
716
|
+
}
|
|
717
|
+
off(event, listener) {
|
|
718
|
+
return super.off(event, listener);
|
|
719
|
+
}
|
|
720
|
+
// Helper method for execution mode
|
|
721
|
+
setExecutionMode(mode, options) {
|
|
722
|
+
this.emit("execution:mode", { mode, ...options });
|
|
723
|
+
}
|
|
724
|
+
// Helper methods for rule progress
|
|
725
|
+
startRules(totalRules) {
|
|
726
|
+
this.rulesStartTime = Date.now();
|
|
727
|
+
this.emit("rules:start", { totalRules });
|
|
728
|
+
}
|
|
729
|
+
progressRule(currentRule, totalRules, ruleId, isLlm) {
|
|
730
|
+
const percentage = Math.round(currentRule / totalRules * 100);
|
|
731
|
+
this.emit("rules:progress", { currentRule, totalRules, ruleId, percentage, isLlm });
|
|
732
|
+
}
|
|
733
|
+
completeRules(totalRules, totalMatches) {
|
|
734
|
+
const executionTime = Date.now() - this.rulesStartTime;
|
|
735
|
+
this.emit("rules:complete", { totalRules, totalMatches, executionTime });
|
|
736
|
+
}
|
|
737
|
+
// Helper methods for file discovery
|
|
738
|
+
startFileDiscovery(mode) {
|
|
739
|
+
this.fileDiscoveryStartTime = Date.now();
|
|
740
|
+
this.emit("files:discovery:start", { mode });
|
|
741
|
+
}
|
|
742
|
+
fileDiscoveryProgress(message, currentCount) {
|
|
743
|
+
this.emit("files:discovery:progress", { message, currentCount });
|
|
744
|
+
}
|
|
745
|
+
completeFileDiscovery(totalFiles, mode) {
|
|
746
|
+
const executionTime = Date.now() - this.fileDiscoveryStartTime;
|
|
747
|
+
this.emit("files:discovery:complete", { totalFiles, mode, executionTime });
|
|
748
|
+
}
|
|
749
|
+
fileFilter(originalCount, filteredCount, filterType) {
|
|
750
|
+
this.emit("files:filter", { originalCount, filteredCount, filterType });
|
|
751
|
+
}
|
|
752
|
+
startScipMatchLookup(language, match) {
|
|
753
|
+
this.emit("scip:match-lookup:start", { language, match });
|
|
754
|
+
}
|
|
755
|
+
scipMatchLookupProgress(language, document) {
|
|
756
|
+
this.emit("scip:match-lookup:progress", { language, document });
|
|
757
|
+
}
|
|
758
|
+
scipMatchLookupComplete(language, document) {
|
|
759
|
+
this.emit("scip:match-lookup:complete", { language, document });
|
|
760
|
+
}
|
|
761
|
+
// Helper methods for indexing
|
|
762
|
+
startIndexing(language) {
|
|
763
|
+
this.indexingStartTimes.set(language, Date.now());
|
|
764
|
+
this.emit("indexing:start", { language });
|
|
765
|
+
}
|
|
766
|
+
indexingProgress(language, message, packageName, timeMs) {
|
|
767
|
+
this.emit("indexing:progress", { language, message, packageName, timeMs });
|
|
768
|
+
}
|
|
769
|
+
completeIndexing(language) {
|
|
770
|
+
const startTime = this.indexingStartTimes.get(language) || Date.now();
|
|
771
|
+
const executionTime = Date.now() - startTime;
|
|
772
|
+
this.indexingStartTimes.delete(language);
|
|
773
|
+
this.emit("indexing:complete", { language, executionTime });
|
|
774
|
+
}
|
|
775
|
+
// Helper methods for step debugging
|
|
776
|
+
startStep(ruleId, stepName, stepType, inputs) {
|
|
777
|
+
this.emit("step:start", { ruleId, stepName, stepType, inputs });
|
|
778
|
+
}
|
|
779
|
+
completeStep(ruleId, stepName, stepType, outputs, executionTime = 0) {
|
|
780
|
+
this.emit("step:complete", { ruleId, stepName, stepType, outputs, executionTime });
|
|
781
|
+
}
|
|
782
|
+
// Helper methods for test events
|
|
783
|
+
startTest(ruleId, testName) {
|
|
784
|
+
this.emit("test:start", { ruleId, testName });
|
|
785
|
+
}
|
|
786
|
+
testMatches(ruleId, testName, matches) {
|
|
787
|
+
this.emit("test:matches", { ruleId, testName, matches });
|
|
788
|
+
}
|
|
789
|
+
// Helper methods for LLM validation events
|
|
790
|
+
startLLMValidation(ruleId, matchCount, estimatedCost) {
|
|
791
|
+
this.emit("llm:validation:start", { ruleId, matchCount, estimatedCost });
|
|
792
|
+
}
|
|
793
|
+
llmValidationProgress(ruleId, tokenCount, elapsedTime, streamedText) {
|
|
794
|
+
this.emit("llm:validation:progress", { ruleId, tokenCount, elapsedTime, streamedText });
|
|
795
|
+
}
|
|
796
|
+
completeLLMValidation(ruleId, tokenCount, executionTime, violationCount, fromCache) {
|
|
797
|
+
this.emit("llm:validation:complete", {
|
|
798
|
+
ruleId,
|
|
799
|
+
tokenCount,
|
|
800
|
+
executionTime,
|
|
801
|
+
violationCount,
|
|
802
|
+
fromCache
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
// src/steps/FileExecutionContext.ts
|
|
808
|
+
import * as fs2 from "fs";
|
|
809
|
+
import * as path2 from "path";
|
|
810
|
+
import { glob } from "glob";
|
|
811
|
+
import { minimatch } from "minimatch";
|
|
812
|
+
var FileExecutionContext = class _FileExecutionContext {
|
|
813
|
+
environment;
|
|
814
|
+
_filePaths = [];
|
|
815
|
+
mode;
|
|
816
|
+
eventEmitter;
|
|
817
|
+
config;
|
|
818
|
+
diffMode;
|
|
819
|
+
constructor(config, environment, mode, eventEmitter) {
|
|
820
|
+
this.environment = environment;
|
|
821
|
+
this.mode = mode;
|
|
822
|
+
this.eventEmitter = eventEmitter || new ExecutionEventEmitter();
|
|
823
|
+
this.config = config;
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Create and initialize an ExecutionContext
|
|
827
|
+
*/
|
|
828
|
+
static async initialize(config, environment, mode, eventEmitter, filePath, baseSha) {
|
|
829
|
+
const context = new _FileExecutionContext(config, environment, mode, eventEmitter);
|
|
830
|
+
const initialFiles = await context.discoverFiles(filePath, baseSha);
|
|
831
|
+
context._filePaths = initialFiles;
|
|
832
|
+
return context;
|
|
833
|
+
}
|
|
834
|
+
// ===== State Management =====
|
|
835
|
+
get filePaths() {
|
|
836
|
+
return this._filePaths;
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Filter files based on execution mode and return filtered files
|
|
840
|
+
*/
|
|
841
|
+
filterFiles(filePaths) {
|
|
842
|
+
const originalCount = filePaths.length;
|
|
843
|
+
let filteredFiles;
|
|
844
|
+
if (this.mode === "check") {
|
|
845
|
+
filteredFiles = filePaths;
|
|
846
|
+
} else if (!this.diffMode) {
|
|
847
|
+
filteredFiles = filePaths;
|
|
848
|
+
} else {
|
|
849
|
+
filteredFiles = filePaths.filter((filePath) => this.isFileValid({ filePath }));
|
|
850
|
+
}
|
|
851
|
+
if (originalCount !== filteredFiles.length) {
|
|
852
|
+
this.eventEmitter.fileFilter(originalCount, filteredFiles.length, `${this.mode}-mode-files`);
|
|
853
|
+
}
|
|
854
|
+
return filteredFiles;
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Filter matches based on execution mode and return filtered matches
|
|
858
|
+
*/
|
|
859
|
+
filterMatches(matches) {
|
|
860
|
+
const originalCount = matches.length;
|
|
861
|
+
const filteredMatches = matches.filter((match) => this.isMatchValid({ match }));
|
|
862
|
+
if (originalCount !== filteredMatches.length) {
|
|
863
|
+
this.eventEmitter.fileFilter(
|
|
864
|
+
originalCount,
|
|
865
|
+
filteredMatches.length,
|
|
866
|
+
`${this.mode}-mode-matches`
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
return filteredMatches;
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Filter matches to only include those from the given file paths
|
|
873
|
+
*/
|
|
874
|
+
filterMatchesByFilePaths(matches, filePaths) {
|
|
875
|
+
if (matches.length === 0) {
|
|
876
|
+
return matches;
|
|
877
|
+
}
|
|
878
|
+
const filePathSet = new Set(filePaths);
|
|
879
|
+
return matches.filter((match) => filePathSet.has(match.filePath));
|
|
880
|
+
}
|
|
881
|
+
get executionMode() {
|
|
882
|
+
return this.mode;
|
|
883
|
+
}
|
|
884
|
+
// ===== File Discovery =====
|
|
885
|
+
/**
|
|
886
|
+
* Discover files based on the execution mode
|
|
887
|
+
* In 'scan' mode: discovers all files in workspace
|
|
888
|
+
* In 'diff' mode: gets changed files from git and returns only those
|
|
889
|
+
*
|
|
890
|
+
* The filePath parameter can be:
|
|
891
|
+
* - A specific filename (e.g., "src/file.ts")
|
|
892
|
+
* - A directory path (e.g., "src/components/")
|
|
893
|
+
* - A glob pattern (e.g., ".ts")
|
|
894
|
+
*/
|
|
895
|
+
async discoverFiles(filePath, baseSha) {
|
|
896
|
+
this.eventEmitter.startFileDiscovery(this.mode);
|
|
897
|
+
let discoveredFiles;
|
|
898
|
+
if (filePath) {
|
|
899
|
+
discoveredFiles = await this.discoverFilesFromPath(filePath);
|
|
900
|
+
} else if (this.mode === "diff") {
|
|
901
|
+
const workspaceRoot = this.environment.getWorkspaceRoot();
|
|
902
|
+
if (baseSha) {
|
|
903
|
+
this.eventEmitter.fileDiscoveryProgress(`Getting changed files from ${baseSha}...`);
|
|
904
|
+
} else {
|
|
905
|
+
this.eventEmitter.fileDiscoveryProgress("Getting changed files from git...");
|
|
906
|
+
}
|
|
907
|
+
const gitChanges = await getChangedFiles(workspaceRoot, baseSha);
|
|
908
|
+
this.diffMode = {
|
|
909
|
+
gitChanges,
|
|
910
|
+
changedFiles: gitChanges.files.map((f) => f.filename),
|
|
911
|
+
fileChangeMap: new Map(gitChanges.files.map((file) => [file.filename, file]))
|
|
912
|
+
};
|
|
913
|
+
this.eventEmitter.fileDiscoveryProgress(
|
|
914
|
+
`Found ${this.diffMode.changedFiles.length} changed files`
|
|
915
|
+
);
|
|
916
|
+
discoveredFiles = this.diffMode.changedFiles.filter((file) => fs2.existsSync(path2.resolve(workspaceRoot, file))).map((file) => file);
|
|
917
|
+
this.eventEmitter.fileDiscoveryProgress("Applying ignore patterns...");
|
|
918
|
+
const allIgnorePatterns = this.config.getIgnoredGlobs();
|
|
919
|
+
if (allIgnorePatterns.length > 0) {
|
|
920
|
+
const beforeIgnore = discoveredFiles.length;
|
|
921
|
+
discoveredFiles = discoveredFiles.filter((filePath2) => {
|
|
922
|
+
const matchesIgnore = allIgnorePatterns.some(
|
|
923
|
+
(pattern) => this.matchesPattern(filePath2, pattern)
|
|
924
|
+
);
|
|
925
|
+
return !matchesIgnore;
|
|
926
|
+
});
|
|
927
|
+
if (beforeIgnore !== discoveredFiles.length) {
|
|
928
|
+
this.eventEmitter.fileDiscoveryProgress(
|
|
929
|
+
`Filtered out ${beforeIgnore - discoveredFiles.length} ignored files`
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
} else {
|
|
934
|
+
this.eventEmitter.fileDiscoveryProgress("Scanning workspace for files...");
|
|
935
|
+
const workspaceRoot = this.environment.getWorkspaceRoot();
|
|
936
|
+
const allIgnorePatterns = this.config.getIgnoredGlobs();
|
|
937
|
+
discoveredFiles = await this.discoverAllFiles([workspaceRoot], allIgnorePatterns);
|
|
938
|
+
}
|
|
939
|
+
this.eventEmitter.completeFileDiscovery(discoveredFiles.length, this.mode);
|
|
940
|
+
return discoveredFiles;
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Discover files from a given path which can be a file, directory, or glob pattern
|
|
944
|
+
*/
|
|
945
|
+
async discoverFilesFromPath(filePath) {
|
|
946
|
+
const workspaceRoot = this.environment.getWorkspaceRoot();
|
|
947
|
+
const fullPath = path2.resolve(workspaceRoot, filePath);
|
|
948
|
+
const allIgnorePatterns = this.config.getIgnoredGlobs();
|
|
949
|
+
if (this.isGlobPattern(filePath)) {
|
|
950
|
+
this.eventEmitter.fileDiscoveryProgress(
|
|
951
|
+
`Discovering files matching glob pattern: ${filePath}`
|
|
952
|
+
);
|
|
953
|
+
return await this.discoverFilesFromGlob(filePath, allIgnorePatterns);
|
|
954
|
+
}
|
|
955
|
+
if (!fs2.existsSync(fullPath)) {
|
|
956
|
+
throw new Error(`Path not found: ${filePath}`);
|
|
957
|
+
}
|
|
958
|
+
const stats = fs2.statSync(fullPath);
|
|
959
|
+
if (stats.isFile()) {
|
|
960
|
+
this.eventEmitter.fileDiscoveryProgress(`Checking specific file: ${filePath}`);
|
|
961
|
+
const matchesIgnore = allIgnorePatterns.some(
|
|
962
|
+
(pattern) => this.matchesPattern(filePath, pattern)
|
|
963
|
+
);
|
|
964
|
+
if (matchesIgnore) {
|
|
965
|
+
this.eventEmitter.fileDiscoveryProgress(`File ${filePath} is ignored by patterns`);
|
|
966
|
+
return [];
|
|
967
|
+
} else {
|
|
968
|
+
return [filePath];
|
|
969
|
+
}
|
|
970
|
+
} else if (stats.isDirectory()) {
|
|
971
|
+
this.eventEmitter.fileDiscoveryProgress(`Discovering files in directory: ${filePath}`);
|
|
972
|
+
return await this.discoverFilesFromDirectory(filePath, allIgnorePatterns);
|
|
973
|
+
} else {
|
|
974
|
+
throw new Error(`Path is neither a file nor directory: ${filePath}`);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Check if a path contains glob pattern characters
|
|
979
|
+
*/
|
|
980
|
+
isGlobPattern(filePath) {
|
|
981
|
+
return /[*?[\]{}]/.test(filePath);
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Discover files matching a glob pattern
|
|
985
|
+
*/
|
|
986
|
+
async discoverFilesFromGlob(globPattern, ignorePatterns) {
|
|
987
|
+
const workspaceRoot = this.environment.getWorkspaceRoot();
|
|
988
|
+
const allIgnorePatterns = ["**/node_modules/**", "**/.git/**", ...ignorePatterns];
|
|
989
|
+
const matches = await glob(globPattern, {
|
|
990
|
+
cwd: workspaceRoot,
|
|
991
|
+
nodir: true,
|
|
992
|
+
absolute: false,
|
|
993
|
+
ignore: allIgnorePatterns
|
|
994
|
+
});
|
|
995
|
+
this.eventEmitter.fileDiscoveryProgress(`Found ${matches.length} files matching pattern`);
|
|
996
|
+
return matches;
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Discover all files in a directory
|
|
1000
|
+
*/
|
|
1001
|
+
async discoverFilesFromDirectory(dirPath, ignorePatterns) {
|
|
1002
|
+
const workspaceRoot = this.environment.getWorkspaceRoot();
|
|
1003
|
+
const globPattern = path2.join(dirPath, "**/*").replace(/\\/g, "/");
|
|
1004
|
+
const allIgnorePatterns = ["**/node_modules/**", "**/.git/**", ...ignorePatterns];
|
|
1005
|
+
const matches = await glob(globPattern, {
|
|
1006
|
+
cwd: workspaceRoot,
|
|
1007
|
+
nodir: true,
|
|
1008
|
+
absolute: false,
|
|
1009
|
+
ignore: allIgnorePatterns
|
|
1010
|
+
});
|
|
1011
|
+
this.eventEmitter.fileDiscoveryProgress(`Found ${matches.length} files in directory`);
|
|
1012
|
+
return matches;
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Discover all files from directories using glob patterns (scan mode)
|
|
1016
|
+
*/
|
|
1017
|
+
async discoverAllFiles(directories, ignorePatterns) {
|
|
1018
|
+
const allFiles = [];
|
|
1019
|
+
const workspaceRoot = this.environment.getWorkspaceRoot();
|
|
1020
|
+
const allIgnorePatterns = ["**/node_modules/**", "**/.git/**", ...ignorePatterns];
|
|
1021
|
+
for (const dir of directories) {
|
|
1022
|
+
const stats = fs2.statSync(dir);
|
|
1023
|
+
if (!stats.isDirectory()) {
|
|
1024
|
+
const shouldIgnore = ignorePatterns.some((pattern) => this.matchesPattern(dir, pattern));
|
|
1025
|
+
if (!shouldIgnore) {
|
|
1026
|
+
allFiles.push(path2.relative(workspaceRoot, dir));
|
|
1027
|
+
}
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
const matches = await glob("**/*", {
|
|
1031
|
+
cwd: dir,
|
|
1032
|
+
nodir: true,
|
|
1033
|
+
absolute: false,
|
|
1034
|
+
// Get relative paths from the directory
|
|
1035
|
+
ignore: allIgnorePatterns
|
|
1036
|
+
});
|
|
1037
|
+
const relativePaths = matches.map((match) => {
|
|
1038
|
+
const absolutePath = path2.resolve(dir, match);
|
|
1039
|
+
return path2.relative(workspaceRoot, absolutePath);
|
|
1040
|
+
});
|
|
1041
|
+
allFiles.push(...relativePaths);
|
|
1042
|
+
}
|
|
1043
|
+
return [...new Set(allFiles)];
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Pattern matching function that uses consistent options
|
|
1047
|
+
*/
|
|
1048
|
+
matchesPattern(filePath, pattern) {
|
|
1049
|
+
return minimatch(filePath, pattern, { dot: true });
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Check if a file should be processed
|
|
1053
|
+
*/
|
|
1054
|
+
isFileValid(options) {
|
|
1055
|
+
if (this.mode === "check") {
|
|
1056
|
+
return true;
|
|
1057
|
+
}
|
|
1058
|
+
if (!this.diffMode) {
|
|
1059
|
+
return true;
|
|
1060
|
+
}
|
|
1061
|
+
const { filePath } = options;
|
|
1062
|
+
return this.diffMode.changedFiles.some((changedFile) => {
|
|
1063
|
+
return filePath === changedFile || filePath.endsWith(changedFile) || changedFile.endsWith(filePath);
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* Check if a specific line range should be processed
|
|
1068
|
+
*/
|
|
1069
|
+
isLineRangeValid(options) {
|
|
1070
|
+
if (this.mode === "check") {
|
|
1071
|
+
return true;
|
|
1072
|
+
}
|
|
1073
|
+
if (!this.diffMode) {
|
|
1074
|
+
return true;
|
|
1075
|
+
}
|
|
1076
|
+
const { filePath, startLine, endLine } = options;
|
|
1077
|
+
const fileChange = this.diffMode.fileChangeMap.get(filePath);
|
|
1078
|
+
if (!fileChange) {
|
|
1079
|
+
return false;
|
|
1080
|
+
}
|
|
1081
|
+
if (fileChange.status === "added") {
|
|
1082
|
+
return true;
|
|
1083
|
+
}
|
|
1084
|
+
if (fileChange.status === "removed") {
|
|
1085
|
+
return false;
|
|
1086
|
+
}
|
|
1087
|
+
return this.isLineRangeInPatch({ patch: fileChange.patch, startLine, endLine });
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Check if a match should be processed
|
|
1091
|
+
*/
|
|
1092
|
+
isMatchValid(options) {
|
|
1093
|
+
if (this.mode === "check") {
|
|
1094
|
+
return true;
|
|
1095
|
+
}
|
|
1096
|
+
if (!this.diffMode) {
|
|
1097
|
+
return true;
|
|
1098
|
+
}
|
|
1099
|
+
const { match } = options;
|
|
1100
|
+
return this.isFileValid({ filePath: match.filePath }) && this.isLineRangeValid({
|
|
1101
|
+
filePath: match.filePath,
|
|
1102
|
+
startLine: match.range.start.line,
|
|
1103
|
+
endLine: match.range.end.line
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
// ===== Helper Methods =====
|
|
1107
|
+
/**
|
|
1108
|
+
* Parse a git patch to determine if a line range intersects with changed lines
|
|
1109
|
+
* Adapted from patchParser.ts logic
|
|
1110
|
+
*/
|
|
1111
|
+
isLineRangeInPatch(options) {
|
|
1112
|
+
const { patch, startLine, endLine } = options;
|
|
1113
|
+
const lines = patch.split("\n");
|
|
1114
|
+
let currentNewLine = 0;
|
|
1115
|
+
let inHunk = false;
|
|
1116
|
+
for (const line of lines) {
|
|
1117
|
+
const hunkMatch = line.match(/^@@\s+-\d+(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/);
|
|
1118
|
+
if (hunkMatch) {
|
|
1119
|
+
currentNewLine = parseInt(hunkMatch[1], 10);
|
|
1120
|
+
inHunk = true;
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
1123
|
+
if (!inHunk) continue;
|
|
1124
|
+
if (line.startsWith("+")) {
|
|
1125
|
+
if (currentNewLine >= startLine && currentNewLine <= endLine) {
|
|
1126
|
+
return true;
|
|
1127
|
+
}
|
|
1128
|
+
currentNewLine++;
|
|
1129
|
+
} else if (line.startsWith("-")) {
|
|
1130
|
+
continue;
|
|
1131
|
+
} else if (!line.startsWith("\\")) {
|
|
1132
|
+
if (currentNewLine >= startLine && currentNewLine <= endLine) {
|
|
1133
|
+
return true;
|
|
1134
|
+
}
|
|
1135
|
+
currentNewLine++;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
return false;
|
|
1139
|
+
}
|
|
1140
|
+
};
|
|
1141
|
+
|
|
1142
|
+
// src/steps/FileFilterStep.ts
|
|
1143
|
+
import * as fs3 from "fs";
|
|
1144
|
+
import * as path3 from "path";
|
|
1145
|
+
import { minimatch as minimatch2 } from "minimatch";
|
|
1146
|
+
var FileFilterStep = class {
|
|
1147
|
+
environment;
|
|
1148
|
+
constructor(environment) {
|
|
1149
|
+
this.environment = environment;
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* Centralized pattern matching function that uses consistent options
|
|
1153
|
+
* @param path - The path to test
|
|
1154
|
+
* @param pattern - The glob pattern to match against
|
|
1155
|
+
* @returns true if the path matches the pattern
|
|
1156
|
+
*/
|
|
1157
|
+
matchesPattern(path11, pattern) {
|
|
1158
|
+
return minimatch2(path11, pattern, { dot: true });
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Evaluate a single file filter condition for a given file path
|
|
1162
|
+
*/
|
|
1163
|
+
evaluateCondition(condition, filePath) {
|
|
1164
|
+
const workspaceRoot = this.environment.getWorkspaceRoot();
|
|
1165
|
+
const absoluteFilePath = path3.resolve(workspaceRoot, filePath);
|
|
1166
|
+
if ("fs.siblingExists" in condition) {
|
|
1167
|
+
const { filename } = condition["fs.siblingExists"];
|
|
1168
|
+
const dir = path3.dirname(absoluteFilePath);
|
|
1169
|
+
const siblingPath = path3.join(dir, filename);
|
|
1170
|
+
return fs3.existsSync(siblingPath);
|
|
1171
|
+
}
|
|
1172
|
+
if ("fs.pathMatches" in condition) {
|
|
1173
|
+
const { pattern } = condition["fs.pathMatches"];
|
|
1174
|
+
return this.matchesPattern(filePath, pattern);
|
|
1175
|
+
}
|
|
1176
|
+
if ("fs.ancestorHas" in condition) {
|
|
1177
|
+
const { filename } = condition["fs.ancestorHas"];
|
|
1178
|
+
let currentDir = path3.dirname(absoluteFilePath);
|
|
1179
|
+
const root = path3.parse(currentDir).root;
|
|
1180
|
+
while (currentDir !== root) {
|
|
1181
|
+
const targetPath = path3.join(currentDir, filename);
|
|
1182
|
+
if (fs3.existsSync(targetPath)) {
|
|
1183
|
+
return true;
|
|
1184
|
+
}
|
|
1185
|
+
const parentDir = path3.dirname(currentDir);
|
|
1186
|
+
if (parentDir === currentDir) break;
|
|
1187
|
+
currentDir = parentDir;
|
|
1188
|
+
}
|
|
1189
|
+
return false;
|
|
1190
|
+
}
|
|
1191
|
+
if ("fs.siblingAny" in condition) {
|
|
1192
|
+
const { pattern } = condition["fs.siblingAny"];
|
|
1193
|
+
const dir = path3.dirname(absoluteFilePath);
|
|
1194
|
+
if (!fs3.existsSync(dir)) {
|
|
1195
|
+
return false;
|
|
1196
|
+
}
|
|
1197
|
+
const siblings = fs3.readdirSync(dir);
|
|
1198
|
+
return siblings.some((sibling) => this.matchesPattern(sibling, pattern));
|
|
1199
|
+
}
|
|
1200
|
+
return false;
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Evaluate file filter conditions for a given file path
|
|
1204
|
+
*/
|
|
1205
|
+
evaluateConditions(conditions, filePath) {
|
|
1206
|
+
const results = [];
|
|
1207
|
+
if (conditions.all) {
|
|
1208
|
+
results.push(conditions.all.every((condition) => this.evaluateCondition(condition, filePath)));
|
|
1209
|
+
}
|
|
1210
|
+
if (conditions.any) {
|
|
1211
|
+
results.push(conditions.any.some((condition) => this.evaluateCondition(condition, filePath)));
|
|
1212
|
+
}
|
|
1213
|
+
if (conditions.not) {
|
|
1214
|
+
results.push(!conditions.not.some((condition) => this.evaluateCondition(condition, filePath)));
|
|
1215
|
+
}
|
|
1216
|
+
return results.length === 0 ? true : results.every((result) => result === true);
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Execute file filter on file paths
|
|
1220
|
+
*/
|
|
1221
|
+
async execute(filePaths, options) {
|
|
1222
|
+
var _a, _b;
|
|
1223
|
+
const startTime = Date.now();
|
|
1224
|
+
let filteredPaths = filePaths;
|
|
1225
|
+
if ((_a = options.include) == null ? void 0 : _a.length) {
|
|
1226
|
+
filteredPaths = filteredPaths.filter(
|
|
1227
|
+
(filePath) => options.include.some((pattern) => this.matchesPattern(filePath, pattern))
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
if ((_b = options.ignore) == null ? void 0 : _b.length) {
|
|
1231
|
+
filteredPaths = filteredPaths.filter((filePath) => {
|
|
1232
|
+
const matchesIgnore = options.ignore.some(
|
|
1233
|
+
(pattern) => this.matchesPattern(filePath, pattern)
|
|
1234
|
+
);
|
|
1235
|
+
return !matchesIgnore;
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
if (options.conditions) {
|
|
1239
|
+
filteredPaths = filteredPaths.filter(
|
|
1240
|
+
(filePath) => this.evaluateConditions(options.conditions, filePath)
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
const executionTime = Date.now() - startTime;
|
|
1244
|
+
return await Promise.resolve({
|
|
1245
|
+
filteredPaths,
|
|
1246
|
+
executionTime
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
};
|
|
1250
|
+
|
|
1251
|
+
// src/steps/FindMatchesStep.ts
|
|
1252
|
+
import path8 from "path";
|
|
1253
|
+
|
|
1254
|
+
// src/languages.ts
|
|
1255
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1256
|
+
import { createRequire } from "module";
|
|
1257
|
+
import path4 from "path";
|
|
1258
|
+
import angular from "@ast-grep/lang-angular";
|
|
1259
|
+
import bash from "@ast-grep/lang-bash";
|
|
1260
|
+
import c from "@ast-grep/lang-c";
|
|
1261
|
+
import cpp from "@ast-grep/lang-cpp";
|
|
1262
|
+
import csharp from "@ast-grep/lang-csharp";
|
|
1263
|
+
import css from "@ast-grep/lang-css";
|
|
1264
|
+
import dart from "@ast-grep/lang-dart";
|
|
1265
|
+
import elixir from "@ast-grep/lang-elixir";
|
|
1266
|
+
import go from "@ast-grep/lang-go";
|
|
1267
|
+
import haskell from "@ast-grep/lang-haskell";
|
|
1268
|
+
import html from "@ast-grep/lang-html";
|
|
1269
|
+
import java from "@ast-grep/lang-java";
|
|
1270
|
+
import javascript from "@ast-grep/lang-javascript";
|
|
1271
|
+
import json from "@ast-grep/lang-json";
|
|
1272
|
+
import kotlin from "@ast-grep/lang-kotlin";
|
|
1273
|
+
import lua from "@ast-grep/lang-lua";
|
|
1274
|
+
import markdown from "@ast-grep/lang-markdown";
|
|
1275
|
+
import php from "@ast-grep/lang-php";
|
|
1276
|
+
import python from "@ast-grep/lang-python";
|
|
1277
|
+
import ruby from "@ast-grep/lang-ruby";
|
|
1278
|
+
import rust from "@ast-grep/lang-rust";
|
|
1279
|
+
import scala from "@ast-grep/lang-scala";
|
|
1280
|
+
import sql from "@ast-grep/lang-sql";
|
|
1281
|
+
import swift from "@ast-grep/lang-swift";
|
|
1282
|
+
import toml from "@ast-grep/lang-toml";
|
|
1283
|
+
import tsx from "@ast-grep/lang-tsx";
|
|
1284
|
+
import typescript from "@ast-grep/lang-typescript";
|
|
1285
|
+
import yaml from "@ast-grep/lang-yaml";
|
|
1286
|
+
import { registerDynamicLanguage } from "@ast-grep/napi";
|
|
1287
|
+
console.debug = () => {
|
|
1288
|
+
};
|
|
1289
|
+
var require2 = createRequire(import.meta.url ? import.meta.url : __filename);
|
|
1290
|
+
function getGraphQLLibPath() {
|
|
1291
|
+
const graphqlDir = path4.dirname(require2.resolve("tree-sitter-graphql"));
|
|
1292
|
+
const releaseNode = path4.join(graphqlDir, "../../build/Release/tree_sitter_graphql_binding.node");
|
|
1293
|
+
if (existsSync3(releaseNode)) {
|
|
1294
|
+
return releaseNode;
|
|
1295
|
+
}
|
|
1296
|
+
const debugNode = path4.join(graphqlDir, "../../build/Debug/tree_sitter_graphql_binding.node");
|
|
1297
|
+
if (existsSync3(debugNode)) {
|
|
1298
|
+
return debugNode;
|
|
1299
|
+
}
|
|
1300
|
+
const soFile = path4.join(graphqlDir, "parser.so");
|
|
1301
|
+
if (existsSync3(soFile)) {
|
|
1302
|
+
return soFile;
|
|
1303
|
+
}
|
|
1304
|
+
return null;
|
|
1305
|
+
}
|
|
1306
|
+
var graphqlPath = getGraphQLLibPath();
|
|
1307
|
+
var graphql = {
|
|
1308
|
+
// node-gyp-build puts the .node file in build/Release/
|
|
1309
|
+
libraryPath: graphqlPath,
|
|
1310
|
+
/** the file extensions of the language. e.g. mojo */
|
|
1311
|
+
extensions: ["graphql"],
|
|
1312
|
+
/** the dylib symbol to load ts-language, default is `tree_sitter_{name}` */
|
|
1313
|
+
languageSymbol: "tree_sitter_graphql",
|
|
1314
|
+
/** the meta variable leading character, default is $ */
|
|
1315
|
+
metaVarChar: "$",
|
|
1316
|
+
/**
|
|
1317
|
+
* An optional char to replace $ in your pattern.
|
|
1318
|
+
* See https://ast-grep.github.io/advanced/custom-language.html#register-language-in-sgconfig-yml
|
|
1319
|
+
*/
|
|
1320
|
+
expandoChar: void 0
|
|
1321
|
+
};
|
|
1322
|
+
var registeredLanguages = {
|
|
1323
|
+
["Angular" /* Angular */]: angular,
|
|
1324
|
+
["Bash" /* Bash */]: bash,
|
|
1325
|
+
["C" /* C */]: c,
|
|
1326
|
+
["Cpp" /* Cpp */]: cpp,
|
|
1327
|
+
["Csharp" /* Csharp */]: csharp,
|
|
1328
|
+
["Css" /* Css */]: css,
|
|
1329
|
+
["Dart" /* Dart */]: dart,
|
|
1330
|
+
["Elixir" /* Elixir */]: elixir,
|
|
1331
|
+
["Go" /* Go */]: go,
|
|
1332
|
+
["Haskell" /* Haskell */]: haskell,
|
|
1333
|
+
["Html" /* Html */]: html,
|
|
1334
|
+
["Java" /* Java */]: java,
|
|
1335
|
+
["JavaScript" /* JavaScript */]: javascript,
|
|
1336
|
+
["Json" /* Json */]: json,
|
|
1337
|
+
["Kotlin" /* Kotlin */]: kotlin,
|
|
1338
|
+
["Lua" /* Lua */]: lua,
|
|
1339
|
+
["Markdown" /* Markdown */]: markdown,
|
|
1340
|
+
["Php" /* Php */]: php,
|
|
1341
|
+
["Python" /* Python */]: python,
|
|
1342
|
+
["Ruby" /* Ruby */]: ruby,
|
|
1343
|
+
["Rust" /* Rust */]: rust,
|
|
1344
|
+
["Scala" /* Scala */]: scala,
|
|
1345
|
+
["Sql" /* Sql */]: sql,
|
|
1346
|
+
["Swift" /* Swift */]: swift,
|
|
1347
|
+
["Toml" /* Toml */]: toml,
|
|
1348
|
+
["Tsx" /* Tsx */]: tsx,
|
|
1349
|
+
["TypeScript" /* TypeScript */]: typescript,
|
|
1350
|
+
["Yaml" /* Yaml */]: yaml,
|
|
1351
|
+
...graphqlPath ? { ["GraphQL" /* GraphQL */]: graphql } : {}
|
|
1352
|
+
};
|
|
1353
|
+
registerDynamicLanguage(registeredLanguages);
|
|
1354
|
+
var REGISTERED_LANGUAGE_EXTENSIONS = Object.entries(
|
|
1355
|
+
registeredLanguages
|
|
1356
|
+
).reduce(
|
|
1357
|
+
(acc, [language, registration]) => {
|
|
1358
|
+
acc[language] = registration.extensions ?? [];
|
|
1359
|
+
return acc;
|
|
1360
|
+
},
|
|
1361
|
+
{}
|
|
1362
|
+
);
|
|
1363
|
+
function getLanguageFromFilePath(filePath) {
|
|
1364
|
+
const extension = path4.extname(filePath).slice(1);
|
|
1365
|
+
const language = findRegisteredLanguageFromExtension(extension);
|
|
1366
|
+
return language ?? "Unknown" /* Unknown */;
|
|
1367
|
+
}
|
|
1368
|
+
function findRegisteredLanguageFromExtension(extension) {
|
|
1369
|
+
return Object.keys(REGISTERED_LANGUAGE_EXTENSIONS).find(
|
|
1370
|
+
(language) => REGISTERED_LANGUAGE_EXTENSIONS[language].includes(extension)
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// src/providers/AstGrepAstProvider.ts
|
|
1375
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1376
|
+
import path5 from "path";
|
|
1377
|
+
import { parse as parse2, parseAsync } from "@ast-grep/napi";
|
|
1378
|
+
var AstGrepAstProvider = class {
|
|
1379
|
+
environment;
|
|
1380
|
+
language;
|
|
1381
|
+
constructor(environment, language) {
|
|
1382
|
+
this.environment = environment;
|
|
1383
|
+
this.language = language;
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Find all matches based on ast-grep pattern
|
|
1387
|
+
* @param filePaths File paths to search in (relative to workspace root)
|
|
1388
|
+
* @param schema The ast-grep schema (rule, constraints, etc.)
|
|
1389
|
+
* @returns Array of matches found
|
|
1390
|
+
*/
|
|
1391
|
+
async findMatches(filePaths, schema) {
|
|
1392
|
+
if (filePaths.length === 0) {
|
|
1393
|
+
return [];
|
|
1394
|
+
}
|
|
1395
|
+
const { rule, constraints } = schema;
|
|
1396
|
+
const batchSize = this.getBatchSize();
|
|
1397
|
+
const matches = [];
|
|
1398
|
+
for (let i = 0; i < filePaths.length; i += batchSize) {
|
|
1399
|
+
const batch = filePaths.slice(i, i + batchSize);
|
|
1400
|
+
const batchMatches = await this.processBatch(batch, rule, constraints);
|
|
1401
|
+
matches.push(...batchMatches);
|
|
1402
|
+
}
|
|
1403
|
+
return matches;
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Get optimal batch size based on available CPU cores and thread pool size
|
|
1407
|
+
*/
|
|
1408
|
+
getBatchSize() {
|
|
1409
|
+
const threadPoolSize = parseInt(process.env.UV_THREADPOOL_SIZE || "4", 10);
|
|
1410
|
+
return Math.max(2, Math.floor(threadPoolSize / 2));
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Process a batch of files with memory-efficient approach
|
|
1414
|
+
*/
|
|
1415
|
+
async processBatch(filePaths, rule, constraints) {
|
|
1416
|
+
const parsePromises = filePaths.map(async (filePath) => {
|
|
1417
|
+
const content = await readFile2(
|
|
1418
|
+
path5.resolve(this.environment.getWorkspaceRoot(), filePath),
|
|
1419
|
+
"utf-8"
|
|
1420
|
+
);
|
|
1421
|
+
const node = await parseAsync(this.language, content);
|
|
1422
|
+
const foundNodes = node.root().findAll({
|
|
1423
|
+
rule,
|
|
1424
|
+
language: this.language,
|
|
1425
|
+
constraints
|
|
1426
|
+
});
|
|
1427
|
+
return foundNodes.map((sgNode) => ({ filePath, sgNode }));
|
|
1428
|
+
});
|
|
1429
|
+
const batchResults = await Promise.all(parsePromises);
|
|
1430
|
+
const sgNodes = batchResults.flat();
|
|
1431
|
+
const matches = [];
|
|
1432
|
+
for (const { filePath, sgNode } of sgNodes) {
|
|
1433
|
+
const range = sgNode.range();
|
|
1434
|
+
matches.push({
|
|
1435
|
+
filePath,
|
|
1436
|
+
text: sgNode.text(),
|
|
1437
|
+
range: {
|
|
1438
|
+
start: {
|
|
1439
|
+
line: range.start.line,
|
|
1440
|
+
column: range.start.column
|
|
1441
|
+
},
|
|
1442
|
+
end: {
|
|
1443
|
+
line: range.end.line,
|
|
1444
|
+
column: range.end.column
|
|
1445
|
+
}
|
|
1446
|
+
},
|
|
1447
|
+
// Try to extract symbol if possible
|
|
1448
|
+
symbol: this.extractSymbol(sgNode),
|
|
1449
|
+
language: this.language
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
return matches;
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* Extract symbol name from ast-grep node if possible
|
|
1456
|
+
*/
|
|
1457
|
+
extractSymbol(node) {
|
|
1458
|
+
const kind = node.kind();
|
|
1459
|
+
if (kind === "call_expression") {
|
|
1460
|
+
const functionNode = node.field("function");
|
|
1461
|
+
if (functionNode) {
|
|
1462
|
+
return this.extractSymbol(functionNode);
|
|
1463
|
+
}
|
|
1464
|
+
} else if (kind === "member_expression") {
|
|
1465
|
+
const propertyNode = node.field("property");
|
|
1466
|
+
if (propertyNode) {
|
|
1467
|
+
return propertyNode.text();
|
|
1468
|
+
}
|
|
1469
|
+
} else if (kind === "identifier") {
|
|
1470
|
+
return node.text();
|
|
1471
|
+
}
|
|
1472
|
+
return void 0;
|
|
1473
|
+
}
|
|
1474
|
+
/**
|
|
1475
|
+
* Expand a match to its enclosing function, method, or class definition
|
|
1476
|
+
* @param match The match to expand (typically a small range like a method name)
|
|
1477
|
+
* @returns Expanded match with the full function/method/class body, or null if not found
|
|
1478
|
+
*/
|
|
1479
|
+
async expandMatch(match) {
|
|
1480
|
+
const targetNodeKinds = /* @__PURE__ */ new Set([
|
|
1481
|
+
"method_definition",
|
|
1482
|
+
"function_declaration",
|
|
1483
|
+
"function_expression",
|
|
1484
|
+
"arrow_function",
|
|
1485
|
+
"class_declaration",
|
|
1486
|
+
"export_statement",
|
|
1487
|
+
"lexical_declaration"
|
|
1488
|
+
// For const/let function expressions
|
|
1489
|
+
]);
|
|
1490
|
+
const absolutePath = path5.resolve(this.environment.getWorkspaceRoot(), match.filePath);
|
|
1491
|
+
const content = await readFile2(absolutePath, "utf-8");
|
|
1492
|
+
const root = parse2(this.language, content).root();
|
|
1493
|
+
const nodeAtPosition = this.findNodeAtPosition(
|
|
1494
|
+
root,
|
|
1495
|
+
match.range.start.line,
|
|
1496
|
+
match.range.start.column
|
|
1497
|
+
);
|
|
1498
|
+
if (!nodeAtPosition) {
|
|
1499
|
+
return null;
|
|
1500
|
+
}
|
|
1501
|
+
let current = nodeAtPosition;
|
|
1502
|
+
while (current) {
|
|
1503
|
+
const kindValue = current.kind();
|
|
1504
|
+
const kindStr = typeof kindValue === "string" ? kindValue : String(kindValue);
|
|
1505
|
+
if (targetNodeKinds.has(kindStr)) {
|
|
1506
|
+
const range = current.range();
|
|
1507
|
+
return {
|
|
1508
|
+
filePath: match.filePath,
|
|
1509
|
+
text: current.text(),
|
|
1510
|
+
range: {
|
|
1511
|
+
start: { line: range.start.line, column: range.start.column },
|
|
1512
|
+
end: { line: range.end.line, column: range.end.column }
|
|
1513
|
+
},
|
|
1514
|
+
symbol: match.symbol,
|
|
1515
|
+
language: this.language
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
current = current.parent();
|
|
1519
|
+
}
|
|
1520
|
+
return null;
|
|
1521
|
+
}
|
|
1522
|
+
/**
|
|
1523
|
+
* Find the deepest node at a given position
|
|
1524
|
+
*/
|
|
1525
|
+
findNodeAtPosition(node, line, column) {
|
|
1526
|
+
const range = node.range();
|
|
1527
|
+
const isAfterStart = line > range.start.line || line === range.start.line && column >= range.start.column;
|
|
1528
|
+
const isBeforeEnd = line < range.end.line || line === range.end.line && column <= range.end.column;
|
|
1529
|
+
if (!isAfterStart || !isBeforeEnd) {
|
|
1530
|
+
return null;
|
|
1531
|
+
}
|
|
1532
|
+
const children = node.children();
|
|
1533
|
+
for (const child of children) {
|
|
1534
|
+
const deeperNode = this.findNodeAtPosition(child, line, column);
|
|
1535
|
+
if (deeperNode) {
|
|
1536
|
+
return deeperNode;
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
return node;
|
|
1540
|
+
}
|
|
1541
|
+
};
|
|
1542
|
+
|
|
1543
|
+
// src/providers/ScipIntelligenceProvider.ts
|
|
1544
|
+
import { spawn } from "child_process";
|
|
1545
|
+
import fs4 from "fs/promises";
|
|
1546
|
+
import { createRequire as createRequire2 } from "module";
|
|
1547
|
+
import os2 from "os";
|
|
1548
|
+
import path7 from "path";
|
|
1549
|
+
|
|
1550
|
+
// src/utils/readTextAtRange.ts
|
|
1551
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
1552
|
+
import path6 from "path";
|
|
1553
|
+
async function readTextAtRange(workspaceRoot, filePath, range) {
|
|
1554
|
+
const absolutePath = path6.resolve(workspaceRoot, filePath);
|
|
1555
|
+
const content = await readFile3(absolutePath, "utf-8");
|
|
1556
|
+
const lines = content.split("\n");
|
|
1557
|
+
const { start, end } = range;
|
|
1558
|
+
if (start.line === end.line) {
|
|
1559
|
+
const line = lines[start.line] || "";
|
|
1560
|
+
return line.substring(start.column, end.column);
|
|
1561
|
+
}
|
|
1562
|
+
const result = [];
|
|
1563
|
+
for (let i = start.line; i <= end.line && i < lines.length; i++) {
|
|
1564
|
+
const line = lines[i];
|
|
1565
|
+
if (i === start.line) {
|
|
1566
|
+
result.push(line.substring(start.column));
|
|
1567
|
+
} else if (i === end.line) {
|
|
1568
|
+
result.push(line.substring(0, end.column));
|
|
1569
|
+
} else {
|
|
1570
|
+
result.push(line);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
return result.join("\n");
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// src/providers/ScipIntelligenceProvider.ts
|
|
1577
|
+
var require3 = createRequire2(import.meta.url ? import.meta.url : __filename);
|
|
1578
|
+
var { scip } = require3("@sourcegraph/scip-root/bindings/typescript/scip.js");
|
|
1579
|
+
var PACKAGE_REGEX = /^\+ (.+?) \((\d+)ms\)$/;
|
|
1580
|
+
function processScipProgressData(data, buffer, eventEmitter, language, rootPath) {
|
|
1581
|
+
const text = data.toString();
|
|
1582
|
+
buffer += text;
|
|
1583
|
+
const lines = buffer.split("\n");
|
|
1584
|
+
const newBuffer = lines.pop() || "";
|
|
1585
|
+
let latestPackageLine = null;
|
|
1586
|
+
for (const line of lines) {
|
|
1587
|
+
const trimmedLine = line.trim();
|
|
1588
|
+
const packageMatch = trimmedLine.match(PACKAGE_REGEX);
|
|
1589
|
+
if (packageMatch) {
|
|
1590
|
+
latestPackageLine = trimmedLine;
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
for (const line of lines) {
|
|
1594
|
+
const trimmedLine = line.trim();
|
|
1595
|
+
if (!trimmedLine) continue;
|
|
1596
|
+
const packageMatch = trimmedLine.match(PACKAGE_REGEX);
|
|
1597
|
+
if (packageMatch) {
|
|
1598
|
+
if (trimmedLine === latestPackageLine) {
|
|
1599
|
+
const [, packagePath, timeMs] = packageMatch;
|
|
1600
|
+
const relativePackagePath = path7.relative(rootPath, packagePath);
|
|
1601
|
+
eventEmitter.indexingProgress(
|
|
1602
|
+
language,
|
|
1603
|
+
`Indexed ${relativePackagePath}`,
|
|
1604
|
+
relativePackagePath,
|
|
1605
|
+
parseInt(timeMs, 10)
|
|
1606
|
+
);
|
|
1607
|
+
}
|
|
1608
|
+
} else {
|
|
1609
|
+
eventEmitter.indexingProgress(language, trimmedLine);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
return newBuffer;
|
|
1613
|
+
}
|
|
1614
|
+
var ScipIntelligenceProvider = class {
|
|
1615
|
+
environment;
|
|
1616
|
+
storage;
|
|
1617
|
+
language;
|
|
1618
|
+
scipIndex = null;
|
|
1619
|
+
indexingPromise = null;
|
|
1620
|
+
eventEmitter;
|
|
1621
|
+
constructor(environment, language, eventEmitter) {
|
|
1622
|
+
this.environment = environment;
|
|
1623
|
+
this.storage = new Storage(environment);
|
|
1624
|
+
this.language = language;
|
|
1625
|
+
this.eventEmitter = eventEmitter || new ExecutionEventEmitter();
|
|
1626
|
+
}
|
|
1627
|
+
/**
|
|
1628
|
+
* Get the SCIP CLI command for the given language
|
|
1629
|
+
*/
|
|
1630
|
+
getScipCommand() {
|
|
1631
|
+
switch (this.language) {
|
|
1632
|
+
case "TypeScript" /* TypeScript */:
|
|
1633
|
+
case "JavaScript" /* JavaScript */:
|
|
1634
|
+
case "Tsx" /* Tsx */:
|
|
1635
|
+
return "scip-typescript";
|
|
1636
|
+
case "Python" /* Python */:
|
|
1637
|
+
return "scip-python";
|
|
1638
|
+
default:
|
|
1639
|
+
throw new Error(`SCIP is not supported for language: ${this.language}`);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
/**
|
|
1643
|
+
* Ensure the index is ready, starting indexing if needed
|
|
1644
|
+
*/
|
|
1645
|
+
async ensureIndexReady() {
|
|
1646
|
+
if (this.scipIndex !== null) {
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
if (this.indexingPromise !== null) {
|
|
1650
|
+
return this.indexingPromise;
|
|
1651
|
+
}
|
|
1652
|
+
this.indexingPromise = this.startIndexing();
|
|
1653
|
+
await this.indexingPromise;
|
|
1654
|
+
}
|
|
1655
|
+
/**
|
|
1656
|
+
* Start the indexing process
|
|
1657
|
+
*/
|
|
1658
|
+
async startIndexing() {
|
|
1659
|
+
if (await this.storage.indexExists(this.language, `index.scip`)) {
|
|
1660
|
+
await this.loadIndex();
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
const finalIndexPath = await this.storage.getIndexFilePath(this.language, `index.scip`);
|
|
1664
|
+
const tempIndexPath = path7.join(os2.tmpdir(), `scip-index-${this.language}-${Date.now()}.tmp`);
|
|
1665
|
+
this.eventEmitter.startIndexing(this.language);
|
|
1666
|
+
const scipCommand = this.getScipCommand();
|
|
1667
|
+
const child = spawn(scipCommand, ["index", "--output", tempIndexPath], {
|
|
1668
|
+
cwd: this.environment.getWorkspaceRoot(),
|
|
1669
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1670
|
+
env: process.env
|
|
1671
|
+
});
|
|
1672
|
+
let progressBuffer = "";
|
|
1673
|
+
child.stdout.on("data", (data) => {
|
|
1674
|
+
progressBuffer = processScipProgressData(
|
|
1675
|
+
data,
|
|
1676
|
+
progressBuffer,
|
|
1677
|
+
this.eventEmitter,
|
|
1678
|
+
this.language,
|
|
1679
|
+
this.environment.getWorkspaceRoot()
|
|
1680
|
+
);
|
|
1681
|
+
});
|
|
1682
|
+
let stderr = "";
|
|
1683
|
+
child.stderr.on("data", (data) => {
|
|
1684
|
+
stderr += data.toString();
|
|
1685
|
+
process.stderr.write(data);
|
|
1686
|
+
});
|
|
1687
|
+
await new Promise((resolve3, reject) => {
|
|
1688
|
+
child.on("close", async (code) => {
|
|
1689
|
+
if (code === 0) {
|
|
1690
|
+
await fs4.rename(tempIndexPath, finalIndexPath);
|
|
1691
|
+
resolve3();
|
|
1692
|
+
} else {
|
|
1693
|
+
await fs4.unlink(tempIndexPath).catch(() => {
|
|
1694
|
+
});
|
|
1695
|
+
reject(new Error(`${scipCommand} index exited with code ${code}, stderr: ${stderr}`));
|
|
1696
|
+
}
|
|
1697
|
+
});
|
|
1698
|
+
child.on("error", async (err) => {
|
|
1699
|
+
await fs4.unlink(tempIndexPath).catch(() => {
|
|
1700
|
+
});
|
|
1701
|
+
reject(err);
|
|
1702
|
+
});
|
|
1703
|
+
});
|
|
1704
|
+
if (stderr) {
|
|
1705
|
+
console.error("SCIP indexing stderr:", stderr);
|
|
1706
|
+
}
|
|
1707
|
+
this.eventEmitter.completeIndexing(this.language);
|
|
1708
|
+
await this.loadIndex();
|
|
1709
|
+
}
|
|
1710
|
+
/**
|
|
1711
|
+
* Find all definitions for a given match
|
|
1712
|
+
* Automatically filters out external symbols
|
|
1713
|
+
*/
|
|
1714
|
+
async findDefinitions(match) {
|
|
1715
|
+
await this.ensureIndexReady();
|
|
1716
|
+
if (!this.scipIndex) throw new Error("SCIP index not ready");
|
|
1717
|
+
this.eventEmitter.startScipMatchLookup(this.language, match);
|
|
1718
|
+
const definitions = [];
|
|
1719
|
+
const documents = this.scipIndex.documents;
|
|
1720
|
+
for (const document of documents) {
|
|
1721
|
+
if (document.relative_path !== match.filePath) continue;
|
|
1722
|
+
this.eventEmitter.scipMatchLookupProgress(this.language, document);
|
|
1723
|
+
const scipOcc = this.findBestOverlappingOccurrence(document.occurrences, match);
|
|
1724
|
+
if (scipOcc) {
|
|
1725
|
+
if (this.isExternalSymbol(scipOcc.symbol)) {
|
|
1726
|
+
continue;
|
|
1727
|
+
}
|
|
1728
|
+
this.eventEmitter.scipMatchLookupProgress(this.language, scipOcc);
|
|
1729
|
+
const def = await this.findDefinitionForSymbol(scipOcc.symbol, documents);
|
|
1730
|
+
if (def) {
|
|
1731
|
+
definitions.push(def);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
return definitions;
|
|
1736
|
+
}
|
|
1737
|
+
/**
|
|
1738
|
+
* Find all references for a given match
|
|
1739
|
+
* Automatically filters out external symbols
|
|
1740
|
+
*/
|
|
1741
|
+
async findReferences(match) {
|
|
1742
|
+
await this.ensureIndexReady();
|
|
1743
|
+
if (!this.scipIndex) {
|
|
1744
|
+
return [];
|
|
1745
|
+
}
|
|
1746
|
+
const references = [];
|
|
1747
|
+
const documents = this.scipIndex.documents;
|
|
1748
|
+
for (const document of documents) {
|
|
1749
|
+
if (document.relative_path !== match.filePath) continue;
|
|
1750
|
+
const scipOcc = this.findBestOverlappingOccurrence(document.occurrences, match);
|
|
1751
|
+
if (scipOcc) {
|
|
1752
|
+
if (this.isExternalSymbol(scipOcc.symbol)) {
|
|
1753
|
+
break;
|
|
1754
|
+
}
|
|
1755
|
+
const refs = await this.findReferencesForSymbol(scipOcc.symbol, documents);
|
|
1756
|
+
references.push(...refs);
|
|
1757
|
+
break;
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
return references;
|
|
1761
|
+
}
|
|
1762
|
+
/**
|
|
1763
|
+
* Check if a symbol is from an external library (internal use)
|
|
1764
|
+
*/
|
|
1765
|
+
isExternalSymbol(symbol) {
|
|
1766
|
+
return symbol.includes("node_modules") || symbol.startsWith("npm/");
|
|
1767
|
+
}
|
|
1768
|
+
/**
|
|
1769
|
+
* Find the best SCIP occurrence that overlaps with the given match
|
|
1770
|
+
* Uses heuristics to pick the most relevant occurrence when multiple overlap
|
|
1771
|
+
*/
|
|
1772
|
+
findBestOverlappingOccurrence(occurrences, match) {
|
|
1773
|
+
const candidates = [];
|
|
1774
|
+
for (const scipOcc of occurrences) {
|
|
1775
|
+
const range = scipOcc.range;
|
|
1776
|
+
if (!range || range.length < 3) continue;
|
|
1777
|
+
if (!this.rangesOverlap(range, match)) continue;
|
|
1778
|
+
let score = 0;
|
|
1779
|
+
if (!this.rangeFullyInside(range, match)) continue;
|
|
1780
|
+
const startCol = range.length === 3 ? range[1] : range[1];
|
|
1781
|
+
score += startCol * 1e3;
|
|
1782
|
+
if (scipOcc.symbol_roles !== scip.SymbolRole.Definition) {
|
|
1783
|
+
score += 100;
|
|
1784
|
+
}
|
|
1785
|
+
if (match.symbol) {
|
|
1786
|
+
const { className, methodName } = this.extractSymbolNames(scipOcc.symbol);
|
|
1787
|
+
if (methodName === match.symbol || className === match.symbol) {
|
|
1788
|
+
score += 1e4;
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
candidates.push({ occurrence: scipOcc, score });
|
|
1792
|
+
}
|
|
1793
|
+
if (candidates.length === 0) return null;
|
|
1794
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
1795
|
+
return candidates[0].occurrence;
|
|
1796
|
+
}
|
|
1797
|
+
/**
|
|
1798
|
+
* Check if two ranges overlap
|
|
1799
|
+
*/
|
|
1800
|
+
rangesOverlap(scipRange, match) {
|
|
1801
|
+
if (scipRange.length === 3) {
|
|
1802
|
+
const [line, startCol, endCol] = scipRange;
|
|
1803
|
+
if (match.range.start.line === match.range.end.line) {
|
|
1804
|
+
return line === match.range.start.line && !(endCol <= match.range.start.column || startCol >= match.range.end.column);
|
|
1805
|
+
} else {
|
|
1806
|
+
if (line < match.range.start.line || line > match.range.end.line) return false;
|
|
1807
|
+
if (line === match.range.start.line && endCol <= match.range.start.column) return false;
|
|
1808
|
+
if (line === match.range.end.line && startCol >= match.range.end.column) return false;
|
|
1809
|
+
return true;
|
|
1810
|
+
}
|
|
1811
|
+
} else if (scipRange.length === 4) {
|
|
1812
|
+
const [startLine, _startCol, endLine, _endCol] = scipRange;
|
|
1813
|
+
if (endLine < match.range.start.line || startLine > match.range.end.line) return false;
|
|
1814
|
+
return true;
|
|
1815
|
+
}
|
|
1816
|
+
return false;
|
|
1817
|
+
}
|
|
1818
|
+
/**
|
|
1819
|
+
* Check if SCIP range is fully inside the match span
|
|
1820
|
+
*/
|
|
1821
|
+
rangeFullyInside(scipRange, match) {
|
|
1822
|
+
if (scipRange.length === 3) {
|
|
1823
|
+
const [line, startCol, endCol] = scipRange;
|
|
1824
|
+
if (line < match.range.start.line || line > match.range.end.line) return false;
|
|
1825
|
+
if (line === match.range.start.line && startCol < match.range.start.column) return false;
|
|
1826
|
+
if (line === match.range.end.line && endCol > match.range.end.column) return false;
|
|
1827
|
+
return true;
|
|
1828
|
+
} else if (scipRange.length === 4) {
|
|
1829
|
+
const [startLine, startCol, endLine, endCol] = scipRange;
|
|
1830
|
+
if (startLine < match.range.start.line) return false;
|
|
1831
|
+
if (endLine > match.range.end.line) return false;
|
|
1832
|
+
if (startLine === match.range.start.line && startCol < match.range.start.column) return false;
|
|
1833
|
+
if (endLine === match.range.end.line && endCol > match.range.end.column) return false;
|
|
1834
|
+
return true;
|
|
1835
|
+
}
|
|
1836
|
+
return false;
|
|
1837
|
+
}
|
|
1838
|
+
/**
|
|
1839
|
+
* Extract class and method names from SCIP symbol
|
|
1840
|
+
* e.g., "scip-typescript npm @wispbit/server 1.0.0 src/services/`OrganizationService`#updateViolationCounts()."
|
|
1841
|
+
* returns { className: "OrganizationService", methodName: "updateViolationCounts" }
|
|
1842
|
+
*
|
|
1843
|
+
* For standalone functions without a class:
|
|
1844
|
+
* e.g., "scip-typescript npm @wispbit/server 1.0.0 src/utils/helper()."
|
|
1845
|
+
* returns { className: null, methodName: "helper" }
|
|
1846
|
+
*/
|
|
1847
|
+
extractSymbolNames(symbol) {
|
|
1848
|
+
const classMethodMatch = symbol.match(/`([^`]+)`#([^#./`(]+)(?:\(\))?[.`]*$/);
|
|
1849
|
+
if (classMethodMatch) {
|
|
1850
|
+
return {
|
|
1851
|
+
className: classMethodMatch[1],
|
|
1852
|
+
methodName: classMethodMatch[2]
|
|
1853
|
+
};
|
|
1854
|
+
}
|
|
1855
|
+
const methodMatch = symbol.match(/([^#./`(]+)(?:\(\))?[.`]*$/);
|
|
1856
|
+
if (methodMatch) {
|
|
1857
|
+
return {
|
|
1858
|
+
className: null,
|
|
1859
|
+
methodName: methodMatch[1]
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
return { className: null, methodName: null };
|
|
1863
|
+
}
|
|
1864
|
+
/**
|
|
1865
|
+
* Find definition for a symbol across all documents
|
|
1866
|
+
*/
|
|
1867
|
+
async findDefinitionForSymbol(symbol, documents) {
|
|
1868
|
+
for (const document of documents) {
|
|
1869
|
+
for (const occ of document.occurrences) {
|
|
1870
|
+
if (occ.symbol === symbol && occ.symbol_roles === scip.SymbolRole.Definition) {
|
|
1871
|
+
const range = occ.range ?? [];
|
|
1872
|
+
const startLine = range[0] ?? 0;
|
|
1873
|
+
const startColumn = range[1] ?? 0;
|
|
1874
|
+
const endLine = range.length === 4 ? range[2] : startLine;
|
|
1875
|
+
const endColumn = range.length === 4 ? range[3] : range[2] ?? startColumn;
|
|
1876
|
+
const text = await readTextAtRange(
|
|
1877
|
+
this.environment.getWorkspaceRoot(),
|
|
1878
|
+
document.relative_path,
|
|
1879
|
+
{
|
|
1880
|
+
start: { line: startLine, column: startColumn },
|
|
1881
|
+
end: { line: endLine, column: endColumn }
|
|
1882
|
+
}
|
|
1883
|
+
);
|
|
1884
|
+
return {
|
|
1885
|
+
language: this.language,
|
|
1886
|
+
filePath: document.relative_path,
|
|
1887
|
+
range: {
|
|
1888
|
+
start: { line: startLine, column: startColumn },
|
|
1889
|
+
end: { line: endLine, column: endColumn }
|
|
1890
|
+
},
|
|
1891
|
+
symbol,
|
|
1892
|
+
text
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
return null;
|
|
1898
|
+
}
|
|
1899
|
+
/**
|
|
1900
|
+
* Find all references for a symbol across all documents
|
|
1901
|
+
*/
|
|
1902
|
+
async findReferencesForSymbol(symbol, documents) {
|
|
1903
|
+
const references = [];
|
|
1904
|
+
for (const document of documents) {
|
|
1905
|
+
for (const occ of document.occurrences) {
|
|
1906
|
+
if (occ.symbol === symbol && occ.symbol_roles !== scip.SymbolRole.Definition) {
|
|
1907
|
+
const range = occ.range ?? [];
|
|
1908
|
+
const startLine = range[0] ?? 0;
|
|
1909
|
+
const startColumn = range[1] ?? 0;
|
|
1910
|
+
const endLine = range.length === 4 ? range[2] : startLine;
|
|
1911
|
+
const endColumn = range.length === 4 ? range[3] : range[2] ?? startColumn;
|
|
1912
|
+
const text = await readTextAtRange(
|
|
1913
|
+
this.environment.getWorkspaceRoot(),
|
|
1914
|
+
document.relative_path,
|
|
1915
|
+
{
|
|
1916
|
+
start: { line: startLine, column: startColumn },
|
|
1917
|
+
end: { line: endLine, column: endColumn }
|
|
1918
|
+
}
|
|
1919
|
+
);
|
|
1920
|
+
references.push({
|
|
1921
|
+
language: this.language,
|
|
1922
|
+
filePath: document.relative_path,
|
|
1923
|
+
range: {
|
|
1924
|
+
start: { line: startLine, column: startColumn },
|
|
1925
|
+
end: { line: endLine, column: endColumn }
|
|
1926
|
+
},
|
|
1927
|
+
symbol,
|
|
1928
|
+
text
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
return references;
|
|
1934
|
+
}
|
|
1935
|
+
/**
|
|
1936
|
+
* Load SCIP index from the configured path
|
|
1937
|
+
*/
|
|
1938
|
+
async loadIndex() {
|
|
1939
|
+
const buffer = await this.storage.readIndex(this.language, `index.scip`);
|
|
1940
|
+
if (!buffer) {
|
|
1941
|
+
throw new Error(`Index not found for language: ${this.language}`);
|
|
1942
|
+
}
|
|
1943
|
+
this.scipIndex = this.parseIndex(new Uint8Array(buffer));
|
|
1944
|
+
}
|
|
1945
|
+
/**
|
|
1946
|
+
* Parse a SCIP index from bytes
|
|
1947
|
+
*/
|
|
1948
|
+
parseIndex(buffer) {
|
|
1949
|
+
return scip.Index.deserialize(buffer);
|
|
1950
|
+
}
|
|
1951
|
+
};
|
|
1952
|
+
|
|
1953
|
+
// src/providers/LanguageBackend.ts
|
|
1954
|
+
var LanguageBackendNotSupportedError = class extends Error {
|
|
1955
|
+
constructor(methodName, reason) {
|
|
1956
|
+
super(`${methodName} is not supported: ${reason}`);
|
|
1957
|
+
this.name = "LanguageBackendNotSupportedError";
|
|
1958
|
+
}
|
|
1959
|
+
};
|
|
1960
|
+
var LanguageBackend = class {
|
|
1961
|
+
astProvider;
|
|
1962
|
+
intelligenceProvider;
|
|
1963
|
+
language;
|
|
1964
|
+
constructor(environment, language, eventEmitter) {
|
|
1965
|
+
this.language = language;
|
|
1966
|
+
this.astProvider = new AstGrepAstProvider(environment, language);
|
|
1967
|
+
this.intelligenceProvider = new ScipIntelligenceProvider(environment, language, eventEmitter);
|
|
1968
|
+
}
|
|
1969
|
+
/**
|
|
1970
|
+
* Find all matches based on the AST provider
|
|
1971
|
+
*/
|
|
1972
|
+
async findMatches(filePaths, schema) {
|
|
1973
|
+
if (!this.astProvider) {
|
|
1974
|
+
throw new LanguageBackendNotSupportedError(
|
|
1975
|
+
"findMatches",
|
|
1976
|
+
"no AST provider configured for this language"
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
return await this.astProvider.findMatches(filePaths, schema);
|
|
1980
|
+
}
|
|
1981
|
+
/**
|
|
1982
|
+
* Find all definitions for a given match
|
|
1983
|
+
*/
|
|
1984
|
+
async findDefinitions(match) {
|
|
1985
|
+
if (!this.intelligenceProvider) {
|
|
1986
|
+
throw new LanguageBackendNotSupportedError(
|
|
1987
|
+
"findDefinitions",
|
|
1988
|
+
"no intelligence provider configured for this language"
|
|
1989
|
+
);
|
|
1990
|
+
}
|
|
1991
|
+
return await this.intelligenceProvider.findDefinitions(match);
|
|
1992
|
+
}
|
|
1993
|
+
/**
|
|
1994
|
+
* Find all references for a given match
|
|
1995
|
+
*/
|
|
1996
|
+
async findReferences(match) {
|
|
1997
|
+
if (!this.intelligenceProvider) {
|
|
1998
|
+
throw new LanguageBackendNotSupportedError(
|
|
1999
|
+
"findReferences",
|
|
2000
|
+
"no intelligence provider configured for this language"
|
|
2001
|
+
);
|
|
2002
|
+
}
|
|
2003
|
+
return await this.intelligenceProvider.findReferences(match);
|
|
2004
|
+
}
|
|
2005
|
+
/**
|
|
2006
|
+
* Expand a match to its enclosing function, method, or class definition
|
|
2007
|
+
* Useful for expanding a definition point (e.g., method name) to the full body
|
|
2008
|
+
*/
|
|
2009
|
+
async expandMatch(match) {
|
|
2010
|
+
if (!this.astProvider) {
|
|
2011
|
+
throw new LanguageBackendNotSupportedError(
|
|
2012
|
+
"expandMatch",
|
|
2013
|
+
"no AST provider configured for this language"
|
|
2014
|
+
);
|
|
2015
|
+
}
|
|
2016
|
+
return await this.astProvider.expandMatch(match);
|
|
2017
|
+
}
|
|
2018
|
+
};
|
|
2019
|
+
|
|
2020
|
+
// src/steps/FindMatchesStep.ts
|
|
2021
|
+
var FindMatchesStep = class {
|
|
2022
|
+
environment;
|
|
2023
|
+
eventEmitter;
|
|
2024
|
+
constructor(environment, eventEmitter) {
|
|
2025
|
+
this.environment = environment;
|
|
2026
|
+
this.eventEmitter = eventEmitter;
|
|
2027
|
+
}
|
|
2028
|
+
/**
|
|
2029
|
+
* Execute pattern matching on files
|
|
2030
|
+
* @param schema - The matching schema (rule, constraints, etc.) - provider-specific
|
|
2031
|
+
* @param language - The language to use for pattern matching
|
|
2032
|
+
* @param context - Context containing filePaths and optional previousMatches to filter against
|
|
2033
|
+
*/
|
|
2034
|
+
async execute(schema, language, context) {
|
|
2035
|
+
const startTime = Date.now();
|
|
2036
|
+
const languageFilteredPaths = context.filePaths.filter((filePath) => {
|
|
2037
|
+
const fileLanguage = getLanguageFromFilePath(filePath);
|
|
2038
|
+
return fileLanguage === language;
|
|
2039
|
+
});
|
|
2040
|
+
const backend = new LanguageBackend(this.environment, language, this.eventEmitter);
|
|
2041
|
+
const allMatches = await backend.findMatches(languageFilteredPaths, schema);
|
|
2042
|
+
const matchesWithLanguage = allMatches.map((match) => {
|
|
2043
|
+
if (path8.isAbsolute(match.filePath)) {
|
|
2044
|
+
throw new Error(
|
|
2045
|
+
`Match provider returned absolute path: ${match.filePath}. All file paths must be relative to workspace root.`
|
|
2046
|
+
);
|
|
2047
|
+
}
|
|
2048
|
+
return {
|
|
2049
|
+
...match,
|
|
2050
|
+
language
|
|
2051
|
+
};
|
|
2052
|
+
});
|
|
2053
|
+
let filteredMatches = context.previousMatches && context.previousMatches.length > 0 ? this.filterMatchesByRanges(matchesWithLanguage, context.previousMatches, language) : matchesWithLanguage;
|
|
2054
|
+
filteredMatches = this.deduplicateMatches(filteredMatches);
|
|
2055
|
+
const executionTime = Date.now() - startTime;
|
|
2056
|
+
return {
|
|
2057
|
+
matches: filteredMatches,
|
|
2058
|
+
totalMatches: filteredMatches.length,
|
|
2059
|
+
executionTime
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
/**
|
|
2063
|
+
* Filter matches to only include those within the ranges of previous matches
|
|
2064
|
+
* A match is included if it falls within any of the previous match ranges
|
|
2065
|
+
* The source is propagated from the previous match
|
|
2066
|
+
*/
|
|
2067
|
+
filterMatchesByRanges(matches, previousMatches, _language) {
|
|
2068
|
+
const result = [];
|
|
2069
|
+
for (const match of matches) {
|
|
2070
|
+
const containingMatch = previousMatches.find(
|
|
2071
|
+
(prevMatch) => this.isMatchInRange(match, prevMatch)
|
|
2072
|
+
);
|
|
2073
|
+
if (!containingMatch) {
|
|
2074
|
+
continue;
|
|
2075
|
+
}
|
|
2076
|
+
const source = [
|
|
2077
|
+
...containingMatch.source || [],
|
|
2078
|
+
{
|
|
2079
|
+
filePath: containingMatch.filePath,
|
|
2080
|
+
text: containingMatch.text,
|
|
2081
|
+
range: containingMatch.range,
|
|
2082
|
+
symbol: containingMatch.symbol,
|
|
2083
|
+
language: containingMatch.language
|
|
2084
|
+
}
|
|
2085
|
+
];
|
|
2086
|
+
result.push({
|
|
2087
|
+
...match,
|
|
2088
|
+
source
|
|
2089
|
+
});
|
|
2090
|
+
}
|
|
2091
|
+
return result;
|
|
2092
|
+
}
|
|
2093
|
+
/**
|
|
2094
|
+
* Check if a match falls within the range of another match
|
|
2095
|
+
* Both matches must be in the same file
|
|
2096
|
+
*/
|
|
2097
|
+
isMatchInRange(match, rangeMatch) {
|
|
2098
|
+
if (match.filePath !== rangeMatch.filePath) {
|
|
2099
|
+
return false;
|
|
2100
|
+
}
|
|
2101
|
+
const matchStart = match.range.start;
|
|
2102
|
+
const matchEnd = match.range.end;
|
|
2103
|
+
const rangeStart = rangeMatch.range.start;
|
|
2104
|
+
const rangeEnd = rangeMatch.range.end;
|
|
2105
|
+
const startInRange = matchStart.line > rangeStart.line || matchStart.line === rangeStart.line && matchStart.column >= rangeStart.column;
|
|
2106
|
+
const endInRange = matchEnd.line < rangeEnd.line || matchEnd.line === rangeEnd.line && matchEnd.column <= rangeEnd.column;
|
|
2107
|
+
return startInRange && endInRange;
|
|
2108
|
+
}
|
|
2109
|
+
/**
|
|
2110
|
+
* Remove duplicate matches by keeping only the largest overlapping match.
|
|
2111
|
+
* If multiple matches overlap (one is contained within another), keep only the largest one.
|
|
2112
|
+
*/
|
|
2113
|
+
deduplicateMatches(matches) {
|
|
2114
|
+
if (matches.length <= 1) {
|
|
2115
|
+
return matches;
|
|
2116
|
+
}
|
|
2117
|
+
const result = [];
|
|
2118
|
+
for (const match of matches) {
|
|
2119
|
+
const isContainedByAnother = matches.some((otherMatch) => {
|
|
2120
|
+
if (match === otherMatch) return false;
|
|
2121
|
+
return this.isMatchInRange(match, otherMatch);
|
|
2122
|
+
});
|
|
2123
|
+
if (!isContainedByAnother) {
|
|
2124
|
+
result.push(match);
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
return result;
|
|
2128
|
+
}
|
|
2129
|
+
};
|
|
2130
|
+
|
|
2131
|
+
// src/steps/GotoDefinitionStep.ts
|
|
2132
|
+
import path9 from "path";
|
|
2133
|
+
import { minimatch as minimatch3 } from "minimatch";
|
|
2134
|
+
var GotoDefinitionStep = class {
|
|
2135
|
+
maxDepth = 1;
|
|
2136
|
+
environment;
|
|
2137
|
+
eventEmitter;
|
|
2138
|
+
constructor(environment, eventEmitter) {
|
|
2139
|
+
this.environment = environment;
|
|
2140
|
+
this.eventEmitter = eventEmitter || new ExecutionEventEmitter();
|
|
2141
|
+
}
|
|
2142
|
+
/**
|
|
2143
|
+
* Execute goto-definition on matches
|
|
2144
|
+
*/
|
|
2145
|
+
async execute(matches, language, options = {}) {
|
|
2146
|
+
const startTime = Date.now();
|
|
2147
|
+
const backend = new LanguageBackend(this.environment, language, this.eventEmitter);
|
|
2148
|
+
const definitions = await this.followDefinitions(
|
|
2149
|
+
matches,
|
|
2150
|
+
backend,
|
|
2151
|
+
this.maxDepth,
|
|
2152
|
+
options.where,
|
|
2153
|
+
language
|
|
2154
|
+
);
|
|
2155
|
+
const executionTime = Date.now() - startTime;
|
|
2156
|
+
const flattenedDefinitions = definitions.map((def) => {
|
|
2157
|
+
const { depth: _depth, sourceMatch, ...match } = def;
|
|
2158
|
+
const source = sourceMatch ? [
|
|
2159
|
+
{
|
|
2160
|
+
filePath: sourceMatch.filePath,
|
|
2161
|
+
text: sourceMatch.text,
|
|
2162
|
+
range: sourceMatch.range,
|
|
2163
|
+
symbol: sourceMatch.symbol,
|
|
2164
|
+
language: sourceMatch.language
|
|
2165
|
+
},
|
|
2166
|
+
...sourceMatch.source || []
|
|
2167
|
+
] : [];
|
|
2168
|
+
return {
|
|
2169
|
+
...match,
|
|
2170
|
+
source
|
|
2171
|
+
};
|
|
2172
|
+
});
|
|
2173
|
+
return {
|
|
2174
|
+
definitions: flattenedDefinitions,
|
|
2175
|
+
totalDefinitions: flattenedDefinitions.length,
|
|
2176
|
+
executionTime
|
|
2177
|
+
};
|
|
2178
|
+
}
|
|
2179
|
+
/**
|
|
2180
|
+
* Follow definitions up to maxDepth
|
|
2181
|
+
*/
|
|
2182
|
+
async followDefinitions(matches, backend, maxDepth, filter, language) {
|
|
2183
|
+
const definitions = [];
|
|
2184
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2185
|
+
for (const match of matches) {
|
|
2186
|
+
await this.followDefinitionsRecursive(
|
|
2187
|
+
match,
|
|
2188
|
+
backend,
|
|
2189
|
+
0,
|
|
2190
|
+
maxDepth,
|
|
2191
|
+
filter,
|
|
2192
|
+
definitions,
|
|
2193
|
+
visited,
|
|
2194
|
+
match,
|
|
2195
|
+
language
|
|
2196
|
+
);
|
|
2197
|
+
}
|
|
2198
|
+
return definitions;
|
|
2199
|
+
}
|
|
2200
|
+
/**
|
|
2201
|
+
* Recursively follow definitions
|
|
2202
|
+
*/
|
|
2203
|
+
async followDefinitionsRecursive(match, backend, currentDepth, maxDepth, filter, definitions, visited, sourceMatch, language) {
|
|
2204
|
+
if (currentDepth >= maxDepth) {
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
const key = `${match.filePath}:${match.range.start.line}:${match.range.start.column}:${match.symbol ?? ""}`;
|
|
2208
|
+
if (visited.has(key)) {
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
visited.add(key);
|
|
2212
|
+
const backendDefinitions = await backend.findDefinitions(match);
|
|
2213
|
+
for (const backendDef of backendDefinitions) {
|
|
2214
|
+
const expandedDef = await backend.expandMatch(backendDef) ?? backendDef;
|
|
2215
|
+
const defResult = {
|
|
2216
|
+
...expandedDef,
|
|
2217
|
+
language,
|
|
2218
|
+
depth: currentDepth,
|
|
2219
|
+
sourceMatch: sourceMatch || match
|
|
2220
|
+
};
|
|
2221
|
+
if (filter && !this.matchesDefinitionFilter(defResult, filter)) {
|
|
2222
|
+
continue;
|
|
2223
|
+
}
|
|
2224
|
+
definitions.push(defResult);
|
|
2225
|
+
if (currentDepth + 1 < maxDepth) {
|
|
2226
|
+
await this.followDefinitionsRecursive(
|
|
2227
|
+
backendDef,
|
|
2228
|
+
backend,
|
|
2229
|
+
currentDepth + 1,
|
|
2230
|
+
maxDepth,
|
|
2231
|
+
filter,
|
|
2232
|
+
definitions,
|
|
2233
|
+
visited,
|
|
2234
|
+
sourceMatch || match,
|
|
2235
|
+
language
|
|
2236
|
+
);
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
/**
|
|
2241
|
+
* Check if a definition matches a filter
|
|
2242
|
+
*/
|
|
2243
|
+
matchesDefinitionFilter(def, filter) {
|
|
2244
|
+
var _a, _b, _c;
|
|
2245
|
+
if (filter.all) {
|
|
2246
|
+
return filter.all.every((f) => this.matchesDefinitionFilter(def, f));
|
|
2247
|
+
}
|
|
2248
|
+
if (filter.any) {
|
|
2249
|
+
return filter.any.some((f) => this.matchesDefinitionFilter(def, f));
|
|
2250
|
+
}
|
|
2251
|
+
if (filter.not) {
|
|
2252
|
+
return !this.matchesDefinitionFilter(def, filter.not);
|
|
2253
|
+
}
|
|
2254
|
+
if (filter.file) {
|
|
2255
|
+
if (!this.matchesFileSpec(def.filePath, filter.file)) {
|
|
2256
|
+
return false;
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
if ((_a = filter.method) == null ? void 0 : _a.name) {
|
|
2260
|
+
const symbol = def.symbol;
|
|
2261
|
+
if (!symbol || !this.matchesNameSpec(symbol, filter.method.name)) {
|
|
2262
|
+
return false;
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
if ((_b = filter.function) == null ? void 0 : _b.name) {
|
|
2266
|
+
const symbol = def.symbol;
|
|
2267
|
+
if (!symbol || !this.matchesNameSpec(symbol, filter.function.name)) {
|
|
2268
|
+
return false;
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
if ((_c = filter.class) == null ? void 0 : _c.name) {
|
|
2272
|
+
}
|
|
2273
|
+
return true;
|
|
2274
|
+
}
|
|
2275
|
+
/**
|
|
2276
|
+
* Check if a name matches a name specification
|
|
2277
|
+
*/
|
|
2278
|
+
matchesNameSpec(name, spec) {
|
|
2279
|
+
if (spec.equals) {
|
|
2280
|
+
return name === spec.equals;
|
|
2281
|
+
}
|
|
2282
|
+
if (spec.anyOf) {
|
|
2283
|
+
return spec.anyOf.includes(name);
|
|
2284
|
+
}
|
|
2285
|
+
if (spec.regex) {
|
|
2286
|
+
const regex = new RegExp(spec.regex);
|
|
2287
|
+
return regex.test(name);
|
|
2288
|
+
}
|
|
2289
|
+
return true;
|
|
2290
|
+
}
|
|
2291
|
+
/**
|
|
2292
|
+
* Check if a file path matches a file specification
|
|
2293
|
+
*/
|
|
2294
|
+
matchesFileSpec(filePath, spec) {
|
|
2295
|
+
const relativePath = path9.relative(this.environment.getWorkspaceRoot(), filePath);
|
|
2296
|
+
if (spec.path) {
|
|
2297
|
+
return relativePath === spec.path;
|
|
2298
|
+
}
|
|
2299
|
+
if (spec.glob) {
|
|
2300
|
+
return minimatch3(relativePath, spec.glob);
|
|
2301
|
+
}
|
|
2302
|
+
if (spec.regex) {
|
|
2303
|
+
const regex = new RegExp(spec.regex);
|
|
2304
|
+
return regex.test(relativePath);
|
|
2305
|
+
}
|
|
2306
|
+
return true;
|
|
2307
|
+
}
|
|
2308
|
+
};
|
|
2309
|
+
|
|
2310
|
+
// src/providers/WispbitViolationValidationProvider.ts
|
|
2311
|
+
var WispbitViolationValidationProvider = class {
|
|
2312
|
+
config;
|
|
2313
|
+
constructor(config) {
|
|
2314
|
+
this.config = config;
|
|
2315
|
+
}
|
|
2316
|
+
async validateViolations(params) {
|
|
2317
|
+
var _a;
|
|
2318
|
+
const matchesWithIds = params.matches.map((match) => ({
|
|
2319
|
+
match,
|
|
2320
|
+
matchId: this.generateMatchId(match)
|
|
2321
|
+
}));
|
|
2322
|
+
const baseUrl = this.config.getBaseUrl();
|
|
2323
|
+
const apiKey = this.config.getApiKey();
|
|
2324
|
+
const response = await fetch(`${baseUrl}/plv1/validate-violation`, {
|
|
2325
|
+
method: "POST",
|
|
2326
|
+
headers: {
|
|
2327
|
+
"Content-Type": "application/json",
|
|
2328
|
+
Authorization: `Bearer ${apiKey}`
|
|
2329
|
+
},
|
|
2330
|
+
body: JSON.stringify({
|
|
2331
|
+
rule: params.rule,
|
|
2332
|
+
matches: matchesWithIds.map(({ match, matchId }) => ({
|
|
2333
|
+
matchId,
|
|
2334
|
+
filePath: match.filePath,
|
|
2335
|
+
range: match.range,
|
|
2336
|
+
text: match.text,
|
|
2337
|
+
language: match.language,
|
|
2338
|
+
symbol: match.symbol,
|
|
2339
|
+
source: match.source
|
|
2340
|
+
})),
|
|
2341
|
+
powerlint_version: this.config.getLocalVersion(),
|
|
2342
|
+
schema_version: this.config.getSchemaVersion()
|
|
2343
|
+
})
|
|
2344
|
+
});
|
|
2345
|
+
if (!response.ok) {
|
|
2346
|
+
const errorText = await response.text();
|
|
2347
|
+
throw new Error(
|
|
2348
|
+
`Wispbit validation failed: ${response.status} ${response.statusText} - ${errorText}`
|
|
2349
|
+
);
|
|
2350
|
+
}
|
|
2351
|
+
const result = await response.json();
|
|
2352
|
+
const results = [];
|
|
2353
|
+
for (const { matchId } of matchesWithIds) {
|
|
2354
|
+
const validationResult = (_a = result.results) == null ? void 0 : _a.find((r) => r.matchId === matchId);
|
|
2355
|
+
results.push({
|
|
2356
|
+
matchId,
|
|
2357
|
+
isViolation: Boolean(validationResult == null ? void 0 : validationResult.isViolation),
|
|
2358
|
+
reason: String((validationResult == null ? void 0 : validationResult.reason) || "No violation detected"),
|
|
2359
|
+
confidence: typeof (validationResult == null ? void 0 : validationResult.confidence) === "number" ? validationResult.confidence : 1
|
|
2360
|
+
});
|
|
2361
|
+
}
|
|
2362
|
+
return results;
|
|
2363
|
+
}
|
|
2364
|
+
/**
|
|
2365
|
+
* Generate a unique ID for a single match
|
|
2366
|
+
*/
|
|
2367
|
+
generateMatchId(match) {
|
|
2368
|
+
const matchData = {
|
|
2369
|
+
filePath: match.filePath,
|
|
2370
|
+
startLine: match.range.start.line,
|
|
2371
|
+
startColumn: match.range.start.column || 0,
|
|
2372
|
+
endLine: match.range.end.line,
|
|
2373
|
+
endColumn: match.range.end.column || 0,
|
|
2374
|
+
text: match.text
|
|
2375
|
+
};
|
|
2376
|
+
return hashString(JSON.stringify(matchData)).substring(0, 16);
|
|
2377
|
+
}
|
|
2378
|
+
};
|
|
2379
|
+
|
|
2380
|
+
// src/steps/LLMStep.ts
|
|
2381
|
+
var LLMStep = class {
|
|
2382
|
+
environment;
|
|
2383
|
+
eventEmitter;
|
|
2384
|
+
config;
|
|
2385
|
+
storage;
|
|
2386
|
+
constructor(config, environment, eventEmitter) {
|
|
2387
|
+
this.environment = environment;
|
|
2388
|
+
this.eventEmitter = eventEmitter || new ExecutionEventEmitter();
|
|
2389
|
+
this.config = config;
|
|
2390
|
+
this.storage = new Storage(environment);
|
|
2391
|
+
}
|
|
2392
|
+
/**
|
|
2393
|
+
* Generate a unique ID for a single match
|
|
2394
|
+
* Hash is based on filePath + range + text
|
|
2395
|
+
*/
|
|
2396
|
+
generateMatchId(match) {
|
|
2397
|
+
const matchData = {
|
|
2398
|
+
filePath: match.filePath,
|
|
2399
|
+
startLine: match.range.start.line,
|
|
2400
|
+
startColumn: match.range.start.column || 0,
|
|
2401
|
+
endLine: match.range.end.line,
|
|
2402
|
+
endColumn: match.range.end.column || 0,
|
|
2403
|
+
text: match.text
|
|
2404
|
+
};
|
|
2405
|
+
return hashString(JSON.stringify(matchData)).substring(0, 16);
|
|
2406
|
+
}
|
|
2407
|
+
/**
|
|
2408
|
+
* Generate a hash for a prompt string
|
|
2409
|
+
*/
|
|
2410
|
+
hashPrompt(prompt) {
|
|
2411
|
+
return hashString(prompt);
|
|
2412
|
+
}
|
|
2413
|
+
/**
|
|
2414
|
+
* Generate a cache key for a single match LLM validation
|
|
2415
|
+
*/
|
|
2416
|
+
generateMatchCacheKey(matchId, promptHash) {
|
|
2417
|
+
return `match_${matchId}_prompt_${promptHash}.json`;
|
|
2418
|
+
}
|
|
2419
|
+
/**
|
|
2420
|
+
* Execute the actual LLM validation for uncached matches
|
|
2421
|
+
* @param config - LLM step configuration (contains provider and model)
|
|
2422
|
+
* @param uncachedMatches - Matches that need LLM evaluation
|
|
2423
|
+
* @param promptContent - The loaded prompt content
|
|
2424
|
+
*/
|
|
2425
|
+
async executeLLMValidation(rule, uncachedMatches) {
|
|
2426
|
+
const llmStartTime = Date.now();
|
|
2427
|
+
const promptHash = this.hashPrompt(rule.prompt);
|
|
2428
|
+
const validationProvider = new WispbitViolationValidationProvider(this.config);
|
|
2429
|
+
const validationParams = {
|
|
2430
|
+
rule,
|
|
2431
|
+
matches: uncachedMatches
|
|
2432
|
+
};
|
|
2433
|
+
this.eventEmitter.startLLMValidation(rule.id, uncachedMatches.length);
|
|
2434
|
+
const validationResults = await validationProvider.validateViolations(validationParams);
|
|
2435
|
+
const newViolationMatches = [];
|
|
2436
|
+
for (const result of validationResults) {
|
|
2437
|
+
const originalMatch = uncachedMatches.find(
|
|
2438
|
+
(match) => this.generateMatchId(match) === result.matchId
|
|
2439
|
+
);
|
|
2440
|
+
if (originalMatch && result.isViolation) {
|
|
2441
|
+
newViolationMatches.push({
|
|
2442
|
+
...originalMatch,
|
|
2443
|
+
metadata: {
|
|
2444
|
+
fromCache: false,
|
|
2445
|
+
llmValidation: {
|
|
2446
|
+
isViolation: true,
|
|
2447
|
+
confidence: result.confidence,
|
|
2448
|
+
reason: result.reason
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
});
|
|
2452
|
+
}
|
|
2453
|
+
const cacheKey = this.generateMatchCacheKey(result.matchId, promptHash);
|
|
2454
|
+
const decision = {
|
|
2455
|
+
matchId: result.matchId,
|
|
2456
|
+
isViolation: result.isViolation,
|
|
2457
|
+
confidence: result.confidence,
|
|
2458
|
+
reason: result.reason
|
|
2459
|
+
};
|
|
2460
|
+
await this.storage.saveCache(rule.id, cacheKey, decision);
|
|
2461
|
+
}
|
|
2462
|
+
this.eventEmitter.completeLLMValidation(
|
|
2463
|
+
rule.id,
|
|
2464
|
+
0,
|
|
2465
|
+
// Token usage is handled internally by the validation providers
|
|
2466
|
+
Date.now() - llmStartTime,
|
|
2467
|
+
newViolationMatches.length,
|
|
2468
|
+
false
|
|
2469
|
+
);
|
|
2470
|
+
return {
|
|
2471
|
+
matches: newViolationMatches,
|
|
2472
|
+
executionTime: Date.now() - llmStartTime
|
|
2473
|
+
};
|
|
2474
|
+
}
|
|
2475
|
+
/**
|
|
2476
|
+
* Execute the LLM step - always runs LLM validation immediately
|
|
2477
|
+
* @param config - LLM step configuration
|
|
2478
|
+
* @param previousMatches - Matches from previous steps to be judged by LLM
|
|
2479
|
+
*/
|
|
2480
|
+
async execute(rule, previousMatches) {
|
|
2481
|
+
const startTime = Date.now();
|
|
2482
|
+
const promptHash = this.hashPrompt(rule.prompt);
|
|
2483
|
+
const cachedMatches = [];
|
|
2484
|
+
const uncachedMatches = [];
|
|
2485
|
+
for (const match of previousMatches) {
|
|
2486
|
+
const matchId = this.generateMatchId(match);
|
|
2487
|
+
const cacheKey = this.generateMatchCacheKey(matchId, promptHash);
|
|
2488
|
+
const cachedDecision = await this.storage.readCache(rule.id, cacheKey);
|
|
2489
|
+
if (cachedDecision) {
|
|
2490
|
+
if (cachedDecision.isViolation) {
|
|
2491
|
+
cachedMatches.push({
|
|
2492
|
+
...match,
|
|
2493
|
+
metadata: {
|
|
2494
|
+
fromCache: true,
|
|
2495
|
+
llmValidation: {
|
|
2496
|
+
isViolation: cachedDecision.isViolation,
|
|
2497
|
+
confidence: cachedDecision.confidence,
|
|
2498
|
+
reason: cachedDecision.reason
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
});
|
|
2502
|
+
}
|
|
2503
|
+
} else {
|
|
2504
|
+
uncachedMatches.push(match);
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
let newViolationMatches = [];
|
|
2508
|
+
if (uncachedMatches.length > 0) {
|
|
2509
|
+
const llmResult = await this.executeLLMValidation(rule, uncachedMatches);
|
|
2510
|
+
newViolationMatches = llmResult.matches;
|
|
2511
|
+
}
|
|
2512
|
+
if (uncachedMatches.length === 0) {
|
|
2513
|
+
this.eventEmitter.completeLLMValidation(
|
|
2514
|
+
rule.id,
|
|
2515
|
+
0,
|
|
2516
|
+
// No tokens for cached result
|
|
2517
|
+
Date.now() - startTime,
|
|
2518
|
+
cachedMatches.length,
|
|
2519
|
+
true
|
|
2520
|
+
// fromCache = true
|
|
2521
|
+
);
|
|
2522
|
+
}
|
|
2523
|
+
const allMatches = [...cachedMatches, ...newViolationMatches];
|
|
2524
|
+
return {
|
|
2525
|
+
matches: allMatches,
|
|
2526
|
+
executionTime: Date.now() - startTime
|
|
2527
|
+
};
|
|
2528
|
+
}
|
|
2529
|
+
};
|
|
2530
|
+
|
|
2531
|
+
// src/steps/RuleExecutor.ts
|
|
2532
|
+
var RuleExecutor = class {
|
|
2533
|
+
fileFilterStep;
|
|
2534
|
+
findMatchesStep;
|
|
2535
|
+
gotoDefinitionStep;
|
|
2536
|
+
llmStep;
|
|
2537
|
+
environment;
|
|
2538
|
+
executionContext;
|
|
2539
|
+
currentMode;
|
|
2540
|
+
eventEmitter;
|
|
2541
|
+
config;
|
|
2542
|
+
constructor(config, environment, eventEmitter) {
|
|
2543
|
+
this.environment = environment;
|
|
2544
|
+
this.eventEmitter = eventEmitter || new ExecutionEventEmitter();
|
|
2545
|
+
this.config = config;
|
|
2546
|
+
this.fileFilterStep = new FileFilterStep(this.environment);
|
|
2547
|
+
this.findMatchesStep = new FindMatchesStep(this.environment, this.eventEmitter);
|
|
2548
|
+
this.gotoDefinitionStep = new GotoDefinitionStep(this.environment, this.eventEmitter);
|
|
2549
|
+
this.llmStep = new LLMStep(this.config, this.environment, this.eventEmitter);
|
|
2550
|
+
}
|
|
2551
|
+
/**
|
|
2552
|
+
* Execute a single step
|
|
2553
|
+
*/
|
|
2554
|
+
async executeStep(rule, stepName, stepConfig, filePaths, matches, options) {
|
|
2555
|
+
const stepType = stepConfig.type;
|
|
2556
|
+
if (stepType === "ast-grep") {
|
|
2557
|
+
const { type: _type, language, ...schema } = stepConfig;
|
|
2558
|
+
const stepResult = await this.findMatchesStep.execute(schema, language, {
|
|
2559
|
+
filePaths,
|
|
2560
|
+
previousMatches: matches
|
|
2561
|
+
});
|
|
2562
|
+
return {
|
|
2563
|
+
matches: stepResult.matches
|
|
2564
|
+
};
|
|
2565
|
+
}
|
|
2566
|
+
if (stepType === "step-group") {
|
|
2567
|
+
let allMatches = [];
|
|
2568
|
+
for (let i = 0; i < stepConfig.steps.length; i++) {
|
|
2569
|
+
const subStep = stepConfig.steps[i];
|
|
2570
|
+
const subStepName = `${stepName}[${i}]`;
|
|
2571
|
+
const subResult = await this.executeStep(
|
|
2572
|
+
rule,
|
|
2573
|
+
subStepName,
|
|
2574
|
+
subStep,
|
|
2575
|
+
filePaths,
|
|
2576
|
+
matches,
|
|
2577
|
+
options
|
|
2578
|
+
);
|
|
2579
|
+
if (subResult.matches) {
|
|
2580
|
+
allMatches = allMatches.concat(subResult.matches);
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
return {
|
|
2584
|
+
matches: allMatches
|
|
2585
|
+
};
|
|
2586
|
+
}
|
|
2587
|
+
if (stepType === "file-filter") {
|
|
2588
|
+
const stepResult = await this.fileFilterStep.execute(filePaths, {
|
|
2589
|
+
include: stepConfig.include,
|
|
2590
|
+
ignore: stepConfig.ignore,
|
|
2591
|
+
conditions: stepConfig.conditions
|
|
2592
|
+
});
|
|
2593
|
+
return {
|
|
2594
|
+
filePaths: stepResult.filteredPaths
|
|
2595
|
+
};
|
|
2596
|
+
}
|
|
2597
|
+
if (stepType === "goto-definition") {
|
|
2598
|
+
if (matches.length === 0) {
|
|
2599
|
+
return {
|
|
2600
|
+
matches: []
|
|
2601
|
+
};
|
|
2602
|
+
}
|
|
2603
|
+
const { language } = stepConfig;
|
|
2604
|
+
const matchesForLanguage = matches.filter((m) => m.language === language);
|
|
2605
|
+
if (matchesForLanguage.length === 0) {
|
|
2606
|
+
return {
|
|
2607
|
+
matches: []
|
|
2608
|
+
};
|
|
2609
|
+
}
|
|
2610
|
+
const stepResult = await this.gotoDefinitionStep.execute(matchesForLanguage, language, {
|
|
2611
|
+
where: stepConfig.where
|
|
2612
|
+
});
|
|
2613
|
+
const definitionFilePaths = [...new Set(stepResult.definitions.map((d) => d.filePath))];
|
|
2614
|
+
return {
|
|
2615
|
+
filePaths: definitionFilePaths,
|
|
2616
|
+
matches: stepResult.definitions
|
|
2617
|
+
};
|
|
2618
|
+
}
|
|
2619
|
+
if (stepType === "llm") {
|
|
2620
|
+
if (matches.length === 0) {
|
|
2621
|
+
return {
|
|
2622
|
+
matches: []
|
|
2623
|
+
};
|
|
2624
|
+
}
|
|
2625
|
+
const llmResult = await this.llmStep.execute(rule, matches);
|
|
2626
|
+
return {
|
|
2627
|
+
matches: llmResult.matches
|
|
2628
|
+
};
|
|
2629
|
+
}
|
|
2630
|
+
throw new Error(`Unknown step type: ${stepType}`);
|
|
2631
|
+
}
|
|
2632
|
+
/**
|
|
2633
|
+
* Execute all steps in multiple rule configurations
|
|
2634
|
+
*/
|
|
2635
|
+
async execute(rules, options) {
|
|
2636
|
+
if (this.currentMode && this.currentMode !== options.mode) {
|
|
2637
|
+
throw new Error(`Execution mode mismatch: expected ${this.currentMode}, got ${options.mode}`);
|
|
2638
|
+
}
|
|
2639
|
+
if (!this.executionContext || this.currentMode !== options.mode) {
|
|
2640
|
+
this.executionContext = await FileExecutionContext.initialize(
|
|
2641
|
+
this.config,
|
|
2642
|
+
this.environment,
|
|
2643
|
+
options.mode,
|
|
2644
|
+
this.eventEmitter,
|
|
2645
|
+
options.filePath,
|
|
2646
|
+
options.baseSha
|
|
2647
|
+
);
|
|
2648
|
+
this.currentMode = options.mode;
|
|
2649
|
+
}
|
|
2650
|
+
this.eventEmitter.startRules(rules.length);
|
|
2651
|
+
const results = [];
|
|
2652
|
+
for (let i = 0; i < rules.length; i++) {
|
|
2653
|
+
const rule = rules[i];
|
|
2654
|
+
const ruleId = rule.id;
|
|
2655
|
+
let isLlm = false;
|
|
2656
|
+
for (const stepWithId of rule.config.steps) {
|
|
2657
|
+
if (stepWithId.step.type === "llm") {
|
|
2658
|
+
isLlm = true;
|
|
2659
|
+
break;
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
this.eventEmitter.progressRule(i + 1, rules.length, ruleId, isLlm);
|
|
2663
|
+
const ruleConfig = rule.config;
|
|
2664
|
+
let currentFilePaths = [...this.executionContext.filePaths];
|
|
2665
|
+
let currentMatches = [];
|
|
2666
|
+
for (const stepWithId of ruleConfig.steps) {
|
|
2667
|
+
const stepName = stepWithId.id;
|
|
2668
|
+
const stepConfig = stepWithId.step;
|
|
2669
|
+
this.eventEmitter.startStep(ruleId, stepName, stepConfig.type, {
|
|
2670
|
+
filePaths: currentFilePaths,
|
|
2671
|
+
matches: currentMatches
|
|
2672
|
+
});
|
|
2673
|
+
const stepStartTime = Date.now();
|
|
2674
|
+
const result = await this.executeStep(
|
|
2675
|
+
rule,
|
|
2676
|
+
stepName,
|
|
2677
|
+
stepConfig,
|
|
2678
|
+
currentFilePaths,
|
|
2679
|
+
currentMatches,
|
|
2680
|
+
options
|
|
2681
|
+
);
|
|
2682
|
+
const stepExecutionTime = Date.now() - stepStartTime;
|
|
2683
|
+
this.eventEmitter.emit("step:complete", {
|
|
2684
|
+
ruleId,
|
|
2685
|
+
stepName,
|
|
2686
|
+
stepType: stepConfig.type,
|
|
2687
|
+
outputs: result,
|
|
2688
|
+
executionTime: stepExecutionTime
|
|
2689
|
+
});
|
|
2690
|
+
if (result.filePaths !== void 0) {
|
|
2691
|
+
currentFilePaths = this.executionContext.filterFiles(result.filePaths);
|
|
2692
|
+
currentMatches = this.executionContext.filterMatchesByFilePaths(
|
|
2693
|
+
currentMatches,
|
|
2694
|
+
currentFilePaths
|
|
2695
|
+
);
|
|
2696
|
+
}
|
|
2697
|
+
if (result.matches !== void 0) {
|
|
2698
|
+
currentMatches = this.executionContext.filterMatches(result.matches);
|
|
2699
|
+
}
|
|
2700
|
+
if (currentFilePaths.length === 0) {
|
|
2701
|
+
break;
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
const sortedMatches = currentMatches.sort((a, b) => a.filePath.localeCompare(b.filePath));
|
|
2705
|
+
results.push({
|
|
2706
|
+
ruleId,
|
|
2707
|
+
matches: sortedMatches
|
|
2708
|
+
});
|
|
2709
|
+
}
|
|
2710
|
+
const totalMatches = results.reduce((sum, result) => sum + result.matches.length, 0);
|
|
2711
|
+
this.eventEmitter.completeRules(rules.length, totalMatches);
|
|
2712
|
+
return results;
|
|
2713
|
+
}
|
|
2714
|
+
};
|
|
2715
|
+
|
|
2716
|
+
// src/types.ts
|
|
2717
|
+
var InvalidRuleFormatError = class extends Error {
|
|
2718
|
+
constructor(ruleId, message, validationErrors) {
|
|
2719
|
+
super(`Invalid rule format in '${ruleId}': ${message}`);
|
|
2720
|
+
this.validationErrors = validationErrors;
|
|
2721
|
+
this.name = "InvalidRuleFormatError";
|
|
2722
|
+
}
|
|
2723
|
+
};
|
|
2724
|
+
|
|
2725
|
+
// src/utils/formatters.ts
|
|
2726
|
+
import { stripVTControlCharacters } from "node:util";
|
|
2727
|
+
import chalk from "chalk";
|
|
2728
|
+
import ora from "ora";
|
|
2729
|
+
var VIOLATION_COLOR = "#ff6b35";
|
|
2730
|
+
var SUGGESTION_COLOR = "#4a90e2";
|
|
2731
|
+
var SKIPPED_COLOR = "#9b59b6";
|
|
2732
|
+
var BRAND_COLOR = "#fbbf24";
|
|
2733
|
+
function formatClickableRuleId(ruleId, internalId) {
|
|
2734
|
+
if (internalId) {
|
|
2735
|
+
return chalk.underline.dim(
|
|
2736
|
+
`\x1B]8;;https://app.wispbit.com/rules/${internalId}\x1B\\${ruleId}\x1B]8;;\x1B\\`
|
|
2737
|
+
);
|
|
2738
|
+
}
|
|
2739
|
+
return chalk.dim(ruleId);
|
|
2740
|
+
}
|
|
2741
|
+
var LINE_NUMBER_BG_COLOR = "#f8f8f8";
|
|
2742
|
+
var LINE_NUMBER_TEXT_COLOR = "#888888";
|
|
2743
|
+
var loadingFrames = [
|
|
2744
|
+
chalk.hex(BRAND_COLOR)("~(oo)~"),
|
|
2745
|
+
chalk.hex(BRAND_COLOR)("~(oO)~"),
|
|
2746
|
+
chalk.hex(BRAND_COLOR)("~(Oo)~"),
|
|
2747
|
+
chalk.hex(BRAND_COLOR)("~(OO)~"),
|
|
2748
|
+
chalk.hex(BRAND_COLOR)("~(\u25CFo)~"),
|
|
2749
|
+
chalk.hex(BRAND_COLOR)("~(o\u25CF)~"),
|
|
2750
|
+
chalk.hex(BRAND_COLOR)("~(\u25C9o)~"),
|
|
2751
|
+
chalk.hex(BRAND_COLOR)("~(o\u25C9)~"),
|
|
2752
|
+
chalk.hex(BRAND_COLOR)("\\(oo)/"),
|
|
2753
|
+
chalk.hex(BRAND_COLOR)("\\(oO)/"),
|
|
2754
|
+
chalk.hex(BRAND_COLOR)("\\(Oo)/"),
|
|
2755
|
+
chalk.hex(BRAND_COLOR)("\\(OO)/"),
|
|
2756
|
+
chalk.hex(BRAND_COLOR)("~(Oo)~"),
|
|
2757
|
+
chalk.hex(BRAND_COLOR)("~(oO)~"),
|
|
2758
|
+
chalk.hex(BRAND_COLOR)("~(\u25CEo)~"),
|
|
2759
|
+
chalk.hex(BRAND_COLOR)("~(o\u25CE)~"),
|
|
2760
|
+
chalk.hex(BRAND_COLOR)("~(oo)~"),
|
|
2761
|
+
chalk.hex(BRAND_COLOR)("~(OO)~"),
|
|
2762
|
+
chalk.hex(BRAND_COLOR)("~(Oo)~"),
|
|
2763
|
+
chalk.hex(BRAND_COLOR)("\\(oo)/"),
|
|
2764
|
+
chalk.hex(BRAND_COLOR)("\\(oO)/"),
|
|
2765
|
+
chalk.hex(BRAND_COLOR)("\\(Oo)/"),
|
|
2766
|
+
chalk.hex(BRAND_COLOR)("\\(OO)/")
|
|
2767
|
+
];
|
|
2768
|
+
function pluralize(word, count) {
|
|
2769
|
+
return count === 1 ? word : `${word}s`;
|
|
2770
|
+
}
|
|
2771
|
+
function textTable(rows, opts = {}) {
|
|
2772
|
+
const hsep = " ";
|
|
2773
|
+
const align = opts.align || [];
|
|
2774
|
+
const stringLength = opts.stringLength || ((str) => stripVTControlCharacters(str).length);
|
|
2775
|
+
const sizes = rows.reduce((acc, row) => {
|
|
2776
|
+
row.forEach((c2, ix) => {
|
|
2777
|
+
const n = stringLength(c2);
|
|
2778
|
+
if (!acc[ix] || n > acc[ix]) {
|
|
2779
|
+
acc[ix] = n;
|
|
2780
|
+
}
|
|
2781
|
+
});
|
|
2782
|
+
return acc;
|
|
2783
|
+
}, []);
|
|
2784
|
+
return rows.map(
|
|
2785
|
+
(row) => row.map((c2, ix) => {
|
|
2786
|
+
const n = sizes[ix] - stringLength(c2) || 0;
|
|
2787
|
+
const s = Array(Math.max(n + 1, 1)).join(" ");
|
|
2788
|
+
if (align[ix] === "r") {
|
|
2789
|
+
return s + c2;
|
|
2790
|
+
}
|
|
2791
|
+
return c2 + s;
|
|
2792
|
+
}).join(hsep).trimEnd()
|
|
2793
|
+
).join("\n");
|
|
2794
|
+
}
|
|
2795
|
+
function printSummary(results, summary) {
|
|
2796
|
+
let output = "\n";
|
|
2797
|
+
let violationCount = 0;
|
|
2798
|
+
let suggestionCount = 0;
|
|
2799
|
+
const ruleResults = /* @__PURE__ */ new Map();
|
|
2800
|
+
results.forEach((result) => {
|
|
2801
|
+
if (result.matches.length === 0) {
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2804
|
+
const ruleId = result.ruleId || "unknown";
|
|
2805
|
+
if (!ruleResults.has(ruleId)) {
|
|
2806
|
+
ruleResults.set(ruleId, {
|
|
2807
|
+
message: result.message,
|
|
2808
|
+
severity: result.severity,
|
|
2809
|
+
internalId: result.internalId,
|
|
2810
|
+
matches: [],
|
|
2811
|
+
hasLlmValidation: false
|
|
2812
|
+
});
|
|
2813
|
+
}
|
|
2814
|
+
const ruleData = ruleResults.get(ruleId);
|
|
2815
|
+
result.matches.forEach((match) => {
|
|
2816
|
+
var _a;
|
|
2817
|
+
ruleData.matches.push({
|
|
2818
|
+
filePath: match.filePath,
|
|
2819
|
+
line: match.range.start.line + 1,
|
|
2820
|
+
column: match.range.start.column + 1,
|
|
2821
|
+
endLine: match.range.end.line !== match.range.start.line ? match.range.end.line + 1 : void 0,
|
|
2822
|
+
text: match.text
|
|
2823
|
+
});
|
|
2824
|
+
if ((_a = match.metadata) == null ? void 0 : _a.llmValidation) {
|
|
2825
|
+
ruleData.hasLlmValidation = true;
|
|
2826
|
+
}
|
|
2827
|
+
if (result.severity === "violation") {
|
|
2828
|
+
violationCount++;
|
|
2829
|
+
} else {
|
|
2830
|
+
suggestionCount++;
|
|
2831
|
+
}
|
|
2832
|
+
});
|
|
2833
|
+
});
|
|
2834
|
+
ruleResults.forEach((ruleData, ruleId) => {
|
|
2835
|
+
if (ruleData.matches.length === 0 && ruleData.severity !== "skipped") {
|
|
2836
|
+
return;
|
|
2837
|
+
}
|
|
2838
|
+
let messageType;
|
|
2839
|
+
if (ruleData.severity === "violation") {
|
|
2840
|
+
messageType = chalk.hex(VIOLATION_COLOR)(" violation".padStart(9));
|
|
2841
|
+
} else if (ruleData.severity === "suggestion") {
|
|
2842
|
+
messageType = chalk.hex(SUGGESTION_COLOR)("suggestion".padEnd(7));
|
|
2843
|
+
} else {
|
|
2844
|
+
messageType = chalk.hex(SKIPPED_COLOR)(" skipped".padEnd(9));
|
|
2845
|
+
}
|
|
2846
|
+
const llmIndicator = ruleData.hasLlmValidation ? `${chalk.hex(SKIPPED_COLOR)("(llm)")} ` : "";
|
|
2847
|
+
const clickableRuleId = formatClickableRuleId(ruleId, ruleData.internalId);
|
|
2848
|
+
const ruleHeader = `${messageType} ${chalk.dim("\u2502")} ${llmIndicator}${ruleData.message.replace(/([^ ])\.$/u, "$1")} ${chalk.dim("(")}${clickableRuleId}${chalk.dim(")")}`;
|
|
2849
|
+
output += `${ruleHeader}
|
|
2850
|
+
`;
|
|
2851
|
+
output += `${chalk.dim("\u2500".repeat(80))}
|
|
2852
|
+
`;
|
|
2853
|
+
if (ruleData.severity === "skipped") {
|
|
2854
|
+
output += `
|
|
2855
|
+
`;
|
|
2856
|
+
} else {
|
|
2857
|
+
const sortedMatches = ruleData.matches.sort((a, b) => a.filePath.localeCompare(b.filePath));
|
|
2858
|
+
const shouldShowCodeSnippets = sortedMatches.length < 10 && sortedMatches.some((match) => match.text && match.text.split("\n").length <= 10);
|
|
2859
|
+
if (shouldShowCodeSnippets) {
|
|
2860
|
+
sortedMatches.forEach((match, index) => {
|
|
2861
|
+
output += ` ${match.filePath}
|
|
2862
|
+
`;
|
|
2863
|
+
if (match.text && match.text.split("\n").length <= 10) {
|
|
2864
|
+
const lines = match.text.split("\n");
|
|
2865
|
+
lines.forEach((line, lineIndex) => {
|
|
2866
|
+
const lineNumber = match.line + lineIndex;
|
|
2867
|
+
const lineNumberBox = chalk.bgHex(LINE_NUMBER_BG_COLOR).hex(LINE_NUMBER_TEXT_COLOR)(
|
|
2868
|
+
` ${String(lineNumber)} `
|
|
2869
|
+
);
|
|
2870
|
+
output += ` ${lineNumberBox} ${chalk.dim(line)}
|
|
2871
|
+
`;
|
|
2872
|
+
});
|
|
2873
|
+
}
|
|
2874
|
+
if (index < sortedMatches.length - 1) {
|
|
2875
|
+
output += `
|
|
2876
|
+
`;
|
|
2877
|
+
}
|
|
2878
|
+
});
|
|
2879
|
+
} else {
|
|
2880
|
+
sortedMatches.forEach((match) => {
|
|
2881
|
+
const lineRange = match.endLine ? `(${match.line}:${match.endLine})` : `(${match.line})`;
|
|
2882
|
+
output += ` ${match.filePath} ${chalk.dim(lineRange)}
|
|
2883
|
+
`;
|
|
2884
|
+
});
|
|
2885
|
+
}
|
|
2886
|
+
output += `
|
|
2887
|
+
|
|
2888
|
+
`;
|
|
2889
|
+
}
|
|
2890
|
+
});
|
|
2891
|
+
const total = violationCount + suggestionCount;
|
|
2892
|
+
if (total === 0) {
|
|
2893
|
+
console.log(`
|
|
2894
|
+
no results`);
|
|
2895
|
+
}
|
|
2896
|
+
const violationText = violationCount > 0 ? `${chalk.dim("violations".padStart(12))} ${chalk.hex(VIOLATION_COLOR)("\u25A0")} ${chalk.hex(VIOLATION_COLOR).bold(violationCount)}` : `${chalk.dim("violations".padStart(12))} ${chalk.dim(violationCount)}`;
|
|
2897
|
+
const suggestionText = suggestionCount > 0 ? `${chalk.dim("suggestions".padStart(12))} ${chalk.hex(SUGGESTION_COLOR)("\u25CF")} ${chalk.hex(SUGGESTION_COLOR).bold(suggestionCount)}` : `${chalk.dim("suggestions".padStart(12))} ${chalk.dim(suggestionCount)}`;
|
|
2898
|
+
const filesText = summary.totalFiles ? `${summary.totalFiles} ${pluralize("file", summary.totalFiles)}` : "0 files";
|
|
2899
|
+
const rulesText = `${summary.totalRules} ${pluralize("rule", summary.totalRules)}`;
|
|
2900
|
+
const timeText = summary.executionTime ? `${Math.round(summary.executionTime)}ms` : "";
|
|
2901
|
+
const detailsText = [filesText, rulesText, timeText].filter(Boolean).join(", ");
|
|
2902
|
+
output += `${violationText}
|
|
2903
|
+
`;
|
|
2904
|
+
output += `${suggestionText}
|
|
2905
|
+
`;
|
|
2906
|
+
output += `${chalk.dim("summary".padStart(12))} ${detailsText}
|
|
2907
|
+
`;
|
|
2908
|
+
console.log(total > 0 ? chalk.reset(output) : output);
|
|
2909
|
+
if (violationCount > 0) {
|
|
2910
|
+
process.exit(1);
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
function printRulesList(rules) {
|
|
2914
|
+
if (rules.length === 0) {
|
|
2915
|
+
console.log(chalk.yellow("No rules found."));
|
|
2916
|
+
console.log(chalk.dim("Go to https://app.wispbit.com/rules to create a rule."));
|
|
2917
|
+
return;
|
|
2918
|
+
}
|
|
2919
|
+
const tableData = [];
|
|
2920
|
+
tableData.push([
|
|
2921
|
+
"",
|
|
2922
|
+
`${"id".padEnd(12)} ${chalk.dim("\u2502")} ${"severity".padEnd(12)} ${chalk.dim("\u2502")} message`
|
|
2923
|
+
]);
|
|
2924
|
+
tableData.push([
|
|
2925
|
+
"",
|
|
2926
|
+
`${chalk.dim("\u2500".repeat(12))} ${chalk.dim("\u253C")} ${chalk.dim("\u2500".repeat(12))} ${chalk.dim("\u253C")} ${chalk.dim("\u2500".repeat(80))}`
|
|
2927
|
+
]);
|
|
2928
|
+
for (const rule of rules) {
|
|
2929
|
+
const severityColor = rule.config.severity === "violation" ? chalk.hex(VIOLATION_COLOR) : chalk.hex(SUGGESTION_COLOR);
|
|
2930
|
+
const severityText = severityColor(rule.config.severity);
|
|
2931
|
+
const ruleId = formatClickableRuleId(rule.id, rule.internalId);
|
|
2932
|
+
const idPadding = Math.max(0, 12 - rule.id.length);
|
|
2933
|
+
const severityPadding = Math.max(0, 12 - rule.config.severity.length);
|
|
2934
|
+
tableData.push([
|
|
2935
|
+
"",
|
|
2936
|
+
`${ruleId}${" ".repeat(idPadding)} ${chalk.dim("\u2502")} ${severityText}${" ".repeat(severityPadding)} ${chalk.dim("\u2502")} ${rule.config.message}`
|
|
2937
|
+
]);
|
|
2938
|
+
}
|
|
2939
|
+
const table = textTable(tableData, {
|
|
2940
|
+
align: ["", "l"],
|
|
2941
|
+
stringLength(str) {
|
|
2942
|
+
return stripVTControlCharacters(str).length;
|
|
2943
|
+
}
|
|
2944
|
+
});
|
|
2945
|
+
console.log("\n" + table);
|
|
2946
|
+
console.log(chalk.dim(`
|
|
2947
|
+
Use 'wispbit check --rule <rule-id>' to run a specific rule.`));
|
|
2948
|
+
}
|
|
2949
|
+
function outputJSON(results, format = "pretty") {
|
|
2950
|
+
const matches = [];
|
|
2951
|
+
for (const result of results) {
|
|
2952
|
+
for (const match of result.matches) {
|
|
2953
|
+
const jsonMatch = {
|
|
2954
|
+
filePath: match.filePath,
|
|
2955
|
+
range: match.range,
|
|
2956
|
+
language: match.language,
|
|
2957
|
+
text: match.text,
|
|
2958
|
+
symbol: match.symbol,
|
|
2959
|
+
source: match.source,
|
|
2960
|
+
ruleId: result.ruleId,
|
|
2961
|
+
internalId: result.internalId,
|
|
2962
|
+
severity: result.severity,
|
|
2963
|
+
message: result.message
|
|
2964
|
+
};
|
|
2965
|
+
matches.push(jsonMatch);
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
if (format === "stream") {
|
|
2969
|
+
for (const match of matches) {
|
|
2970
|
+
console.log(JSON.stringify(match));
|
|
2971
|
+
}
|
|
2972
|
+
} else if (format === "compact") {
|
|
2973
|
+
console.log(JSON.stringify(matches));
|
|
2974
|
+
} else {
|
|
2975
|
+
console.log(JSON.stringify(matches, null, 2));
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
function setupTerminalReporter(eventEmitter, debugMode = false) {
|
|
2979
|
+
let spinner = null;
|
|
2980
|
+
let indexingStartTime = null;
|
|
2981
|
+
let executionMode = null;
|
|
2982
|
+
eventEmitter.on("execution:mode", (data) => {
|
|
2983
|
+
executionMode = data;
|
|
2984
|
+
});
|
|
2985
|
+
eventEmitter.on("rules:start", (_data) => {
|
|
2986
|
+
spinner = ora({
|
|
2987
|
+
spinner: {
|
|
2988
|
+
interval: 120,
|
|
2989
|
+
frames: loadingFrames
|
|
2990
|
+
},
|
|
2991
|
+
color: false
|
|
2992
|
+
}).start();
|
|
2993
|
+
const handleInterrupt = () => {
|
|
2994
|
+
if (spinner) {
|
|
2995
|
+
spinner.stop();
|
|
2996
|
+
}
|
|
2997
|
+
process.exit(130);
|
|
2998
|
+
};
|
|
2999
|
+
process.on("SIGINT", handleInterrupt);
|
|
3000
|
+
process.on("SIGTERM", handleInterrupt);
|
|
3001
|
+
});
|
|
3002
|
+
eventEmitter.on("rules:progress", (data) => {
|
|
3003
|
+
if (spinner) {
|
|
3004
|
+
let modeText = "";
|
|
3005
|
+
if (executionMode) {
|
|
3006
|
+
if (executionMode.mode === "check") {
|
|
3007
|
+
modeText = executionMode.filePath ? ` (${executionMode.filePath})` : " (ALL FILES)";
|
|
3008
|
+
} else if (executionMode.mode === "diff") {
|
|
3009
|
+
modeText = ` (${executionMode.baseCommit} \u2192 ${executionMode.headCommit})`;
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
spinner.text = `${data.isLlm ? chalk.hex(SKIPPED_COLOR)("(llm) ") : ""}${data.ruleId}${modeText}`;
|
|
3013
|
+
}
|
|
3014
|
+
});
|
|
3015
|
+
eventEmitter.on("rules:complete", () => {
|
|
3016
|
+
if (spinner) {
|
|
3017
|
+
spinner.stop();
|
|
3018
|
+
spinner = null;
|
|
3019
|
+
}
|
|
3020
|
+
});
|
|
3021
|
+
eventEmitter.on("files:discovery:start", (data) => {
|
|
3022
|
+
if (debugMode) {
|
|
3023
|
+
console.log(`
|
|
3024
|
+
${chalk.blue("Discovering files")} in ${chalk.bold(data.mode)} mode...`);
|
|
3025
|
+
}
|
|
3026
|
+
});
|
|
3027
|
+
eventEmitter.on("files:discovery:progress", (data) => {
|
|
3028
|
+
if (debugMode) {
|
|
3029
|
+
console.log(` ${data.message}`);
|
|
3030
|
+
}
|
|
3031
|
+
});
|
|
3032
|
+
eventEmitter.on("files:discovery:complete", (data) => {
|
|
3033
|
+
if (debugMode) {
|
|
3034
|
+
console.log(
|
|
3035
|
+
chalk.green("File discovery complete:"),
|
|
3036
|
+
`${data.totalFiles} files found (${data.executionTime}ms)`
|
|
3037
|
+
);
|
|
3038
|
+
}
|
|
3039
|
+
});
|
|
3040
|
+
eventEmitter.on("files:filter", (data) => {
|
|
3041
|
+
if (debugMode && data.originalCount !== data.filteredCount) {
|
|
3042
|
+
console.log(
|
|
3043
|
+
chalk.yellow("Filtered:"),
|
|
3044
|
+
`${data.originalCount} \u2192 ${data.filteredCount} ${data.filterType}`
|
|
3045
|
+
);
|
|
3046
|
+
}
|
|
3047
|
+
});
|
|
3048
|
+
eventEmitter.on("indexing:start", (_data) => {
|
|
3049
|
+
indexingStartTime = Date.now();
|
|
3050
|
+
});
|
|
3051
|
+
eventEmitter.on("indexing:progress", (data) => {
|
|
3052
|
+
if (spinner) {
|
|
3053
|
+
spinner.color = "yellow";
|
|
3054
|
+
}
|
|
3055
|
+
if (indexingStartTime && Date.now() - indexingStartTime > 1e3) {
|
|
3056
|
+
if (spinner) {
|
|
3057
|
+
spinner.text = `Indexing ${data.language}...`;
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
if (spinner) {
|
|
3061
|
+
if (data.packageName && data.timeMs) {
|
|
3062
|
+
spinner.text = `Indexing ${data.language}: ${data.packageName}`;
|
|
3063
|
+
} else if (data.message !== "Indexing files...") {
|
|
3064
|
+
spinner.text = `Indexing ${data.language}: ${data.message}`;
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
if (debugMode) {
|
|
3068
|
+
if (data.packageName && data.timeMs) {
|
|
3069
|
+
console.log(` + ${data.packageName} (${data.timeMs}ms)`);
|
|
3070
|
+
} else if (data.message === "Indexing files...") {
|
|
3071
|
+
process.stdout.write(".");
|
|
3072
|
+
} else if (data.message !== "Indexing files...") {
|
|
3073
|
+
console.log(` ${data.message}`);
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
});
|
|
3077
|
+
eventEmitter.on("indexing:complete", (data) => {
|
|
3078
|
+
indexingStartTime = null;
|
|
3079
|
+
if (spinner) {
|
|
3080
|
+
spinner.color = "blue";
|
|
3081
|
+
}
|
|
3082
|
+
if (debugMode) {
|
|
3083
|
+
process.stdout.write("\n");
|
|
3084
|
+
console.log(
|
|
3085
|
+
chalk.green("Indexing complete for"),
|
|
3086
|
+
`${data.language} (${data.executionTime}ms)`
|
|
3087
|
+
);
|
|
3088
|
+
}
|
|
3089
|
+
});
|
|
3090
|
+
if (debugMode) {
|
|
3091
|
+
eventEmitter.on("scip:match-lookup:start", (data) => {
|
|
3092
|
+
console.log(
|
|
3093
|
+
chalk.cyan(
|
|
3094
|
+
`
|
|
3095
|
+
\u{1F50D} [${data.language}] Starting SCIP match lookup for: ${JSON.stringify(data.match)}`
|
|
3096
|
+
)
|
|
3097
|
+
);
|
|
3098
|
+
});
|
|
3099
|
+
eventEmitter.on("scip:match-lookup:progress", (data) => {
|
|
3100
|
+
console.log(`${JSON.stringify(data.document)}`);
|
|
3101
|
+
});
|
|
3102
|
+
eventEmitter.on("scip:match-lookup:complete", (data) => {
|
|
3103
|
+
console.log(chalk.green(`SCIP match lookup complete for: ${data.language}`));
|
|
3104
|
+
});
|
|
3105
|
+
eventEmitter.on("step:start", (data) => {
|
|
3106
|
+
console.log(
|
|
3107
|
+
chalk.cyan(`
|
|
3108
|
+
\u{1F527} [${data.ruleId}] Starting step: ${data.stepName} (${data.stepType})`)
|
|
3109
|
+
);
|
|
3110
|
+
console.log(` Input files: ${data.inputs.filePaths.length}`);
|
|
3111
|
+
console.log(` Input matches: ${data.inputs.matches.length}`);
|
|
3112
|
+
if (data.inputs.matches.length > 0) {
|
|
3113
|
+
console.log(` Match details:`);
|
|
3114
|
+
data.inputs.matches.forEach((match, idx) => {
|
|
3115
|
+
var _a;
|
|
3116
|
+
console.log(` Match ${idx + 1}:`);
|
|
3117
|
+
console.log(` File: ${match.filePath}`);
|
|
3118
|
+
console.log(
|
|
3119
|
+
` Range: ${match.range.start.line}:${match.range.start.column} \u2192 ${match.range.end.line}:${match.range.end.column}`
|
|
3120
|
+
);
|
|
3121
|
+
if (match.symbol) {
|
|
3122
|
+
console.log(` Symbol: ${match.symbol}`);
|
|
3123
|
+
}
|
|
3124
|
+
if (match.source && match.source.length > 0) {
|
|
3125
|
+
console.log(` Source chain: ${match.source.length} step(s)`);
|
|
3126
|
+
match.source.forEach((src, srcIdx) => {
|
|
3127
|
+
const range = `${src.range.start.line}:${src.range.start.column} \u2192 ${src.range.end.line}:${src.range.end.column}`;
|
|
3128
|
+
console.log(
|
|
3129
|
+
` ${srcIdx + 1}. ${src.filePath} [${src.symbol || "N/A"}] @ ${range}`
|
|
3130
|
+
);
|
|
3131
|
+
});
|
|
3132
|
+
}
|
|
3133
|
+
console.log(` Text preview: ${(_a = match.text) == null ? void 0 : _a.substring(0, 120)}...`);
|
|
3134
|
+
});
|
|
3135
|
+
}
|
|
3136
|
+
});
|
|
3137
|
+
eventEmitter.on("step:complete", (data) => {
|
|
3138
|
+
console.log(
|
|
3139
|
+
chalk.cyan(
|
|
3140
|
+
`\u2705 [${data.ruleId}] Completed step: ${data.stepName} (${data.stepType}) (${data.executionTime}ms)`
|
|
3141
|
+
)
|
|
3142
|
+
);
|
|
3143
|
+
if (data.outputs.filePaths !== void 0) {
|
|
3144
|
+
console.log(` Output files: ${data.outputs.filePaths.length}`);
|
|
3145
|
+
}
|
|
3146
|
+
if (data.outputs.matches !== void 0) {
|
|
3147
|
+
console.log(` Output matches: ${data.outputs.matches.length}`);
|
|
3148
|
+
if (data.outputs.matches.length > 0) {
|
|
3149
|
+
console.log(` Output match details:`);
|
|
3150
|
+
data.outputs.matches.forEach((match, idx) => {
|
|
3151
|
+
var _a;
|
|
3152
|
+
console.log(` Match ${idx + 1}:`);
|
|
3153
|
+
console.log(` File: ${match.filePath}`);
|
|
3154
|
+
console.log(
|
|
3155
|
+
` Range: ${match.range.start.line}:${match.range.start.column} \u2192 ${match.range.end.line}:${match.range.end.column}`
|
|
3156
|
+
);
|
|
3157
|
+
if (match.symbol) {
|
|
3158
|
+
console.log(` Symbol: ${match.symbol}`);
|
|
3159
|
+
}
|
|
3160
|
+
if (match.source && match.source.length > 0) {
|
|
3161
|
+
console.log(` Source chain: ${match.source.length} step(s)`);
|
|
3162
|
+
match.source.forEach((src, srcIdx) => {
|
|
3163
|
+
const range = `${src.range.start.line}:${src.range.start.column} \u2192 ${src.range.end.line}:${src.range.end.column}`;
|
|
3164
|
+
console.log(
|
|
3165
|
+
` ${srcIdx + 1}. ${src.filePath} [${src.symbol || "N/A"}] @ ${range}`
|
|
3166
|
+
);
|
|
3167
|
+
});
|
|
3168
|
+
}
|
|
3169
|
+
console.log(` Text preview: ${(_a = match.text) == null ? void 0 : _a.substring(0, 120)}...`);
|
|
3170
|
+
});
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
});
|
|
3174
|
+
eventEmitter.on("test:start", (data) => {
|
|
3175
|
+
console.log(chalk.magenta(`
|
|
3176
|
+
\u{1F9EA} [${data.ruleId}] Running test: "${data.testName}"`));
|
|
3177
|
+
});
|
|
3178
|
+
eventEmitter.on("test:matches", (data) => {
|
|
3179
|
+
console.log(chalk.magenta(` Found ${data.matches.length} match(es):`));
|
|
3180
|
+
data.matches.forEach((match, idx) => {
|
|
3181
|
+
var _a;
|
|
3182
|
+
console.log(` Match ${idx + 1}:`);
|
|
3183
|
+
console.log(` File: ${match.filePath}`);
|
|
3184
|
+
console.log(
|
|
3185
|
+
` Range: ${match.range.start.line}:${match.range.start.column} \u2192 ${match.range.end.line}:${match.range.end.column}`
|
|
3186
|
+
);
|
|
3187
|
+
if (match.symbol) {
|
|
3188
|
+
console.log(` Symbol: ${match.symbol}`);
|
|
3189
|
+
}
|
|
3190
|
+
if (match.source && match.source.length > 0) {
|
|
3191
|
+
console.log(` Source chain: ${match.source.length} step(s)`);
|
|
3192
|
+
match.source.forEach((src, srcIdx) => {
|
|
3193
|
+
const range = `${src.range.start.line}:${src.range.start.column} \u2192 ${src.range.end.line}:${src.range.end.column}`;
|
|
3194
|
+
console.log(` ${srcIdx + 1}. ${src.filePath} [${src.symbol || "N/A"}] @ ${range}`);
|
|
3195
|
+
});
|
|
3196
|
+
}
|
|
3197
|
+
console.log(` Text preview: ${(_a = match.text) == null ? void 0 : _a.substring(0, 120)}...`);
|
|
3198
|
+
});
|
|
3199
|
+
});
|
|
3200
|
+
}
|
|
3201
|
+
return () => {
|
|
3202
|
+
if (spinner) {
|
|
3203
|
+
spinner.stop();
|
|
3204
|
+
spinner = null;
|
|
3205
|
+
}
|
|
3206
|
+
};
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
// src/utils/startupScreen.ts
|
|
3210
|
+
import readline from "readline";
|
|
3211
|
+
import chalk3 from "chalk";
|
|
3212
|
+
import ora2 from "ora";
|
|
3213
|
+
|
|
3214
|
+
// src/utils/asciiFrames.ts
|
|
3215
|
+
import chalk2 from "chalk";
|
|
3216
|
+
var BRAND_COLOR2 = "#fbbf24";
|
|
3217
|
+
var VIOLATION_COLOR2 = "#ff6b35";
|
|
3218
|
+
var SUGGESTION_COLOR2 = "#4a90e2";
|
|
3219
|
+
var SKIPPED_COLOR2 = "#9b59b6";
|
|
3220
|
+
var WISPBIT_FRAMES = [
|
|
3221
|
+
// Frame 1 - Normal position
|
|
3222
|
+
`
|
|
3223
|
+
${chalk2.hex(BRAND_COLOR2)(" \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557")}
|
|
3224
|
+
${chalk2.hex(BRAND_COLOR2)(" \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D")}
|
|
3225
|
+
${chalk2.hex(BRAND_COLOR2)(" \u2588\u2588\u2551 \u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551 ")}
|
|
3226
|
+
${chalk2.hex(BRAND_COLOR2)(" \u2588\u2588\u2551\u2588\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 ")}
|
|
3227
|
+
${chalk2.hex(BRAND_COLOR2)(" \u255A\u2588\u2588\u2588\u2554\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551 ")}
|
|
3228
|
+
${chalk2.hex(BRAND_COLOR2)(" \u255A\u2550\u2550\u255D\u255A\u2550\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D ")}
|
|
3229
|
+
`,
|
|
3230
|
+
// Frame 2 - Wave starts from left
|
|
3231
|
+
`
|
|
3232
|
+
${chalk2.hex(SUGGESTION_COLOR2)(" \u2588\u2588\u2557 ")}${chalk2.hex(BRAND_COLOR2)("\u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557")}
|
|
3233
|
+
${chalk2.hex(SUGGESTION_COLOR2)(" \u2588\u2588\u2551 ")}${chalk2.hex(BRAND_COLOR2)("\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D")}
|
|
3234
|
+
${chalk2.hex(SUGGESTION_COLOR2)(" \u2588\u2588\u2551 \u2588\u2557 ")}${chalk2.hex(BRAND_COLOR2)("\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551 ")}
|
|
3235
|
+
${chalk2.hex(SUGGESTION_COLOR2)(" \u2588\u2588\u2551\u2588\u2588\u2588\u2557")}${chalk2.hex(BRAND_COLOR2)("\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 ")}
|
|
3236
|
+
${chalk2.hex(SUGGESTION_COLOR2)(" \u255A\u2588\u2588\u2588\u2554\u2588\u2588")}${chalk2.hex(BRAND_COLOR2)("\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551 ")}
|
|
3237
|
+
${chalk2.hex(SUGGESTION_COLOR2)(" \u255A\u2550\u2550\u255D\u255A\u2550")}${chalk2.hex(BRAND_COLOR2)("\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D ")}
|
|
3238
|
+
`,
|
|
3239
|
+
// Frame 3 - Wave in middle
|
|
3240
|
+
`
|
|
3241
|
+
${chalk2.hex(BRAND_COLOR2)(" \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557")}${chalk2.hex(SKIPPED_COLOR2)("\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 ")}${chalk2.hex(BRAND_COLOR2)("\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557")}
|
|
3242
|
+
${chalk2.hex(BRAND_COLOR2)(" \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551")}${chalk2.hex(SKIPPED_COLOR2)("\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557")}${chalk2.hex(BRAND_COLOR2)("\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D")}
|
|
3243
|
+
${chalk2.hex(BRAND_COLOR2)(" \u2588\u2588\u2551 \u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551")}${chalk2.hex(SKIPPED_COLOR2)("\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D")}${chalk2.hex(BRAND_COLOR2)("\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551 ")}
|
|
3244
|
+
${chalk2.hex(BRAND_COLOR2)(" \u2588\u2588\u2551\u2588\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551")}${chalk2.hex(SKIPPED_COLOR2)("\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u255D ")}${chalk2.hex(BRAND_COLOR2)("\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 ")}
|
|
3245
|
+
${chalk2.hex(BRAND_COLOR2)(" \u255A\u2588\u2588\u2588\u2554\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551")}${chalk2.hex(SKIPPED_COLOR2)("\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 ")}${chalk2.hex(BRAND_COLOR2)("\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551 ")}
|
|
3246
|
+
${chalk2.hex(BRAND_COLOR2)(" \u255A\u2550\u2550\u255D\u255A\u2550\u2550\u255D \u255A\u2550\u255D")}${chalk2.hex(SKIPPED_COLOR2)("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D ")}${chalk2.hex(BRAND_COLOR2)("\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D ")}
|
|
3247
|
+
`,
|
|
3248
|
+
// Frame 4 - Wave towards right
|
|
3249
|
+
`
|
|
3250
|
+
${chalk2.hex(BRAND_COLOR2)(" \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 ")}${chalk2.hex(VIOLATION_COLOR2)("\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557")}
|
|
3251
|
+
${chalk2.hex(BRAND_COLOR2)(" \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557")}${chalk2.hex(VIOLATION_COLOR2)("\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D")}
|
|
3252
|
+
${chalk2.hex(BRAND_COLOR2)(" \u2588\u2588\u2551 \u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D")}${chalk2.hex(VIOLATION_COLOR2)("\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551 ")}
|
|
3253
|
+
${chalk2.hex(BRAND_COLOR2)(" \u2588\u2588\u2551\u2588\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u255D ")}${chalk2.hex(VIOLATION_COLOR2)("\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 ")}
|
|
3254
|
+
${chalk2.hex(BRAND_COLOR2)(" \u255A\u2588\u2588\u2588\u2554\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 ")}${chalk2.hex(VIOLATION_COLOR2)("\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551 ")}
|
|
3255
|
+
${chalk2.hex(BRAND_COLOR2)(" \u255A\u2550\u2550\u255D\u255A\u2550\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D ")}${chalk2.hex(VIOLATION_COLOR2)("\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D ")}
|
|
3256
|
+
`,
|
|
3257
|
+
// Frame 5 - Wave at end
|
|
3258
|
+
`
|
|
3259
|
+
${chalk2.hex(BRAND_COLOR2)(" \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557")}${chalk2.hex(SUGGESTION_COLOR2)("\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557")}
|
|
3260
|
+
${chalk2.hex(BRAND_COLOR2)(" \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551")}${chalk2.hex(SUGGESTION_COLOR2)("\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D")}
|
|
3261
|
+
${chalk2.hex(BRAND_COLOR2)(" \u2588\u2588\u2551 \u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551")}${chalk2.hex(SUGGESTION_COLOR2)(" \u2588\u2588\u2551 ")}
|
|
3262
|
+
${chalk2.hex(BRAND_COLOR2)(" \u2588\u2588\u2551\u2588\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551")}${chalk2.hex(SUGGESTION_COLOR2)(" \u2588\u2588\u2551 ")}
|
|
3263
|
+
${chalk2.hex(BRAND_COLOR2)(" \u255A\u2588\u2588\u2588\u2554\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551")}${chalk2.hex(SUGGESTION_COLOR2)(" \u2588\u2588\u2551 ")}
|
|
3264
|
+
${chalk2.hex(BRAND_COLOR2)(" \u255A\u2550\u2550\u255D\u255A\u2550\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D")}${chalk2.hex(SUGGESTION_COLOR2)(" \u255A\u2550\u255D ")}
|
|
3265
|
+
`
|
|
3266
|
+
];
|
|
3267
|
+
|
|
3268
|
+
// src/utils/startupScreen.ts
|
|
3269
|
+
function clearScreen() {
|
|
3270
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
3271
|
+
}
|
|
3272
|
+
function sleep(ms) {
|
|
3273
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
3274
|
+
}
|
|
3275
|
+
async function showAnimatedFrames(durationMs = 3e3) {
|
|
3276
|
+
const spinner = ora2({
|
|
3277
|
+
text: "",
|
|
3278
|
+
spinner: {
|
|
3279
|
+
interval: 100,
|
|
3280
|
+
// ms per frame
|
|
3281
|
+
frames: WISPBIT_FRAMES
|
|
3282
|
+
}
|
|
3283
|
+
}).start();
|
|
3284
|
+
await sleep(durationMs);
|
|
3285
|
+
spinner.stop();
|
|
3286
|
+
}
|
|
3287
|
+
async function promptForInput(question) {
|
|
3288
|
+
const rl = readline.createInterface({
|
|
3289
|
+
input: process.stdin,
|
|
3290
|
+
output: process.stdout
|
|
3291
|
+
});
|
|
3292
|
+
return await new Promise((resolve3) => {
|
|
3293
|
+
rl.question(question, (answer) => {
|
|
3294
|
+
rl.close();
|
|
3295
|
+
resolve3(answer.trim());
|
|
3296
|
+
});
|
|
3297
|
+
});
|
|
3298
|
+
}
|
|
3299
|
+
async function showStartupScreen() {
|
|
3300
|
+
clearScreen();
|
|
3301
|
+
await showAnimatedFrames(1500);
|
|
3302
|
+
console.log(WISPBIT_FRAMES[0]);
|
|
3303
|
+
console.log(chalk3.hex(VIOLATION_COLOR)("\n Welcome to wispbit"));
|
|
3304
|
+
console.log(chalk3(" The linter for AI\n"));
|
|
3305
|
+
console.log(chalk3.dim(" To use wispbit, you need an API key."));
|
|
3306
|
+
console.log(chalk3.dim(" You can get one at: https://app.wispbit.com/api-keys\n"));
|
|
3307
|
+
const apiKey = await promptForInput(
|
|
3308
|
+
chalk3.bold.hex(SUGGESTION_COLOR)(" Enter your Wispbit API key (or press Enter to exit): ")
|
|
3309
|
+
);
|
|
3310
|
+
if (!apiKey) {
|
|
3311
|
+
console.log(chalk3.dim("\n Setup cancelled."));
|
|
3312
|
+
return null;
|
|
3313
|
+
}
|
|
3314
|
+
return apiKey;
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3317
|
+
// src/cli.ts
|
|
3318
|
+
dotenv.config();
|
|
3319
|
+
function getConfigFilePath() {
|
|
3320
|
+
return path10.join(os3.homedir(), ".powerlint", "config.json");
|
|
3321
|
+
}
|
|
3322
|
+
async function saveApiKey(apiKey) {
|
|
3323
|
+
const configPath = getConfigFilePath();
|
|
3324
|
+
const configDir = path10.dirname(configPath);
|
|
3325
|
+
await fs5.mkdir(configDir, { recursive: true });
|
|
3326
|
+
let existingConfig = {};
|
|
3327
|
+
try {
|
|
3328
|
+
const configContent = await fs5.readFile(configPath, "utf-8");
|
|
3329
|
+
existingConfig = JSON.parse(configContent);
|
|
3330
|
+
} catch {
|
|
3331
|
+
}
|
|
3332
|
+
const newConfig = {
|
|
3333
|
+
...existingConfig,
|
|
3334
|
+
apiKey
|
|
3335
|
+
};
|
|
3336
|
+
await fs5.writeFile(configPath, JSON.stringify(newConfig, null, 2));
|
|
3337
|
+
}
|
|
3338
|
+
async function loadApiKey() {
|
|
3339
|
+
const configPath = getConfigFilePath();
|
|
3340
|
+
try {
|
|
3341
|
+
const configContent = await fs5.readFile(configPath, "utf-8");
|
|
3342
|
+
const config = JSON.parse(configContent);
|
|
3343
|
+
return config.apiKey || null;
|
|
3344
|
+
} catch {
|
|
3345
|
+
return null;
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
async function ensureConfigured() {
|
|
3349
|
+
const environment = new Environment();
|
|
3350
|
+
let apiKey = process.env.WISPBIT_API_KEY || null;
|
|
3351
|
+
if (!apiKey) {
|
|
3352
|
+
apiKey = await loadApiKey();
|
|
3353
|
+
}
|
|
3354
|
+
let config = await Config.initialize(environment, { apiKey: apiKey || void 0 });
|
|
3355
|
+
if ("failed" in config) {
|
|
3356
|
+
if (config.error === "INVALID_API_KEY") {
|
|
3357
|
+
const newApiKey = await showStartupScreen();
|
|
3358
|
+
if (!newApiKey) {
|
|
3359
|
+
process.exit(0);
|
|
3360
|
+
}
|
|
3361
|
+
const repositoryUrl = await environment.getRepositoryUrl();
|
|
3362
|
+
if (!repositoryUrl) {
|
|
3363
|
+
console.log(chalk4.red("Repository URL not found. Make sure you're in a git repository."));
|
|
3364
|
+
process.exit(1);
|
|
3365
|
+
}
|
|
3366
|
+
console.log(chalk4.dim("Validating API key..."));
|
|
3367
|
+
await saveApiKey(newApiKey);
|
|
3368
|
+
config = await Config.initialize(environment, { apiKey: newApiKey });
|
|
3369
|
+
if ("failed" in config) {
|
|
3370
|
+
console.log(chalk4.red("Invalid API key. Please check your API key and try again."));
|
|
3371
|
+
process.exit(1);
|
|
3372
|
+
} else {
|
|
3373
|
+
console.log(chalk4.green("Setup complete! powerlint has been configured."));
|
|
3374
|
+
return config;
|
|
3375
|
+
}
|
|
3376
|
+
} else if (config.error === "INVALID_REPOSITORY") {
|
|
3377
|
+
const repositoryUrl = await environment.getRepositoryUrl();
|
|
3378
|
+
console.log(
|
|
3379
|
+
chalk4.red(
|
|
3380
|
+
`No repository in wispbit found for url: ${repositoryUrl}. If your git remote URL was recently modified, use "git remote set-url origin <new-url>" to update the remote URL.`
|
|
3381
|
+
)
|
|
3382
|
+
);
|
|
3383
|
+
process.exit(1);
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
return config;
|
|
3387
|
+
}
|
|
3388
|
+
async function checkForUpdates() {
|
|
3389
|
+
const currentVersion = getCurrentVersion();
|
|
3390
|
+
const latestCliVersion = await getLatestVersion();
|
|
3391
|
+
if (semver.gt(latestCliVersion, currentVersion)) {
|
|
3392
|
+
console.log(
|
|
3393
|
+
chalk4.bgHex("#b2f5ea").black.bold(` NEW VERSION AVAILABLE: ${latestCliVersion} (current: ${currentVersion}) `)
|
|
3394
|
+
);
|
|
3395
|
+
console.log(
|
|
3396
|
+
chalk4.bgHex("#b2f5ea").black.bold(` Run 'pnpm install -g @wispbit/cli' to update
|
|
3397
|
+
`)
|
|
3398
|
+
);
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
var cli = meow(
|
|
3402
|
+
`
|
|
3403
|
+
wispbit - the linter for AI
|
|
3404
|
+
https://wispbit.com/
|
|
3405
|
+
|
|
3406
|
+
Usage:
|
|
3407
|
+
$ wispbit [diff-options] (run diff by default)
|
|
3408
|
+
$ wispbit check [file-path/directory] [check-options]
|
|
3409
|
+
$ wispbit diff [diff-options]
|
|
3410
|
+
$ wispbit list
|
|
3411
|
+
$ wispbit cache purge
|
|
3412
|
+
|
|
3413
|
+
Commands:
|
|
3414
|
+
check [file-path/directory] Run linting rules against a specific file/folder or entire codebase
|
|
3415
|
+
diff Run linting rules only on changed files in the current PR
|
|
3416
|
+
list List all available rules with their ID, message, and severity
|
|
3417
|
+
cache purge Purge the cache directory (indexes, caching, etc.)
|
|
3418
|
+
|
|
3419
|
+
Options for check:
|
|
3420
|
+
--rule <ruleId> Optional rule ID to run specific rule
|
|
3421
|
+
--json [format] Output in JSON format (pretty, stream, compact)
|
|
3422
|
+
-d, --debug Enable debug output
|
|
3423
|
+
|
|
3424
|
+
Options for diff:
|
|
3425
|
+
--rule <ruleId> Optional rule ID to run specific rule
|
|
3426
|
+
--json [format] Output in JSON format (pretty, stream, compact)
|
|
3427
|
+
--base <commit> Base commit/branch/SHA to compare against (defaults to origin/main)
|
|
3428
|
+
--head <commit> Head commit/branch/SHA to compare against (defaults to current branch)
|
|
3429
|
+
-d, --debug Enable debug output
|
|
3430
|
+
|
|
3431
|
+
|
|
3432
|
+
Global options:
|
|
3433
|
+
-v, --version Show version number
|
|
3434
|
+
-h, --help Show help
|
|
3435
|
+
`,
|
|
3436
|
+
{
|
|
3437
|
+
importMeta: import.meta,
|
|
3438
|
+
flags: {
|
|
3439
|
+
// Scan/diff options
|
|
3440
|
+
rule: {
|
|
3441
|
+
type: "string"
|
|
3442
|
+
},
|
|
3443
|
+
json: {
|
|
3444
|
+
type: "string"
|
|
3445
|
+
},
|
|
3446
|
+
base: {
|
|
3447
|
+
type: "string"
|
|
3448
|
+
},
|
|
3449
|
+
head: {
|
|
3450
|
+
type: "string"
|
|
3451
|
+
},
|
|
3452
|
+
// Debug option
|
|
3453
|
+
debug: {
|
|
3454
|
+
type: "boolean",
|
|
3455
|
+
shortFlag: "d",
|
|
3456
|
+
default: false
|
|
3457
|
+
},
|
|
3458
|
+
// Global options
|
|
3459
|
+
version: {
|
|
3460
|
+
type: "boolean",
|
|
3461
|
+
shortFlag: "v"
|
|
3462
|
+
},
|
|
3463
|
+
help: {
|
|
3464
|
+
type: "boolean",
|
|
3465
|
+
shortFlag: "h"
|
|
3466
|
+
}
|
|
3467
|
+
},
|
|
3468
|
+
version: getCurrentVersion()
|
|
3469
|
+
}
|
|
3470
|
+
);
|
|
3471
|
+
async function executeCommand(options) {
|
|
3472
|
+
const environment = new Environment();
|
|
3473
|
+
const config = await ensureConfigured();
|
|
3474
|
+
const { ruleId, json: json2, mode, filePath } = options;
|
|
3475
|
+
let jsonOutput = false;
|
|
3476
|
+
let jsonFormat = "pretty";
|
|
3477
|
+
if (json2) {
|
|
3478
|
+
jsonOutput = true;
|
|
3479
|
+
if (json2 === "stream") {
|
|
3480
|
+
jsonFormat = "stream";
|
|
3481
|
+
} else if (json2 === "compact") {
|
|
3482
|
+
jsonFormat = "compact";
|
|
3483
|
+
} else {
|
|
3484
|
+
jsonFormat = "pretty";
|
|
3485
|
+
}
|
|
3486
|
+
} else {
|
|
3487
|
+
await checkForUpdates();
|
|
3488
|
+
}
|
|
3489
|
+
let rules;
|
|
3490
|
+
try {
|
|
3491
|
+
const ruleProvider = new WispbitRuleProvider(config, environment);
|
|
3492
|
+
if (ruleId) {
|
|
3493
|
+
const rule = await ruleProvider.loadRuleById(ruleId);
|
|
3494
|
+
rules = [rule];
|
|
3495
|
+
} else {
|
|
3496
|
+
rules = await ruleProvider.loadAllRules();
|
|
3497
|
+
}
|
|
3498
|
+
} catch (error) {
|
|
3499
|
+
if (error instanceof InvalidRuleFormatError) {
|
|
3500
|
+
console.error(chalk4.red("Rule Validation Error:"), error.message);
|
|
3501
|
+
if (error.validationErrors && error.validationErrors.length > 0) {
|
|
3502
|
+
console.error(chalk4.red("Validation errors:"));
|
|
3503
|
+
error.validationErrors.forEach((err) => {
|
|
3504
|
+
console.error(chalk4.red(" \u2022"), err);
|
|
3505
|
+
});
|
|
3506
|
+
}
|
|
3507
|
+
process.exit(1);
|
|
3508
|
+
}
|
|
3509
|
+
throw error;
|
|
3510
|
+
}
|
|
3511
|
+
if (!json2) {
|
|
3512
|
+
if (mode === "check") {
|
|
3513
|
+
const modeBox = chalk4.bgHex("#eab308").black(" CHECK ");
|
|
3514
|
+
const targetInfo = chalk4.dim(` (${filePath || "all files"})`);
|
|
3515
|
+
console.log(`${modeBox}${targetInfo}`);
|
|
3516
|
+
} else {
|
|
3517
|
+
const baseCommit = cli.flags.base || "origin/main";
|
|
3518
|
+
const headCommit = cli.flags.head || "HEAD";
|
|
3519
|
+
const modeBox = chalk4.bgHex("#4a90e2").black(" DIFF ");
|
|
3520
|
+
const branchInfo = chalk4.dim(` (${headCommit}..${baseCommit})`);
|
|
3521
|
+
console.log(`${modeBox}${branchInfo}`);
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
const eventEmitter = new ExecutionEventEmitter();
|
|
3525
|
+
if (mode === "check") {
|
|
3526
|
+
eventEmitter.setExecutionMode("check", { filePath });
|
|
3527
|
+
} else {
|
|
3528
|
+
const baseCommit = cli.flags.base || "origin/main";
|
|
3529
|
+
const headCommit = cli.flags.head || "current branch";
|
|
3530
|
+
eventEmitter.setExecutionMode("diff", { baseCommit, headCommit });
|
|
3531
|
+
}
|
|
3532
|
+
const cleanupTerminalReporter = !options.json ? setupTerminalReporter(eventEmitter, options.debug || false) : void 0;
|
|
3533
|
+
const executionStartTime = Date.now();
|
|
3534
|
+
const ruleExecutor = new RuleExecutor(config, environment, eventEmitter);
|
|
3535
|
+
let totalFiles = 0;
|
|
3536
|
+
eventEmitter.on("files:discovery:complete", (data) => {
|
|
3537
|
+
totalFiles = data.totalFiles;
|
|
3538
|
+
});
|
|
3539
|
+
const ruleResults = await ruleExecutor.execute(rules, {
|
|
3540
|
+
mode,
|
|
3541
|
+
filePath,
|
|
3542
|
+
baseSha: cli.flags.base
|
|
3543
|
+
});
|
|
3544
|
+
const results = [];
|
|
3545
|
+
for (const ruleResult of ruleResults) {
|
|
3546
|
+
const rule = rules.find((r) => r.id === ruleResult.ruleId);
|
|
3547
|
+
if (rule) {
|
|
3548
|
+
let llmCost;
|
|
3549
|
+
let llmTokens;
|
|
3550
|
+
const result = {
|
|
3551
|
+
ruleId: ruleResult.ruleId,
|
|
3552
|
+
internalId: rule.internalId,
|
|
3553
|
+
message: rule.config.message,
|
|
3554
|
+
severity: rule.config.severity,
|
|
3555
|
+
matches: ruleResult.matches,
|
|
3556
|
+
llmCost,
|
|
3557
|
+
llmTokens
|
|
3558
|
+
};
|
|
3559
|
+
results.push(result);
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3562
|
+
cleanupTerminalReporter == null ? void 0 : cleanupTerminalReporter();
|
|
3563
|
+
if (jsonOutput) {
|
|
3564
|
+
outputJSON(results, jsonFormat);
|
|
3565
|
+
} else {
|
|
3566
|
+
const violationResults = results.filter((r) => r.severity === "violation");
|
|
3567
|
+
const suggestionResults = results.filter((r) => r.severity === "suggestion");
|
|
3568
|
+
const violationCount = violationResults.reduce((sum, r) => sum + r.matches.length, 0);
|
|
3569
|
+
const suggestionCount = suggestionResults.reduce((sum, r) => sum + r.matches.length, 0);
|
|
3570
|
+
const totalMatches = violationCount + suggestionCount;
|
|
3571
|
+
const totalLLMCost = results.filter((r) => r.llmCost).reduce((sum, r) => sum.plus(new Big(r.llmCost || 0)), new Big(0));
|
|
3572
|
+
const totalLLMTokens = results.filter((r) => r.llmTokens).reduce((sum, r) => sum + (r.llmTokens || 0), 0);
|
|
3573
|
+
let executionModeInfo;
|
|
3574
|
+
if (mode === "check") {
|
|
3575
|
+
executionModeInfo = { mode: "check", filePath };
|
|
3576
|
+
} else {
|
|
3577
|
+
const baseCommit = cli.flags.base || "origin/main";
|
|
3578
|
+
const headCommit = cli.flags.head || "current branch";
|
|
3579
|
+
executionModeInfo = { mode: "diff", baseCommit, headCommit };
|
|
3580
|
+
}
|
|
3581
|
+
const executionTime = Date.now() - executionStartTime;
|
|
3582
|
+
printSummary(results, {
|
|
3583
|
+
totalRules: rules.length,
|
|
3584
|
+
violationCount,
|
|
3585
|
+
suggestionCount,
|
|
3586
|
+
totalMatches,
|
|
3587
|
+
totalLLMCost: totalLLMCost.toString(),
|
|
3588
|
+
totalLLMTokens,
|
|
3589
|
+
executionMode: executionModeInfo,
|
|
3590
|
+
executionTime,
|
|
3591
|
+
totalFiles
|
|
3592
|
+
});
|
|
3593
|
+
}
|
|
3594
|
+
return results;
|
|
3595
|
+
}
|
|
3596
|
+
async function listRules() {
|
|
3597
|
+
const config = await ensureConfigured();
|
|
3598
|
+
const environment = new Environment();
|
|
3599
|
+
const ruleProvider = new WispbitRuleProvider(config, environment);
|
|
3600
|
+
const rules = await ruleProvider.loadAllRules();
|
|
3601
|
+
printRulesList(rules);
|
|
3602
|
+
}
|
|
3603
|
+
async function main() {
|
|
3604
|
+
const command = cli.input[0];
|
|
3605
|
+
const subcommand = cli.input[1];
|
|
3606
|
+
switch (command) {
|
|
3607
|
+
case "check": {
|
|
3608
|
+
const filePath = cli.input[1];
|
|
3609
|
+
await executeCommand({
|
|
3610
|
+
ruleId: cli.flags.rule,
|
|
3611
|
+
json: cli.flags.json,
|
|
3612
|
+
debug: cli.flags.debug,
|
|
3613
|
+
mode: "check",
|
|
3614
|
+
filePath
|
|
3615
|
+
});
|
|
3616
|
+
break;
|
|
3617
|
+
}
|
|
3618
|
+
case "diff": {
|
|
3619
|
+
await executeCommand({
|
|
3620
|
+
ruleId: cli.flags.rule,
|
|
3621
|
+
json: cli.flags.json,
|
|
3622
|
+
debug: cli.flags.debug,
|
|
3623
|
+
mode: "diff"
|
|
3624
|
+
});
|
|
3625
|
+
break;
|
|
3626
|
+
}
|
|
3627
|
+
case "list": {
|
|
3628
|
+
await listRules();
|
|
3629
|
+
break;
|
|
3630
|
+
}
|
|
3631
|
+
case "cache": {
|
|
3632
|
+
if (subcommand === "purge") {
|
|
3633
|
+
const environment = new Environment();
|
|
3634
|
+
const storage = new Storage(environment);
|
|
3635
|
+
const result = await storage.purgeCache();
|
|
3636
|
+
console.log(
|
|
3637
|
+
`
|
|
3638
|
+
${chalk4.green("Cache purged successfully.")} Removed ${result.deletedCount} item(s).`
|
|
3639
|
+
);
|
|
3640
|
+
} else {
|
|
3641
|
+
console.error(chalk4.red("Unknown cache subcommand:"), subcommand);
|
|
3642
|
+
cli.showHelp();
|
|
3643
|
+
process.exit(1);
|
|
3644
|
+
}
|
|
3645
|
+
break;
|
|
3646
|
+
}
|
|
3647
|
+
default: {
|
|
3648
|
+
if (!command) {
|
|
3649
|
+
await executeCommand({
|
|
3650
|
+
ruleId: cli.flags.rule,
|
|
3651
|
+
json: cli.flags.json,
|
|
3652
|
+
debug: cli.flags.debug,
|
|
3653
|
+
mode: "diff"
|
|
3654
|
+
});
|
|
3655
|
+
} else {
|
|
3656
|
+
console.error(chalk4.red("Unknown command:"), command);
|
|
3657
|
+
cli.showHelp();
|
|
3658
|
+
process.exit(1);
|
|
3659
|
+
}
|
|
3660
|
+
break;
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
main();
|
|
3665
|
+
export {
|
|
3666
|
+
checkForUpdates
|
|
3667
|
+
};
|
|
3668
|
+
//# sourceMappingURL=cli.js.map
|