@wispbit/local 1.0.25 → 1.0.27
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/cli.js +1190 -638
- package/dist/cli.js.map +4 -4
- package/dist/index.js +10 -3857
- package/dist/index.js.map +4 -4
- package/dist/package.json +3 -2
- package/dist/src/api/WispbitApiClient.d.ts +185 -0
- package/dist/src/api/WispbitApiClient.d.ts.map +1 -0
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/environment/Config.d.ts +15 -5
- package/dist/src/environment/Config.d.ts.map +1 -1
- package/dist/src/providers/ViolationValidationProvider.d.ts +7 -11
- package/dist/src/providers/ViolationValidationProvider.d.ts.map +1 -1
- package/dist/src/providers/WispbitRuleProvider.d.ts +0 -16
- package/dist/src/providers/WispbitRuleProvider.d.ts.map +1 -1
- package/dist/src/providers/WispbitViolationValidationProvider.d.ts +4 -7
- package/dist/src/providers/WispbitViolationValidationProvider.d.ts.map +1 -1
- package/dist/src/schemas.d.ts +13 -514
- package/dist/src/schemas.d.ts.map +1 -1
- package/dist/src/steps/ExecutionEventEmitter.d.ts +37 -2
- package/dist/src/steps/ExecutionEventEmitter.d.ts.map +1 -1
- package/dist/src/steps/FileExecutionContext.d.ts +14 -5
- package/dist/src/steps/FileExecutionContext.d.ts.map +1 -1
- package/dist/src/steps/FileFilterStep.d.ts +0 -7
- package/dist/src/steps/FileFilterStep.d.ts.map +1 -1
- package/dist/src/steps/GotoDefinitionStep.d.ts.map +1 -1
- package/dist/src/steps/LLMStep.d.ts +0 -10
- package/dist/src/steps/LLMStep.d.ts.map +1 -1
- package/dist/src/steps/RuleExecutor.d.ts +5 -1
- package/dist/src/steps/RuleExecutor.d.ts.map +1 -1
- package/dist/src/test/TestExecutor.d.ts +6 -1
- package/dist/src/test/TestExecutor.d.ts.map +1 -1
- package/dist/src/types.d.ts +1 -5
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils/debugLogger.d.ts +6 -0
- package/dist/src/utils/debugLogger.d.ts.map +1 -0
- package/dist/src/utils/formatters.d.ts.map +1 -1
- package/dist/src/utils/git.d.ts +31 -2
- package/dist/src/utils/git.d.ts.map +1 -1
- package/dist/src/utils/git.test.d.ts +2 -0
- package/dist/src/utils/git.test.d.ts.map +1 -0
- package/dist/src/utils/patternMatching.d.ts +2 -0
- package/dist/src/utils/patternMatching.d.ts.map +1 -0
- package/dist/src/utils/validateRule.d.ts +37 -1
- package/dist/src/utils/validateRule.d.ts.map +1 -1
- package/dist/src/validationSchemas.d.ts +553 -0
- package/dist/src/validationSchemas.d.ts.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -4
package/dist/cli.js
CHANGED
|
@@ -10,6 +10,340 @@ import dotenv from "dotenv";
|
|
|
10
10
|
import meow from "meow";
|
|
11
11
|
import semver from "semver";
|
|
12
12
|
|
|
13
|
+
// src/api/WispbitApiClient.ts
|
|
14
|
+
import pRetry from "p-retry";
|
|
15
|
+
import { z as z2 } from "zod";
|
|
16
|
+
|
|
17
|
+
// src/validationSchemas.ts
|
|
18
|
+
import { z } from "zod";
|
|
19
|
+
|
|
20
|
+
// src/languages.ts
|
|
21
|
+
import { existsSync } from "fs";
|
|
22
|
+
import { createRequire } from "module";
|
|
23
|
+
import path from "path";
|
|
24
|
+
import angular from "@ast-grep/lang-angular";
|
|
25
|
+
import bash from "@ast-grep/lang-bash";
|
|
26
|
+
import c from "@ast-grep/lang-c";
|
|
27
|
+
import cpp from "@ast-grep/lang-cpp";
|
|
28
|
+
import csharp from "@ast-grep/lang-csharp";
|
|
29
|
+
import css from "@ast-grep/lang-css";
|
|
30
|
+
import dart from "@ast-grep/lang-dart";
|
|
31
|
+
import elixir from "@ast-grep/lang-elixir";
|
|
32
|
+
import go from "@ast-grep/lang-go";
|
|
33
|
+
import haskell from "@ast-grep/lang-haskell";
|
|
34
|
+
import html from "@ast-grep/lang-html";
|
|
35
|
+
import java from "@ast-grep/lang-java";
|
|
36
|
+
import javascript from "@ast-grep/lang-javascript";
|
|
37
|
+
import json from "@ast-grep/lang-json";
|
|
38
|
+
import kotlin from "@ast-grep/lang-kotlin";
|
|
39
|
+
import lua from "@ast-grep/lang-lua";
|
|
40
|
+
import markdown from "@ast-grep/lang-markdown";
|
|
41
|
+
import php from "@ast-grep/lang-php";
|
|
42
|
+
import python from "@ast-grep/lang-python";
|
|
43
|
+
import ruby from "@ast-grep/lang-ruby";
|
|
44
|
+
import rust from "@ast-grep/lang-rust";
|
|
45
|
+
import scala from "@ast-grep/lang-scala";
|
|
46
|
+
import sql from "@ast-grep/lang-sql";
|
|
47
|
+
import swift from "@ast-grep/lang-swift";
|
|
48
|
+
import toml from "@ast-grep/lang-toml";
|
|
49
|
+
import tsx from "@ast-grep/lang-tsx";
|
|
50
|
+
import typescript from "@ast-grep/lang-typescript";
|
|
51
|
+
import yaml from "@ast-grep/lang-yaml";
|
|
52
|
+
import { registerDynamicLanguage } from "@ast-grep/napi";
|
|
53
|
+
console.debug = () => {
|
|
54
|
+
};
|
|
55
|
+
var Language = /* @__PURE__ */ ((Language2) => {
|
|
56
|
+
Language2["Angular"] = "Angular";
|
|
57
|
+
Language2["Bash"] = "Bash";
|
|
58
|
+
Language2["C"] = "C";
|
|
59
|
+
Language2["Cpp"] = "Cpp";
|
|
60
|
+
Language2["Csharp"] = "Csharp";
|
|
61
|
+
Language2["Css"] = "Css";
|
|
62
|
+
Language2["Dart"] = "Dart";
|
|
63
|
+
Language2["Elixir"] = "Elixir";
|
|
64
|
+
Language2["Go"] = "Go";
|
|
65
|
+
Language2["Haskell"] = "Haskell";
|
|
66
|
+
Language2["Html"] = "Html";
|
|
67
|
+
Language2["Java"] = "Java";
|
|
68
|
+
Language2["JavaScript"] = "JavaScript";
|
|
69
|
+
Language2["Json"] = "Json";
|
|
70
|
+
Language2["Kotlin"] = "Kotlin";
|
|
71
|
+
Language2["Lua"] = "Lua";
|
|
72
|
+
Language2["Markdown"] = "Markdown";
|
|
73
|
+
Language2["Php"] = "Php";
|
|
74
|
+
Language2["Python"] = "Python";
|
|
75
|
+
Language2["Ruby"] = "Ruby";
|
|
76
|
+
Language2["Rust"] = "Rust";
|
|
77
|
+
Language2["Scala"] = "Scala";
|
|
78
|
+
Language2["Sql"] = "Sql";
|
|
79
|
+
Language2["Swift"] = "Swift";
|
|
80
|
+
Language2["Toml"] = "Toml";
|
|
81
|
+
Language2["Tsx"] = "Tsx";
|
|
82
|
+
Language2["TypeScript"] = "TypeScript";
|
|
83
|
+
Language2["Yaml"] = "Yaml";
|
|
84
|
+
Language2["GraphQL"] = "GraphQL";
|
|
85
|
+
Language2["Unknown"] = "Unknown";
|
|
86
|
+
return Language2;
|
|
87
|
+
})(Language || {});
|
|
88
|
+
var require2 = createRequire(import.meta.url ? import.meta.url : __filename);
|
|
89
|
+
function getGraphQLLibPath() {
|
|
90
|
+
const graphqlDir = path.dirname(require2.resolve("tree-sitter-graphql"));
|
|
91
|
+
const releaseNode = path.join(graphqlDir, "../../build/Release/tree_sitter_graphql_binding.node");
|
|
92
|
+
if (existsSync(releaseNode)) {
|
|
93
|
+
return releaseNode;
|
|
94
|
+
}
|
|
95
|
+
const debugNode = path.join(graphqlDir, "../../build/Debug/tree_sitter_graphql_binding.node");
|
|
96
|
+
if (existsSync(debugNode)) {
|
|
97
|
+
return debugNode;
|
|
98
|
+
}
|
|
99
|
+
const soFile = path.join(graphqlDir, "parser.so");
|
|
100
|
+
if (existsSync(soFile)) {
|
|
101
|
+
return soFile;
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
var graphqlPath = getGraphQLLibPath();
|
|
106
|
+
var graphql = {
|
|
107
|
+
// node-gyp-build puts the .node file in build/Release/
|
|
108
|
+
libraryPath: graphqlPath,
|
|
109
|
+
/** the file extensions of the language. e.g. mojo */
|
|
110
|
+
extensions: ["graphql"],
|
|
111
|
+
/** the dylib symbol to load ts-language, default is `tree_sitter_{name}` */
|
|
112
|
+
languageSymbol: "tree_sitter_graphql",
|
|
113
|
+
/** the meta variable leading character, default is $ */
|
|
114
|
+
metaVarChar: "$",
|
|
115
|
+
/**
|
|
116
|
+
* An optional char to replace $ in your pattern.
|
|
117
|
+
* See https://ast-grep.github.io/advanced/custom-language.html#register-language-in-sgconfig-yml
|
|
118
|
+
*/
|
|
119
|
+
expandoChar: void 0
|
|
120
|
+
};
|
|
121
|
+
var registeredLanguages = {
|
|
122
|
+
["Angular" /* Angular */]: angular,
|
|
123
|
+
["Bash" /* Bash */]: bash,
|
|
124
|
+
["C" /* C */]: c,
|
|
125
|
+
["Cpp" /* Cpp */]: cpp,
|
|
126
|
+
["Csharp" /* Csharp */]: csharp,
|
|
127
|
+
["Css" /* Css */]: css,
|
|
128
|
+
["Dart" /* Dart */]: dart,
|
|
129
|
+
["Elixir" /* Elixir */]: elixir,
|
|
130
|
+
["Go" /* Go */]: go,
|
|
131
|
+
["Haskell" /* Haskell */]: haskell,
|
|
132
|
+
["Html" /* Html */]: html,
|
|
133
|
+
["Java" /* Java */]: java,
|
|
134
|
+
["JavaScript" /* JavaScript */]: javascript,
|
|
135
|
+
["Json" /* Json */]: json,
|
|
136
|
+
["Kotlin" /* Kotlin */]: kotlin,
|
|
137
|
+
["Lua" /* Lua */]: lua,
|
|
138
|
+
["Markdown" /* Markdown */]: markdown,
|
|
139
|
+
["Php" /* Php */]: php,
|
|
140
|
+
["Python" /* Python */]: python,
|
|
141
|
+
["Ruby" /* Ruby */]: ruby,
|
|
142
|
+
["Rust" /* Rust */]: rust,
|
|
143
|
+
["Scala" /* Scala */]: scala,
|
|
144
|
+
["Sql" /* Sql */]: sql,
|
|
145
|
+
["Swift" /* Swift */]: swift,
|
|
146
|
+
["Toml" /* Toml */]: toml,
|
|
147
|
+
["Tsx" /* Tsx */]: tsx,
|
|
148
|
+
["TypeScript" /* TypeScript */]: typescript,
|
|
149
|
+
["Yaml" /* Yaml */]: yaml,
|
|
150
|
+
...graphqlPath ? { ["GraphQL" /* GraphQL */]: graphql } : {}
|
|
151
|
+
};
|
|
152
|
+
registerDynamicLanguage(registeredLanguages);
|
|
153
|
+
var REGISTERED_LANGUAGE_EXTENSIONS = Object.entries(
|
|
154
|
+
registeredLanguages
|
|
155
|
+
).reduce(
|
|
156
|
+
(acc, [language, registration]) => {
|
|
157
|
+
acc[language] = registration.extensions ?? [];
|
|
158
|
+
return acc;
|
|
159
|
+
},
|
|
160
|
+
{}
|
|
161
|
+
);
|
|
162
|
+
function getLanguageFromFilePath(filePath) {
|
|
163
|
+
const extension = path.extname(filePath).slice(1);
|
|
164
|
+
const language = findRegisteredLanguageFromExtension(extension);
|
|
165
|
+
return language ?? "Unknown" /* Unknown */;
|
|
166
|
+
}
|
|
167
|
+
function findRegisteredLanguageFromExtension(extension) {
|
|
168
|
+
return Object.keys(REGISTERED_LANGUAGE_EXTENSIONS).find(
|
|
169
|
+
(language) => REGISTERED_LANGUAGE_EXTENSIONS[language].includes(extension)
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/validationSchemas.ts
|
|
174
|
+
var MatchRangeSchema = z.object({
|
|
175
|
+
start: z.object({
|
|
176
|
+
line: z.number().describe("0-based line number"),
|
|
177
|
+
column: z.number().describe("0-based column number")
|
|
178
|
+
}),
|
|
179
|
+
end: z.object({
|
|
180
|
+
line: z.number().describe("0-based line number"),
|
|
181
|
+
column: z.number().describe("0-based column number")
|
|
182
|
+
})
|
|
183
|
+
});
|
|
184
|
+
var MatchSourceSchema = z.lazy(
|
|
185
|
+
() => z.object({
|
|
186
|
+
filePath: z.string().describe("File path where this source match occurred"),
|
|
187
|
+
text: z.string().optional().describe("The matched text at this source step"),
|
|
188
|
+
range: MatchRangeSchema.describe("Position range of this source match"),
|
|
189
|
+
symbol: z.string().optional().describe("Optional symbol name if applicable"),
|
|
190
|
+
language: z.nativeEnum(Language).describe(
|
|
191
|
+
"The language this source match was found in (e.g., Language.TypeScript, Language.Python)"
|
|
192
|
+
)
|
|
193
|
+
})
|
|
194
|
+
);
|
|
195
|
+
var MatchMetadataSchema = z.object({
|
|
196
|
+
fromCache: z.boolean().optional().describe("Whether this match was retrieved from cache"),
|
|
197
|
+
llmValidation: z.object({
|
|
198
|
+
isViolation: z.boolean().describe("Whether the LLM determined this is a violation"),
|
|
199
|
+
confidence: z.number().min(0).max(1).describe("Confidence score (0-1) of the validation"),
|
|
200
|
+
reason: z.string().describe("Explanation of the validation decision")
|
|
201
|
+
}).optional().describe("LLM validation metadata if applicable")
|
|
202
|
+
}).optional().describe("Metadata about the match and its processing");
|
|
203
|
+
var MatchSchema = z.object({
|
|
204
|
+
filePath: z.string().describe("File path where the match was found"),
|
|
205
|
+
text: z.string().optional().describe("The matched text/code snippet"),
|
|
206
|
+
range: MatchRangeSchema.describe("Position range of the match"),
|
|
207
|
+
symbol: z.string().optional().describe("Optional symbol name if applicable"),
|
|
208
|
+
language: z.nativeEnum(Language).describe("The language this match was found in (e.g., Language.TypeScript, Language.Python)"),
|
|
209
|
+
source: z.array(MatchSourceSchema).optional().describe(
|
|
210
|
+
"Chain of sources that led to this match. First entry is the original source, last entry is the immediate parent."
|
|
211
|
+
),
|
|
212
|
+
metadata: MatchMetadataSchema
|
|
213
|
+
});
|
|
214
|
+
var ValidateViolationResponseSchema = z.object({
|
|
215
|
+
validMatches: z.array(MatchSchema).describe("Matches that are valid violations"),
|
|
216
|
+
skippedMatches: z.array(MatchSchema).describe("Matches that are not violations")
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// src/api/WispbitApiClient.ts
|
|
220
|
+
var InitializeRequestSchema = z2.object({
|
|
221
|
+
repository_url: z2.string(),
|
|
222
|
+
powerlint_version: z2.string(),
|
|
223
|
+
schema_version: z2.string()
|
|
224
|
+
});
|
|
225
|
+
var InitializeResponseSchema = z2.object({
|
|
226
|
+
configured: z2.boolean(),
|
|
227
|
+
invalid_api_key: z2.boolean().optional(),
|
|
228
|
+
is_valid_repository: z2.boolean().optional(),
|
|
229
|
+
config: z2.object({
|
|
230
|
+
ignored_globs: z2.array(z2.string())
|
|
231
|
+
}).optional()
|
|
232
|
+
});
|
|
233
|
+
var GetRulesRequestSchema = z2.object({
|
|
234
|
+
repository_url: z2.string(),
|
|
235
|
+
rule_ids: z2.array(z2.string()).optional(),
|
|
236
|
+
schema_version: z2.string(),
|
|
237
|
+
powerlint_version: z2.string()
|
|
238
|
+
});
|
|
239
|
+
var GetRulesResponseSchema = z2.object({
|
|
240
|
+
rules: z2.array(
|
|
241
|
+
z2.object({
|
|
242
|
+
id: z2.string(),
|
|
243
|
+
internalId: z2.string(),
|
|
244
|
+
internalVersionId: z2.string(),
|
|
245
|
+
message: z2.string(),
|
|
246
|
+
prompt: z2.string(),
|
|
247
|
+
severity: z2.enum(["suggestion", "violation"]),
|
|
248
|
+
schema: z2.any()
|
|
249
|
+
})
|
|
250
|
+
)
|
|
251
|
+
});
|
|
252
|
+
var ValidateViolationRequestSchema = z2.object({
|
|
253
|
+
rule: z2.object({
|
|
254
|
+
internalId: z2.string(),
|
|
255
|
+
internalVersionId: z2.string(),
|
|
256
|
+
contents: z2.string()
|
|
257
|
+
}),
|
|
258
|
+
matches: z2.array(z2.any()),
|
|
259
|
+
powerlint_version: z2.string(),
|
|
260
|
+
schema_version: z2.string()
|
|
261
|
+
});
|
|
262
|
+
var WispbitApiClient = class {
|
|
263
|
+
baseUrl;
|
|
264
|
+
apiKey;
|
|
265
|
+
constructor(config) {
|
|
266
|
+
this.baseUrl = config.baseUrl;
|
|
267
|
+
this.apiKey = config.apiKey;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Make a request to the Wispbit API with retry logic
|
|
271
|
+
*/
|
|
272
|
+
async request(endpoint, data, responseSchema) {
|
|
273
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
274
|
+
const response = await pRetry(
|
|
275
|
+
async () => {
|
|
276
|
+
const res = await fetch(url, {
|
|
277
|
+
method: "POST",
|
|
278
|
+
headers: {
|
|
279
|
+
"Content-Type": "application/json",
|
|
280
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
281
|
+
},
|
|
282
|
+
body: JSON.stringify(data)
|
|
283
|
+
});
|
|
284
|
+
if (!res.ok) {
|
|
285
|
+
const errorText = await res.text();
|
|
286
|
+
throw new Error(
|
|
287
|
+
`Wispbit API request failed: ${res.status} ${res.statusText} - ${errorText}`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
return res;
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
retries: 3,
|
|
294
|
+
minTimeout: 1e3,
|
|
295
|
+
maxTimeout: 5e3,
|
|
296
|
+
onFailedAttempt: (error) => {
|
|
297
|
+
console.warn(
|
|
298
|
+
`API request to ${endpoint} failed (attempt ${error.attemptNumber}/4): ${error.message}`
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
);
|
|
303
|
+
const json2 = await response.json();
|
|
304
|
+
if (responseSchema) {
|
|
305
|
+
return responseSchema.parse(json2);
|
|
306
|
+
}
|
|
307
|
+
return json2;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Initialize PowerLint configuration with Wispbit
|
|
311
|
+
*/
|
|
312
|
+
async initialize(request) {
|
|
313
|
+
const validatedRequest = InitializeRequestSchema.parse(request);
|
|
314
|
+
return await this.request("/plv1/initialize", validatedRequest, InitializeResponseSchema);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Get rules from Wispbit Cloud
|
|
318
|
+
*/
|
|
319
|
+
async getRules(request) {
|
|
320
|
+
const validatedRequest = GetRulesRequestSchema.parse(request);
|
|
321
|
+
return await this.request("/plv1/get-rules", validatedRequest, GetRulesResponseSchema);
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Validate violations with Wispbit
|
|
325
|
+
*/
|
|
326
|
+
async validateViolation(request) {
|
|
327
|
+
const validatedRequest = ValidateViolationRequestSchema.parse(request);
|
|
328
|
+
return await this.request(
|
|
329
|
+
"/plv1/validate-violation",
|
|
330
|
+
validatedRequest,
|
|
331
|
+
ValidateViolationResponseSchema
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Validate violations with Wispbit (internal endpoint for testing)
|
|
336
|
+
*/
|
|
337
|
+
async validateViolationInternal(request) {
|
|
338
|
+
const validatedRequest = ValidateViolationRequestSchema.parse(request);
|
|
339
|
+
return await this.request(
|
|
340
|
+
"/plv1/internal/validate-violation",
|
|
341
|
+
validatedRequest,
|
|
342
|
+
ValidateViolationResponseSchema
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
13
347
|
// src/version.ts
|
|
14
348
|
import { readFileSync } from "fs";
|
|
15
349
|
import { dirname, join } from "path";
|
|
@@ -41,13 +375,22 @@ var Config = class _Config {
|
|
|
41
375
|
config;
|
|
42
376
|
apiKey = null;
|
|
43
377
|
baseUrl = null;
|
|
44
|
-
|
|
378
|
+
apiClient = null;
|
|
379
|
+
useInternalEndpoint = false;
|
|
380
|
+
constructor(config, options) {
|
|
45
381
|
this.config = {
|
|
46
382
|
...config,
|
|
47
383
|
ignoredGlobs: config.ignoredGlobs || []
|
|
48
384
|
};
|
|
49
385
|
this.apiKey = config.apiKey || null;
|
|
50
386
|
this.baseUrl = config.baseUrl || null;
|
|
387
|
+
this.useInternalEndpoint = (options == null ? void 0 : options.useInternalEndpoint) ?? false;
|
|
388
|
+
if (this.apiKey && this.baseUrl) {
|
|
389
|
+
this.apiClient = new WispbitApiClient({
|
|
390
|
+
baseUrl: this.baseUrl,
|
|
391
|
+
apiKey: this.apiKey
|
|
392
|
+
});
|
|
393
|
+
}
|
|
51
394
|
}
|
|
52
395
|
getIgnoredGlobs() {
|
|
53
396
|
return this.config.ignoredGlobs || [];
|
|
@@ -66,6 +409,16 @@ var Config = class _Config {
|
|
|
66
409
|
getBaseUrl() {
|
|
67
410
|
return this.baseUrl;
|
|
68
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* Get the Wispbit API client
|
|
414
|
+
* @returns The API client instance
|
|
415
|
+
*/
|
|
416
|
+
getApiClient() {
|
|
417
|
+
if (!this.apiClient) {
|
|
418
|
+
throw new Error("API client not initialized. Config must have both apiKey and baseUrl.");
|
|
419
|
+
}
|
|
420
|
+
return this.apiClient;
|
|
421
|
+
}
|
|
69
422
|
/**
|
|
70
423
|
* Get the local PowerLint version
|
|
71
424
|
* @returns The current PowerLint version
|
|
@@ -80,24 +433,6 @@ var Config = class _Config {
|
|
|
80
433
|
getSchemaVersion() {
|
|
81
434
|
return "v1";
|
|
82
435
|
}
|
|
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
436
|
/**
|
|
102
437
|
* Initialize configuration without network validation (for testing)
|
|
103
438
|
* @param options Optional configuration options
|
|
@@ -107,11 +442,16 @@ var Config = class _Config {
|
|
|
107
442
|
const finalBaseUrl = options.baseUrl || process.env.WISPBIT_API_BASE_URL || "https://api.wispbit.com";
|
|
108
443
|
const finalApiKey = options.apiKey || process.env.WISPBIT_API_KEY || null;
|
|
109
444
|
const ignoredGlobs = options.ignoredGlobs || [];
|
|
110
|
-
return new _Config(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
445
|
+
return new _Config(
|
|
446
|
+
{
|
|
447
|
+
ignoredGlobs,
|
|
448
|
+
apiKey: finalApiKey || void 0,
|
|
449
|
+
baseUrl: finalBaseUrl
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
useInternalEndpoint: true
|
|
453
|
+
}
|
|
454
|
+
);
|
|
115
455
|
}
|
|
116
456
|
/**
|
|
117
457
|
* Initialize configuration by validating API key and repository URL with Wispbit
|
|
@@ -131,23 +471,15 @@ var Config = class _Config {
|
|
|
131
471
|
return { failed: true, error: "INVALID_API_KEY" };
|
|
132
472
|
}
|
|
133
473
|
const repositoryUrl = await environment.getRepositoryUrl();
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
Authorization: `Bearer ${finalApiKey}`
|
|
143
|
-
},
|
|
144
|
-
body: JSON.stringify({
|
|
145
|
-
repository_url: repositoryUrl,
|
|
146
|
-
powerlint_version: getCurrentVersion(),
|
|
147
|
-
schema_version: "v1"
|
|
148
|
-
})
|
|
474
|
+
const tempClient = new WispbitApiClient({
|
|
475
|
+
baseUrl: finalBaseUrl,
|
|
476
|
+
apiKey: finalApiKey
|
|
477
|
+
});
|
|
478
|
+
const result = await tempClient.initialize({
|
|
479
|
+
repository_url: repositoryUrl,
|
|
480
|
+
powerlint_version: getCurrentVersion(),
|
|
481
|
+
schema_version: "v1"
|
|
149
482
|
});
|
|
150
|
-
const result = await response.json();
|
|
151
483
|
if (result.invalid_api_key) {
|
|
152
484
|
return { failed: true, error: "INVALID_API_KEY" };
|
|
153
485
|
}
|
|
@@ -164,10 +496,17 @@ var Config = class _Config {
|
|
|
164
496
|
isConfigured() {
|
|
165
497
|
return this.getApiKey() !== null;
|
|
166
498
|
}
|
|
499
|
+
/**
|
|
500
|
+
* Check if the internal validation endpoint should be used
|
|
501
|
+
*/
|
|
502
|
+
shouldUseInternalEndpoint() {
|
|
503
|
+
return this.useInternalEndpoint;
|
|
504
|
+
}
|
|
167
505
|
};
|
|
168
506
|
|
|
169
507
|
// src/utils/git.ts
|
|
170
508
|
import { exec, execSync } from "child_process";
|
|
509
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
171
510
|
import { promisify } from "util";
|
|
172
511
|
|
|
173
512
|
// src/utils/hashString.ts
|
|
@@ -182,189 +521,498 @@ function findGitRoot() {
|
|
|
182
521
|
const stdout = execSync("git rev-parse --show-toplevel", { encoding: "utf-8" });
|
|
183
522
|
return stdout.trim();
|
|
184
523
|
}
|
|
524
|
+
async function getGitIgnoredFiles(repoRoot) {
|
|
525
|
+
const { stdout } = await execPromise("git ls-files --ignored --exclude-standard --others", {
|
|
526
|
+
cwd: repoRoot,
|
|
527
|
+
maxBuffer: 50 * 1024 * 1024
|
|
528
|
+
});
|
|
529
|
+
return stdout.split("\n").filter(Boolean).map((file) => file.trim());
|
|
530
|
+
}
|
|
185
531
|
async function getRepositoryUrl(repoRoot, remoteName = "origin") {
|
|
186
|
-
|
|
187
|
-
const { stdout } = await execPromise(`git remote show ${remoteName}`, {
|
|
532
|
+
const { stdout } = await execPromise(`git config --get remote.${remoteName}.url`, {
|
|
188
533
|
cwd: repoRoot
|
|
189
534
|
});
|
|
190
|
-
|
|
191
|
-
if (fetchUrlLine) {
|
|
192
|
-
return ((_a = fetchUrlLine.split("Fetch URL:").pop()) == null ? void 0 : _a.trim()) || null;
|
|
193
|
-
}
|
|
194
|
-
return null;
|
|
535
|
+
return stdout.trim() || null;
|
|
195
536
|
}
|
|
196
537
|
async function getDefaultBranch(repoRoot, remoteName = "origin") {
|
|
197
|
-
|
|
198
|
-
|
|
538
|
+
try {
|
|
539
|
+
const { stdout } = await execPromise(`git rev-parse --abbrev-ref ${remoteName}/HEAD`, {
|
|
540
|
+
cwd: repoRoot
|
|
541
|
+
});
|
|
542
|
+
const fullRef = stdout.trim();
|
|
543
|
+
const branchName = fullRef.split("/").pop();
|
|
544
|
+
return branchName || null;
|
|
545
|
+
} catch (error) {
|
|
546
|
+
const commonBranches = ["main", "master"];
|
|
547
|
+
for (const branch of commonBranches) {
|
|
548
|
+
try {
|
|
549
|
+
await execPromise(`git rev-parse --verify ${branch}`, { cwd: repoRoot });
|
|
550
|
+
return branch;
|
|
551
|
+
} catch {
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
async function tryGetUpstream(repoRoot) {
|
|
558
|
+
const { stdout } = await execPromise(`git rev-parse --abbrev-ref --symbolic-full-name @{u}`, {
|
|
559
|
+
cwd: repoRoot
|
|
560
|
+
});
|
|
561
|
+
return stdout.trim() || void 0;
|
|
562
|
+
}
|
|
563
|
+
function tryGetGraphiteParent(repoRoot, branch) {
|
|
564
|
+
const metadataPath = `.git/refs/branch-metadata/${branch}`;
|
|
565
|
+
const fullPath = `${repoRoot}/${metadataPath}`;
|
|
566
|
+
if (!existsSync2(fullPath)) {
|
|
567
|
+
return void 0;
|
|
568
|
+
}
|
|
569
|
+
const content = readFileSync2(fullPath, "utf-8");
|
|
570
|
+
const match = content.match(/"parent"\s*:\s*"([^"]+)"/);
|
|
571
|
+
return match == null ? void 0 : match[1];
|
|
572
|
+
}
|
|
573
|
+
function shellQuote(path11) {
|
|
574
|
+
return `'${path11.replace(/'/g, "'\\''")}'`;
|
|
575
|
+
}
|
|
576
|
+
function joinAsShellArgs(paths) {
|
|
577
|
+
return paths.map(shellQuote).join(" ");
|
|
578
|
+
}
|
|
579
|
+
function resolveIncludes(raw) {
|
|
580
|
+
const all = {
|
|
581
|
+
committed: true,
|
|
582
|
+
staged: true,
|
|
583
|
+
unstaged: true,
|
|
584
|
+
untracked: true
|
|
585
|
+
};
|
|
586
|
+
if (!raw || raw.length === 0) return all;
|
|
587
|
+
const s = new Set(raw);
|
|
588
|
+
return {
|
|
589
|
+
committed: s.has("committed"),
|
|
590
|
+
staged: s.has("staged"),
|
|
591
|
+
unstaged: s.has("unstaged"),
|
|
592
|
+
untracked: s.has("untracked")
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
function parseNameStatusZ(buffer, source) {
|
|
596
|
+
if (!buffer) return [];
|
|
597
|
+
const entries = [];
|
|
598
|
+
const parts = buffer.split("\0").filter(Boolean);
|
|
599
|
+
for (let i = 0; i < parts.length; ) {
|
|
600
|
+
const status = parts[i];
|
|
601
|
+
if (!status) break;
|
|
602
|
+
if (status.startsWith("R")) {
|
|
603
|
+
const oldPath = parts[i + 1];
|
|
604
|
+
const newPath = parts[i + 2];
|
|
605
|
+
if (oldPath && newPath) {
|
|
606
|
+
entries.push({ status, path: newPath, oldPath, source });
|
|
607
|
+
i += 3;
|
|
608
|
+
} else {
|
|
609
|
+
i++;
|
|
610
|
+
}
|
|
611
|
+
} else {
|
|
612
|
+
const path11 = parts[i + 1];
|
|
613
|
+
if (path11) {
|
|
614
|
+
entries.push({ status, path: path11, source });
|
|
615
|
+
i += 2;
|
|
616
|
+
} else {
|
|
617
|
+
i++;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return entries;
|
|
622
|
+
}
|
|
623
|
+
function parseLsFilesZ(buffer) {
|
|
624
|
+
if (!buffer) return [];
|
|
625
|
+
return buffer.split("\0").filter(Boolean).map((path11) => ({ status: "U", path: path11, source: "untracked" }));
|
|
626
|
+
}
|
|
627
|
+
function dedupeEntries(entries) {
|
|
628
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
629
|
+
for (const entry of entries) {
|
|
630
|
+
const existing = byPath.get(entry.path);
|
|
631
|
+
if (!existing) {
|
|
632
|
+
byPath.set(entry.path, entry);
|
|
633
|
+
} else {
|
|
634
|
+
const existingPriority = getPriority(existing.source);
|
|
635
|
+
const newPriority = getPriority(entry.source);
|
|
636
|
+
if (newPriority > existingPriority) {
|
|
637
|
+
byPath.set(entry.path, entry);
|
|
638
|
+
} else if (newPriority === existingPriority && entry.oldPath) {
|
|
639
|
+
existing.oldPath = existing.oldPath || entry.oldPath;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return Array.from(byPath.values());
|
|
644
|
+
}
|
|
645
|
+
function getPriority(source) {
|
|
646
|
+
switch (source) {
|
|
647
|
+
case "untracked":
|
|
648
|
+
return 4;
|
|
649
|
+
case "unstaged":
|
|
650
|
+
return 3;
|
|
651
|
+
case "staged":
|
|
652
|
+
return 2;
|
|
653
|
+
case "committed":
|
|
654
|
+
return 1;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
function splitGitPatchPerFile(patchOutput) {
|
|
658
|
+
const patches = /* @__PURE__ */ new Map();
|
|
659
|
+
if (!patchOutput) return patches;
|
|
660
|
+
const sections = patchOutput.split(/^diff --git /m).filter(Boolean);
|
|
661
|
+
for (const section of sections) {
|
|
662
|
+
const lines = section.split("\n");
|
|
663
|
+
const firstLine = lines[0];
|
|
664
|
+
const match = firstLine.match(/a\/(.+?) b\//);
|
|
665
|
+
if (match) {
|
|
666
|
+
const filename = match[1];
|
|
667
|
+
patches.set(filename, "diff --git " + section);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return patches;
|
|
671
|
+
}
|
|
672
|
+
function toChangeStatus(entry) {
|
|
673
|
+
if (entry.status === "U" || entry.status === "A") return "added";
|
|
674
|
+
if (entry.status === "D") return "removed";
|
|
675
|
+
return "modified";
|
|
676
|
+
}
|
|
677
|
+
function stripDiffHeaders(patch) {
|
|
678
|
+
if (!patch) return "";
|
|
679
|
+
const lines = patch.split("\n");
|
|
680
|
+
const output = [];
|
|
681
|
+
let inHunk = false;
|
|
682
|
+
for (const line of lines) {
|
|
683
|
+
if (line.startsWith("diff --git") || line.startsWith("index ") || line.startsWith("--- ") || line.startsWith("+++ ") || line.startsWith("new file mode") || line.startsWith("deleted file mode") || line.startsWith("old mode") || line.startsWith("new mode") || line.startsWith("similarity index") || line.startsWith("rename from") || line.startsWith("rename to") || line.startsWith("copy from") || line.startsWith("copy to") || line === "\") {
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
if (line.startsWith("@@")) {
|
|
687
|
+
inHunk = true;
|
|
688
|
+
}
|
|
689
|
+
if (inHunk) {
|
|
690
|
+
output.push(line);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return output.join("\n").trimEnd();
|
|
694
|
+
}
|
|
695
|
+
function parseRange(selector) {
|
|
696
|
+
const threeDotsMatch = selector.match(/^(.+)\.\.\.(.+)$/);
|
|
697
|
+
if (threeDotsMatch) {
|
|
698
|
+
return [threeDotsMatch[1], threeDotsMatch[2], true];
|
|
699
|
+
}
|
|
700
|
+
const twoDotsMatch = selector.match(/^(.+)\.\.([^.].*)$/);
|
|
701
|
+
if (twoDotsMatch) {
|
|
702
|
+
return [twoDotsMatch[1], twoDotsMatch[2], false];
|
|
703
|
+
}
|
|
704
|
+
return [selector, selector, false];
|
|
705
|
+
}
|
|
706
|
+
function parseRangeRight(selector) {
|
|
707
|
+
const [, right] = parseRange(selector);
|
|
708
|
+
return right;
|
|
709
|
+
}
|
|
710
|
+
async function getCurrentBranch(repoRoot) {
|
|
711
|
+
const { stdout } = await execPromise("git rev-parse --abbrev-ref HEAD", {
|
|
199
712
|
cwd: repoRoot
|
|
200
713
|
});
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
714
|
+
return stdout.trim();
|
|
715
|
+
}
|
|
716
|
+
function validateWorktreeIncludes(commitSelector, includes, currentBranch) {
|
|
717
|
+
const isRange = commitSelector.includes("..");
|
|
718
|
+
if (!isRange) {
|
|
719
|
+
return null;
|
|
720
|
+
}
|
|
721
|
+
const hasWorktreeIncludes = includes.includes("staged") || includes.includes("unstaged") || includes.includes("untracked");
|
|
722
|
+
if (!hasWorktreeIncludes) {
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
const rangeRight = parseRangeRight(commitSelector);
|
|
726
|
+
const endsAtCurrent = rangeRight === "HEAD" || rangeRight === currentBranch;
|
|
727
|
+
if (!endsAtCurrent) {
|
|
728
|
+
return `Worktree includes (staged, unstaged, untracked) require range to end at HEAD or current branch (${currentBranch}). Got: ${commitSelector}`;
|
|
204
729
|
}
|
|
205
730
|
return null;
|
|
206
731
|
}
|
|
207
|
-
async function
|
|
208
|
-
var _a, _b;
|
|
732
|
+
async function getDefaultCommitSelector(repoRoot) {
|
|
209
733
|
const { stdout: currentBranchOutput } = await execPromise("git rev-parse --abbrev-ref HEAD", {
|
|
210
734
|
cwd: repoRoot
|
|
211
735
|
});
|
|
212
736
|
const currentBranch = currentBranchOutput.trim();
|
|
737
|
+
const defaultInclude = ["committed", "staged", "unstaged", "untracked"];
|
|
738
|
+
const graphiteParent = tryGetGraphiteParent(repoRoot, currentBranch);
|
|
739
|
+
if (graphiteParent) {
|
|
740
|
+
return {
|
|
741
|
+
commitSelector: `${graphiteParent}...${currentBranch}`,
|
|
742
|
+
include: defaultInclude
|
|
743
|
+
};
|
|
744
|
+
}
|
|
213
745
|
const defaultBranch = await getDefaultBranch(repoRoot);
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
746
|
+
if (defaultBranch) {
|
|
747
|
+
if (currentBranch === defaultBranch) {
|
|
748
|
+
const upstream = await tryGetUpstream(repoRoot).catch(() => void 0);
|
|
749
|
+
if (upstream) {
|
|
750
|
+
return {
|
|
751
|
+
commitSelector: `${upstream}...HEAD`,
|
|
752
|
+
include: defaultInclude
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
return {
|
|
756
|
+
commitSelector: `HEAD~1...HEAD`,
|
|
757
|
+
include: defaultInclude
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
const originBranch = `origin/${defaultBranch}`;
|
|
761
|
+
const { stdout } = await execPromise(`git rev-parse --verify ${originBranch}`, {
|
|
224
762
|
cwd: repoRoot
|
|
225
763
|
});
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
`git merge-base ${currentBranch} ${compareTo}`,
|
|
232
|
-
{ cwd: repoRoot }
|
|
233
|
-
);
|
|
234
|
-
mergeBase = mergeBaseOutput.trim();
|
|
235
|
-
} catch (error) {
|
|
236
|
-
mergeBase = "HEAD^";
|
|
764
|
+
const base = stdout.trim() ? originBranch : defaultBranch;
|
|
765
|
+
return {
|
|
766
|
+
commitSelector: `${base}...${currentBranch}`,
|
|
767
|
+
include: defaultInclude
|
|
768
|
+
};
|
|
237
769
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
});
|
|
248
|
-
const { stdout: diffOutput } = await execPromise(`git diff ${mergeBase} --name-only`, {
|
|
770
|
+
return {
|
|
771
|
+
commitSelector: `HEAD...HEAD`,
|
|
772
|
+
include: defaultInclude
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
async function getChangedFiles(repoRoot, options) {
|
|
776
|
+
const selector = options.commitSelector.trim();
|
|
777
|
+
const isRange = selector.includes("..");
|
|
778
|
+
const { stdout: currentBranchOutput } = await execPromise("git rev-parse --abbrev-ref HEAD", {
|
|
249
779
|
cwd: repoRoot
|
|
250
780
|
});
|
|
251
|
-
const
|
|
252
|
-
const { stdout:
|
|
781
|
+
const currentBranch = currentBranchOutput.trim();
|
|
782
|
+
const { stdout: currentCommitOutput } = await execPromise("git rev-parse HEAD", {
|
|
253
783
|
cwd: repoRoot
|
|
254
784
|
});
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
785
|
+
const currentCommit = currentCommitOutput.trim();
|
|
786
|
+
let compareTo = "";
|
|
787
|
+
let parentRef = "";
|
|
788
|
+
if (isRange) {
|
|
789
|
+
compareTo = selector;
|
|
790
|
+
parentRef = selector;
|
|
791
|
+
} else {
|
|
792
|
+
const graphiteParent = tryGetGraphiteParent(repoRoot, currentBranch);
|
|
793
|
+
const defaultBranch = await getDefaultBranch(repoRoot);
|
|
794
|
+
const target = selector || graphiteParent || `origin/${defaultBranch || "main"}`;
|
|
795
|
+
parentRef = target;
|
|
796
|
+
const mergeBaseCmd = `git merge-base --fork-point ${shellQuote(target)} HEAD || git merge-base ${shellQuote(target)} HEAD`;
|
|
797
|
+
const { stdout: mergeBaseOutput } = await execPromise(mergeBaseCmd, { cwd: repoRoot });
|
|
798
|
+
compareTo = mergeBaseOutput.trim();
|
|
799
|
+
if (!selector && currentBranch === (defaultBranch || "main")) {
|
|
800
|
+
const upstream = await tryGetUpstream(repoRoot).catch(() => void 0);
|
|
801
|
+
if (upstream) {
|
|
802
|
+
parentRef = upstream;
|
|
803
|
+
const { stdout: upstreamMergeBase } = await execPromise(
|
|
804
|
+
`git merge-base --fork-point ${shellQuote(upstream)} HEAD || git merge-base ${shellQuote(upstream)} HEAD`,
|
|
805
|
+
{ cwd: repoRoot }
|
|
806
|
+
);
|
|
807
|
+
compareTo = upstreamMergeBase.trim();
|
|
808
|
+
} else {
|
|
809
|
+
parentRef = "HEAD~1";
|
|
810
|
+
compareTo = "HEAD~1";
|
|
811
|
+
}
|
|
261
812
|
}
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
`git diff
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
813
|
+
}
|
|
814
|
+
const includes = resolveIncludes(options.include);
|
|
815
|
+
const validationError = validateWorktreeIncludes(selector, options.include, currentBranch);
|
|
816
|
+
if (validationError) {
|
|
817
|
+
throw new Error(validationError);
|
|
818
|
+
}
|
|
819
|
+
const allEntries = [];
|
|
820
|
+
if (includes.committed) {
|
|
821
|
+
if (isRange) {
|
|
822
|
+
const [rangeA, rangeB] = parseRange(selector);
|
|
823
|
+
const { stdout } = await execPromise(
|
|
824
|
+
`git diff --name-status -M -z ${shellQuote(rangeA)}..${shellQuote(rangeB)}`,
|
|
825
|
+
{ cwd: repoRoot, maxBuffer: 50 * 1024 * 1024 }
|
|
826
|
+
);
|
|
827
|
+
allEntries.push(...parseNameStatusZ(stdout, "committed"));
|
|
828
|
+
} else {
|
|
829
|
+
const { stdout } = await execPromise(`git diff --name-status -M -z ${compareTo}..HEAD`, {
|
|
830
|
+
cwd: repoRoot,
|
|
831
|
+
maxBuffer: 50 * 1024 * 1024
|
|
832
|
+
});
|
|
833
|
+
allEntries.push(...parseNameStatusZ(stdout, "committed"));
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
if (includes.staged) {
|
|
837
|
+
const { stdout } = await execPromise(`git diff --name-status -M -z --cached`, {
|
|
838
|
+
cwd: repoRoot,
|
|
839
|
+
maxBuffer: 50 * 1024 * 1024
|
|
840
|
+
});
|
|
841
|
+
allEntries.push(...parseNameStatusZ(stdout, "staged"));
|
|
842
|
+
}
|
|
843
|
+
if (includes.unstaged) {
|
|
844
|
+
const { stdout } = await execPromise(`git diff --name-status -M -z`, {
|
|
845
|
+
cwd: repoRoot,
|
|
846
|
+
maxBuffer: 50 * 1024 * 1024
|
|
847
|
+
});
|
|
848
|
+
allEntries.push(...parseNameStatusZ(stdout, "unstaged"));
|
|
849
|
+
}
|
|
850
|
+
if (includes.untracked) {
|
|
851
|
+
const { stdout } = await execPromise(`git ls-files --others --exclude-standard -z`, {
|
|
852
|
+
cwd: repoRoot,
|
|
853
|
+
maxBuffer: 50 * 1024 * 1024
|
|
854
|
+
});
|
|
855
|
+
allEntries.push(...parseLsFilesZ(stdout));
|
|
856
|
+
}
|
|
857
|
+
const entries = dedupeEntries(allEntries);
|
|
858
|
+
const committedEntries = entries.filter((e) => e.source === "committed");
|
|
859
|
+
const stagedEntries = entries.filter((e) => e.source === "staged");
|
|
860
|
+
const unstagedEntries = entries.filter((e) => e.source === "unstaged");
|
|
861
|
+
const untrackedEntries = entries.filter((e) => e.source === "untracked");
|
|
862
|
+
const patchByFile = /* @__PURE__ */ new Map();
|
|
863
|
+
if (committedEntries.length > 0) {
|
|
864
|
+
const paths = committedEntries.map((e) => e.path);
|
|
865
|
+
let diffCmd = "";
|
|
866
|
+
if (isRange) {
|
|
867
|
+
const [rangeA, rangeB] = parseRange(selector);
|
|
868
|
+
diffCmd = `git diff -U0 -M ${shellQuote(rangeA)}..${shellQuote(rangeB)} -- ${joinAsShellArgs(paths)}`;
|
|
869
|
+
} else {
|
|
870
|
+
diffCmd = `git diff -U0 -M ${compareTo}..HEAD -- ${joinAsShellArgs(paths)}`;
|
|
871
|
+
}
|
|
872
|
+
const { stdout: patchOutput } = await execPromise(diffCmd, {
|
|
873
|
+
cwd: repoRoot,
|
|
874
|
+
maxBuffer: 50 * 1024 * 1024
|
|
875
|
+
});
|
|
876
|
+
const patches = splitGitPatchPerFile(patchOutput);
|
|
877
|
+
patches.forEach((patch, filename) => patchByFile.set(filename, stripDiffHeaders(patch)));
|
|
878
|
+
}
|
|
879
|
+
if (stagedEntries.length > 0) {
|
|
880
|
+
const paths = stagedEntries.map((e) => e.path);
|
|
881
|
+
const diffCmd = `git diff -U0 -M --cached -- ${joinAsShellArgs(paths)}`;
|
|
882
|
+
const { stdout: patchOutput } = await execPromise(diffCmd, {
|
|
883
|
+
cwd: repoRoot,
|
|
884
|
+
maxBuffer: 50 * 1024 * 1024
|
|
885
|
+
});
|
|
886
|
+
const patches = splitGitPatchPerFile(patchOutput);
|
|
887
|
+
patches.forEach((patch, filename) => patchByFile.set(filename, stripDiffHeaders(patch)));
|
|
888
|
+
}
|
|
889
|
+
if (unstagedEntries.length > 0) {
|
|
890
|
+
const paths = unstagedEntries.map((e) => e.path);
|
|
891
|
+
const diffCmd = `git diff -U0 -M -- ${joinAsShellArgs(paths)}`;
|
|
892
|
+
const { stdout: patchOutput } = await execPromise(diffCmd, {
|
|
893
|
+
cwd: repoRoot,
|
|
894
|
+
maxBuffer: 50 * 1024 * 1024
|
|
895
|
+
});
|
|
896
|
+
const patches = splitGitPatchPerFile(patchOutput);
|
|
897
|
+
patches.forEach((patch, filename) => patchByFile.set(filename, stripDiffHeaders(patch)));
|
|
898
|
+
}
|
|
899
|
+
for (const entry of untrackedEntries) {
|
|
900
|
+
try {
|
|
901
|
+
const { stdout } = await execPromise(
|
|
902
|
+
`git diff -U0 --no-index /dev/null ${shellQuote(entry.path)}`,
|
|
903
|
+
{ cwd: repoRoot, maxBuffer: 50 * 1024 * 1024 }
|
|
904
|
+
);
|
|
905
|
+
patchByFile.set(entry.path, stripDiffHeaders(stdout));
|
|
906
|
+
} catch (error) {
|
|
907
|
+
if (error.stdout) {
|
|
908
|
+
patchByFile.set(entry.path, stripDiffHeaders(error.stdout));
|
|
909
|
+
} else {
|
|
910
|
+
throw error;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
const statsByFile = /* @__PURE__ */ new Map();
|
|
915
|
+
if (committedEntries.length > 0) {
|
|
916
|
+
const paths = committedEntries.map((e) => e.path);
|
|
917
|
+
let numstatCmd = "";
|
|
918
|
+
if (isRange) {
|
|
919
|
+
const [rangeA, rangeB] = parseRange(selector);
|
|
920
|
+
numstatCmd = `git diff --numstat ${shellQuote(rangeA)}..${shellQuote(rangeB)} -- ${joinAsShellArgs(paths)}`;
|
|
921
|
+
} else {
|
|
922
|
+
numstatCmd = `git diff --numstat ${compareTo}..HEAD -- ${joinAsShellArgs(paths)}`;
|
|
923
|
+
}
|
|
924
|
+
const { stdout: numstatOutput } = await execPromise(numstatCmd, {
|
|
925
|
+
cwd: repoRoot,
|
|
926
|
+
maxBuffer: 50 * 1024 * 1024
|
|
927
|
+
});
|
|
928
|
+
const lines = numstatOutput.split("\n").filter(Boolean);
|
|
929
|
+
for (const line of lines) {
|
|
283
930
|
const parts = line.split(" ");
|
|
284
931
|
if (parts.length >= 3) {
|
|
285
|
-
const [
|
|
286
|
-
|
|
287
|
-
additions: parseInt(
|
|
288
|
-
deletions: parseInt(
|
|
932
|
+
const [addStr, delStr, filename] = parts;
|
|
933
|
+
statsByFile.set(filename, {
|
|
934
|
+
additions: parseInt(addStr) || 0,
|
|
935
|
+
deletions: parseInt(delStr) || 0
|
|
289
936
|
});
|
|
290
937
|
}
|
|
291
|
-
}
|
|
938
|
+
}
|
|
292
939
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
}
|
|
940
|
+
if (stagedEntries.length > 0) {
|
|
941
|
+
const paths = stagedEntries.map((e) => e.path);
|
|
942
|
+
const numstatCmd = `git diff --numstat --cached -- ${joinAsShellArgs(paths)}`;
|
|
943
|
+
const { stdout: numstatOutput } = await execPromise(numstatCmd, {
|
|
944
|
+
cwd: repoRoot,
|
|
945
|
+
maxBuffer: 50 * 1024 * 1024
|
|
311
946
|
});
|
|
947
|
+
const lines = numstatOutput.split("\n").filter(Boolean);
|
|
948
|
+
for (const line of lines) {
|
|
949
|
+
const parts = line.split(" ");
|
|
950
|
+
if (parts.length >= 3) {
|
|
951
|
+
const [addStr, delStr, filename] = parts;
|
|
952
|
+
statsByFile.set(filename, {
|
|
953
|
+
additions: parseInt(addStr) || 0,
|
|
954
|
+
deletions: parseInt(delStr) || 0
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
}
|
|
312
958
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
{
|
|
320
|
-
cwd: repoRoot
|
|
321
|
-
}
|
|
322
|
-
);
|
|
323
|
-
return { file, content: lastContent };
|
|
959
|
+
if (unstagedEntries.length > 0) {
|
|
960
|
+
const paths = unstagedEntries.map((e) => e.path);
|
|
961
|
+
const numstatCmd = `git diff --numstat -- ${joinAsShellArgs(paths)}`;
|
|
962
|
+
const { stdout: numstatOutput } = await execPromise(numstatCmd, {
|
|
963
|
+
cwd: repoRoot,
|
|
964
|
+
maxBuffer: 50 * 1024 * 1024
|
|
324
965
|
});
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
966
|
+
const lines = numstatOutput.split("\n").filter(Boolean);
|
|
967
|
+
for (const line of lines) {
|
|
968
|
+
const parts = line.split(" ");
|
|
969
|
+
if (parts.length >= 3) {
|
|
970
|
+
const [addStr, delStr, filename] = parts;
|
|
971
|
+
statsByFile.set(filename, {
|
|
972
|
+
additions: parseInt(addStr) || 0,
|
|
973
|
+
deletions: parseInt(delStr) || 0
|
|
974
|
+
});
|
|
329
975
|
}
|
|
330
|
-
}
|
|
976
|
+
}
|
|
331
977
|
}
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
const
|
|
978
|
+
for (const entry of untrackedEntries) {
|
|
979
|
+
const patch = patchByFile.get(entry.path) || "";
|
|
980
|
+
const lines = patch.split("\n");
|
|
335
981
|
let additions = 0;
|
|
336
982
|
let deletions = 0;
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
if (
|
|
341
|
-
deletions
|
|
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;
|
|
983
|
+
for (const line of lines) {
|
|
984
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
985
|
+
additions++;
|
|
986
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
987
|
+
deletions++;
|
|
349
988
|
}
|
|
350
|
-
diffOutput2 = fileDiffs.get(file) || "";
|
|
351
989
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
990
|
+
statsByFile.set(entry.path, { additions, deletions });
|
|
991
|
+
}
|
|
992
|
+
const fileChanges = [];
|
|
993
|
+
for (const entry of entries) {
|
|
994
|
+
const patch = patchByFile.get(entry.path) || "";
|
|
995
|
+
const stats = statsByFile.get(entry.path) || { additions: 0, deletions: 0 };
|
|
996
|
+
const status = toChangeStatus(entry);
|
|
997
|
+
const fileChange = {
|
|
998
|
+
filename: entry.path,
|
|
355
999
|
status,
|
|
356
|
-
patch
|
|
357
|
-
additions,
|
|
358
|
-
deletions,
|
|
359
|
-
sha: hashString(
|
|
360
|
-
}
|
|
1000
|
+
patch,
|
|
1001
|
+
additions: stats.additions,
|
|
1002
|
+
deletions: stats.deletions,
|
|
1003
|
+
sha: hashString(patch)
|
|
1004
|
+
};
|
|
1005
|
+
if (entry.oldPath) {
|
|
1006
|
+
fileChange.oldFilename = entry.oldPath;
|
|
1007
|
+
}
|
|
1008
|
+
fileChanges.push(fileChange);
|
|
361
1009
|
}
|
|
362
1010
|
return {
|
|
363
1011
|
files: fileChanges,
|
|
364
1012
|
currentBranch,
|
|
365
1013
|
currentCommit,
|
|
366
|
-
|
|
367
|
-
|
|
1014
|
+
diffBranch: parentRef,
|
|
1015
|
+
diffCommit: compareTo
|
|
368
1016
|
};
|
|
369
1017
|
}
|
|
370
1018
|
|
|
@@ -404,7 +1052,7 @@ var Environment = class {
|
|
|
404
1052
|
// src/environment/Storage.ts
|
|
405
1053
|
import * as fs from "fs/promises";
|
|
406
1054
|
import os from "os";
|
|
407
|
-
import
|
|
1055
|
+
import path2 from "path";
|
|
408
1056
|
import Keyv from "keyv";
|
|
409
1057
|
import { KeyvFile } from "keyv-file";
|
|
410
1058
|
var Storage = class {
|
|
@@ -418,13 +1066,13 @@ var Storage = class {
|
|
|
418
1066
|
* This replaces the getConfigDirectory functionality
|
|
419
1067
|
*/
|
|
420
1068
|
getStorageDirectory() {
|
|
421
|
-
return
|
|
1069
|
+
return path2.join(os.homedir(), ".powerlint");
|
|
422
1070
|
}
|
|
423
1071
|
/**
|
|
424
1072
|
* Get the base directory for indexes
|
|
425
1073
|
*/
|
|
426
1074
|
getIndexDirectory() {
|
|
427
|
-
return
|
|
1075
|
+
return path2.join(
|
|
428
1076
|
this.getStorageDirectory(),
|
|
429
1077
|
"indexes",
|
|
430
1078
|
hashString(this.environment.getWorkspaceRoot())
|
|
@@ -434,7 +1082,7 @@ var Storage = class {
|
|
|
434
1082
|
* Get the base directory for caches
|
|
435
1083
|
*/
|
|
436
1084
|
getCacheDirectory() {
|
|
437
|
-
return
|
|
1085
|
+
return path2.join(
|
|
438
1086
|
this.getStorageDirectory(),
|
|
439
1087
|
"cache",
|
|
440
1088
|
hashString(this.environment.getWorkspaceRoot())
|
|
@@ -454,7 +1102,7 @@ var Storage = class {
|
|
|
454
1102
|
const indexDir = this.getIndexDirectory();
|
|
455
1103
|
await this.ensureDirectory(indexDir);
|
|
456
1104
|
const file = fileName || `${language.toLowerCase()}-index.scip`;
|
|
457
|
-
return
|
|
1105
|
+
return path2.join(indexDir, `${language.toLowerCase()}-${file}`);
|
|
458
1106
|
}
|
|
459
1107
|
/**
|
|
460
1108
|
* Check if an index exists for a language
|
|
@@ -491,7 +1139,7 @@ var Storage = class {
|
|
|
491
1139
|
* Get the cache file path
|
|
492
1140
|
*/
|
|
493
1141
|
getCacheFilePath() {
|
|
494
|
-
return
|
|
1142
|
+
return path2.join(this.getCacheDirectory(), "cache.json");
|
|
495
1143
|
}
|
|
496
1144
|
/**
|
|
497
1145
|
* Purge all storage (cache and indexes)
|
|
@@ -536,7 +1184,7 @@ var Storage = class {
|
|
|
536
1184
|
const cacheDir = this.getCacheDirectory();
|
|
537
1185
|
this.cacheStore = new Keyv({
|
|
538
1186
|
store: new KeyvFile({
|
|
539
|
-
filename:
|
|
1187
|
+
filename: path2.join(cacheDir, "cache.json")
|
|
540
1188
|
})
|
|
541
1189
|
});
|
|
542
1190
|
}
|
|
@@ -579,13 +1227,6 @@ var Storage = class {
|
|
|
579
1227
|
};
|
|
580
1228
|
|
|
581
1229
|
// 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
1230
|
var WispbitRuleProvider = class {
|
|
590
1231
|
config;
|
|
591
1232
|
environment;
|
|
@@ -593,26 +1234,6 @@ var WispbitRuleProvider = class {
|
|
|
593
1234
|
this.config = config;
|
|
594
1235
|
this.environment = environment;
|
|
595
1236
|
}
|
|
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
1237
|
/**
|
|
617
1238
|
* Get the repository URL for API requests
|
|
618
1239
|
*/
|
|
@@ -646,19 +1267,14 @@ var WispbitRuleProvider = class {
|
|
|
646
1267
|
*/
|
|
647
1268
|
async fetchRules(ruleIds) {
|
|
648
1269
|
const repositoryUrl = await this.getRepositoryUrl();
|
|
649
|
-
const
|
|
1270
|
+
const apiClient = this.config.getApiClient();
|
|
1271
|
+
const response = await apiClient.getRules({
|
|
650
1272
|
repository_url: repositoryUrl,
|
|
651
1273
|
rule_ids: ruleIds,
|
|
652
1274
|
schema_version: this.config.getSchemaVersion(),
|
|
653
1275
|
powerlint_version: this.config.getLocalVersion()
|
|
654
|
-
};
|
|
655
|
-
|
|
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) => ({
|
|
1276
|
+
});
|
|
1277
|
+
return response.rules.map((rule) => ({
|
|
662
1278
|
id: rule.id,
|
|
663
1279
|
internalId: rule.internalId,
|
|
664
1280
|
config: {
|
|
@@ -670,27 +1286,6 @@ var WispbitRuleProvider = class {
|
|
|
670
1286
|
testCases: []
|
|
671
1287
|
}));
|
|
672
1288
|
}
|
|
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
1289
|
};
|
|
695
1290
|
|
|
696
1291
|
// src/steps/ExecutionEventEmitter.ts
|
|
@@ -773,11 +1368,18 @@ var ExecutionEventEmitter = class extends EventEmitter {
|
|
|
773
1368
|
this.emit("indexing:complete", { language, executionTime });
|
|
774
1369
|
}
|
|
775
1370
|
// Helper methods for step debugging
|
|
776
|
-
startStep(ruleId, stepName, stepType, inputs) {
|
|
777
|
-
this.emit("step:start", { ruleId, stepName, stepType, inputs });
|
|
1371
|
+
startStep(ruleId, stepName, stepType, inputs, parentStepName) {
|
|
1372
|
+
this.emit("step:start", { ruleId, stepName, stepType, inputs, parentStepName });
|
|
778
1373
|
}
|
|
779
|
-
completeStep(ruleId, stepName, stepType, outputs, executionTime = 0) {
|
|
780
|
-
this.emit("step:complete", {
|
|
1374
|
+
completeStep(ruleId, stepName, stepType, outputs, executionTime = 0, parentStepName) {
|
|
1375
|
+
this.emit("step:complete", {
|
|
1376
|
+
ruleId,
|
|
1377
|
+
stepName,
|
|
1378
|
+
stepType,
|
|
1379
|
+
outputs,
|
|
1380
|
+
executionTime,
|
|
1381
|
+
parentStepName
|
|
1382
|
+
});
|
|
781
1383
|
}
|
|
782
1384
|
// Helper methods for test events
|
|
783
1385
|
startTest(ruleId, testName) {
|
|
@@ -786,6 +1388,9 @@ var ExecutionEventEmitter = class extends EventEmitter {
|
|
|
786
1388
|
testMatches(ruleId, testName, matches) {
|
|
787
1389
|
this.emit("test:matches", { ruleId, testName, matches });
|
|
788
1390
|
}
|
|
1391
|
+
completeTest(ruleId, testName, matches, stepExecutions) {
|
|
1392
|
+
this.emit("test:complete", { ruleId, testName, matches, stepExecutions });
|
|
1393
|
+
}
|
|
789
1394
|
// Helper methods for LLM validation events
|
|
790
1395
|
startLLMValidation(ruleId, matchCount, estimatedCost) {
|
|
791
1396
|
this.emit("llm:validation:start", { ruleId, matchCount, estimatedCost });
|
|
@@ -806,9 +1411,18 @@ var ExecutionEventEmitter = class extends EventEmitter {
|
|
|
806
1411
|
|
|
807
1412
|
// src/steps/FileExecutionContext.ts
|
|
808
1413
|
import * as fs2 from "fs";
|
|
809
|
-
import * as
|
|
1414
|
+
import * as path3 from "path";
|
|
810
1415
|
import { glob } from "glob";
|
|
811
|
-
|
|
1416
|
+
|
|
1417
|
+
// src/utils/patternMatching.ts
|
|
1418
|
+
import ignore from "ignore";
|
|
1419
|
+
function matchesAnyPattern(filePath, patterns) {
|
|
1420
|
+
if (patterns.length === 0) return false;
|
|
1421
|
+
const ig = ignore().add(patterns);
|
|
1422
|
+
return ig.ignores(filePath);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// src/steps/FileExecutionContext.ts
|
|
812
1426
|
var FileExecutionContext = class _FileExecutionContext {
|
|
813
1427
|
environment;
|
|
814
1428
|
_filePaths = [];
|
|
@@ -825,9 +1439,33 @@ var FileExecutionContext = class _FileExecutionContext {
|
|
|
825
1439
|
/**
|
|
826
1440
|
* Create and initialize an ExecutionContext
|
|
827
1441
|
*/
|
|
828
|
-
static async initialize(config, environment, mode, eventEmitter, filePath,
|
|
1442
|
+
static async initialize(config, environment, mode, eventEmitter, filePath, diffOptions) {
|
|
829
1443
|
const context = new _FileExecutionContext(config, environment, mode, eventEmitter);
|
|
830
|
-
|
|
1444
|
+
if (mode === "diff" && !filePath) {
|
|
1445
|
+
const workspaceRoot = context.environment.getWorkspaceRoot();
|
|
1446
|
+
const include = diffOptions == null ? void 0 : diffOptions.include;
|
|
1447
|
+
const commitSelector = diffOptions == null ? void 0 : diffOptions.commitSelector;
|
|
1448
|
+
if (!commitSelector || !include)
|
|
1449
|
+
throw new Error("Commit selector and include are required in diff mode");
|
|
1450
|
+
const gitChanges = await getChangedFiles(workspaceRoot, {
|
|
1451
|
+
include,
|
|
1452
|
+
commitSelector
|
|
1453
|
+
}).catch(() => {
|
|
1454
|
+
throw new Error(
|
|
1455
|
+
"Diff mode requires a git repository. Please run this command from within a git repository."
|
|
1456
|
+
);
|
|
1457
|
+
});
|
|
1458
|
+
context.diffMode = {
|
|
1459
|
+
gitChanges,
|
|
1460
|
+
changedFiles: gitChanges.files.map((f) => f.filename),
|
|
1461
|
+
fileChangeMap: new Map(gitChanges.files.map((file) => [file.filename, file]))
|
|
1462
|
+
};
|
|
1463
|
+
context.eventEmitter.fileDiscoveryProgress(
|
|
1464
|
+
`Found ${context.diffMode.changedFiles.length} changed files`
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
const gitIgnoredFiles = mode === "check" ? await context.loadGitIgnoredFiles() : /* @__PURE__ */ new Set();
|
|
1468
|
+
const initialFiles = await context.discoverFiles(filePath, gitIgnoredFiles);
|
|
831
1469
|
context._filePaths = initialFiles;
|
|
832
1470
|
return context;
|
|
833
1471
|
}
|
|
@@ -843,8 +1481,6 @@ var FileExecutionContext = class _FileExecutionContext {
|
|
|
843
1481
|
let filteredFiles;
|
|
844
1482
|
if (this.mode === "check") {
|
|
845
1483
|
filteredFiles = filePaths;
|
|
846
|
-
} else if (!this.diffMode) {
|
|
847
|
-
filteredFiles = filePaths;
|
|
848
1484
|
} else {
|
|
849
1485
|
filteredFiles = filePaths.filter((filePath) => this.isFileValid({ filePath }));
|
|
850
1486
|
}
|
|
@@ -881,6 +1517,27 @@ var FileExecutionContext = class _FileExecutionContext {
|
|
|
881
1517
|
get executionMode() {
|
|
882
1518
|
return this.mode;
|
|
883
1519
|
}
|
|
1520
|
+
// ===== Git Ignored Files =====
|
|
1521
|
+
/**
|
|
1522
|
+
* Load git-ignored files from the repository
|
|
1523
|
+
* Uses: git ls-files --ignored --exclude-standard --others
|
|
1524
|
+
*
|
|
1525
|
+
* Only called in "check" mode - in "diff" mode, git automatically excludes ignored files.
|
|
1526
|
+
* Returns a Set for efficient lookup, which can be discarded after file discovery.
|
|
1527
|
+
* If not in a git repository, returns an empty Set.
|
|
1528
|
+
*/
|
|
1529
|
+
async loadGitIgnoredFiles() {
|
|
1530
|
+
const workspaceRoot = this.environment.getWorkspaceRoot();
|
|
1531
|
+
const ignoredFiles = await getGitIgnoredFiles(workspaceRoot).catch(() => {
|
|
1532
|
+
this.eventEmitter.fileDiscoveryProgress("Not in a git repository, skipping git-ignored files");
|
|
1533
|
+
return [];
|
|
1534
|
+
});
|
|
1535
|
+
const ignoredFilesSet = new Set(ignoredFiles);
|
|
1536
|
+
if (ignoredFiles.length > 0) {
|
|
1537
|
+
this.eventEmitter.fileDiscoveryProgress(`Found ${ignoredFiles.length} git-ignored files`);
|
|
1538
|
+
}
|
|
1539
|
+
return ignoredFilesSet;
|
|
1540
|
+
}
|
|
884
1541
|
// ===== File Discovery =====
|
|
885
1542
|
/**
|
|
886
1543
|
* Discover files based on the execution mode
|
|
@@ -892,38 +1549,23 @@ var FileExecutionContext = class _FileExecutionContext {
|
|
|
892
1549
|
* - A directory path (e.g., "src/components/")
|
|
893
1550
|
* - A glob pattern (e.g., ".ts")
|
|
894
1551
|
*/
|
|
895
|
-
async discoverFiles(filePath,
|
|
1552
|
+
async discoverFiles(filePath, gitIgnoredFiles) {
|
|
896
1553
|
this.eventEmitter.startFileDiscovery(this.mode);
|
|
897
1554
|
let discoveredFiles;
|
|
898
1555
|
if (filePath) {
|
|
899
|
-
discoveredFiles = await this.discoverFilesFromPath(filePath);
|
|
1556
|
+
discoveredFiles = await this.discoverFilesFromPath(filePath, gitIgnoredFiles);
|
|
900
1557
|
} else if (this.mode === "diff") {
|
|
901
1558
|
const workspaceRoot = this.environment.getWorkspaceRoot();
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
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...");
|
|
1559
|
+
discoveredFiles = this.diffMode.changedFiles.filter(
|
|
1560
|
+
(file) => fs2.existsSync(path3.resolve(workspaceRoot, file))
|
|
1561
|
+
).map((file) => file);
|
|
918
1562
|
const allIgnorePatterns = this.config.getIgnoredGlobs();
|
|
919
1563
|
if (allIgnorePatterns.length > 0) {
|
|
1564
|
+
this.eventEmitter.fileDiscoveryProgress("Applying ignore patterns...");
|
|
920
1565
|
const beforeIgnore = discoveredFiles.length;
|
|
921
|
-
discoveredFiles = discoveredFiles.filter(
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
);
|
|
925
|
-
return !matchesIgnore;
|
|
926
|
-
});
|
|
1566
|
+
discoveredFiles = discoveredFiles.filter(
|
|
1567
|
+
(filePath2) => !matchesAnyPattern(filePath2, allIgnorePatterns)
|
|
1568
|
+
);
|
|
927
1569
|
if (beforeIgnore !== discoveredFiles.length) {
|
|
928
1570
|
this.eventEmitter.fileDiscoveryProgress(
|
|
929
1571
|
`Filtered out ${beforeIgnore - discoveredFiles.length} ignored files`
|
|
@@ -934,7 +1576,11 @@ var FileExecutionContext = class _FileExecutionContext {
|
|
|
934
1576
|
this.eventEmitter.fileDiscoveryProgress("Scanning workspace for files...");
|
|
935
1577
|
const workspaceRoot = this.environment.getWorkspaceRoot();
|
|
936
1578
|
const allIgnorePatterns = this.config.getIgnoredGlobs();
|
|
937
|
-
discoveredFiles = await this.discoverAllFiles(
|
|
1579
|
+
discoveredFiles = await this.discoverAllFiles(
|
|
1580
|
+
[workspaceRoot],
|
|
1581
|
+
allIgnorePatterns,
|
|
1582
|
+
gitIgnoredFiles
|
|
1583
|
+
);
|
|
938
1584
|
}
|
|
939
1585
|
this.eventEmitter.completeFileDiscovery(discoveredFiles.length, this.mode);
|
|
940
1586
|
return discoveredFiles;
|
|
@@ -942,15 +1588,15 @@ var FileExecutionContext = class _FileExecutionContext {
|
|
|
942
1588
|
/**
|
|
943
1589
|
* Discover files from a given path which can be a file, directory, or glob pattern
|
|
944
1590
|
*/
|
|
945
|
-
async discoverFilesFromPath(filePath) {
|
|
1591
|
+
async discoverFilesFromPath(filePath, gitIgnoredFiles) {
|
|
946
1592
|
const workspaceRoot = this.environment.getWorkspaceRoot();
|
|
947
|
-
const fullPath =
|
|
1593
|
+
const fullPath = path3.resolve(workspaceRoot, filePath);
|
|
948
1594
|
const allIgnorePatterns = this.config.getIgnoredGlobs();
|
|
949
1595
|
if (this.isGlobPattern(filePath)) {
|
|
950
1596
|
this.eventEmitter.fileDiscoveryProgress(
|
|
951
1597
|
`Discovering files matching glob pattern: ${filePath}`
|
|
952
1598
|
);
|
|
953
|
-
return await this.discoverFilesFromGlob(filePath, allIgnorePatterns);
|
|
1599
|
+
return await this.discoverFilesFromGlob(filePath, allIgnorePatterns, gitIgnoredFiles);
|
|
954
1600
|
}
|
|
955
1601
|
if (!fs2.existsSync(fullPath)) {
|
|
956
1602
|
throw new Error(`Path not found: ${filePath}`);
|
|
@@ -958,10 +1604,11 @@ var FileExecutionContext = class _FileExecutionContext {
|
|
|
958
1604
|
const stats = fs2.statSync(fullPath);
|
|
959
1605
|
if (stats.isFile()) {
|
|
960
1606
|
this.eventEmitter.fileDiscoveryProgress(`Checking specific file: ${filePath}`);
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1607
|
+
if (gitIgnoredFiles.has(filePath)) {
|
|
1608
|
+
this.eventEmitter.fileDiscoveryProgress(`File ${filePath} is ignored by git`);
|
|
1609
|
+
return [];
|
|
1610
|
+
}
|
|
1611
|
+
if (matchesAnyPattern(filePath, allIgnorePatterns)) {
|
|
965
1612
|
this.eventEmitter.fileDiscoveryProgress(`File ${filePath} is ignored by patterns`);
|
|
966
1613
|
return [];
|
|
967
1614
|
} else {
|
|
@@ -969,7 +1616,7 @@ var FileExecutionContext = class _FileExecutionContext {
|
|
|
969
1616
|
}
|
|
970
1617
|
} else if (stats.isDirectory()) {
|
|
971
1618
|
this.eventEmitter.fileDiscoveryProgress(`Discovering files in directory: ${filePath}`);
|
|
972
|
-
return await this.discoverFilesFromDirectory(filePath, allIgnorePatterns);
|
|
1619
|
+
return await this.discoverFilesFromDirectory(filePath, allIgnorePatterns, gitIgnoredFiles);
|
|
973
1620
|
} else {
|
|
974
1621
|
throw new Error(`Path is neither a file nor directory: ${filePath}`);
|
|
975
1622
|
}
|
|
@@ -983,7 +1630,7 @@ var FileExecutionContext = class _FileExecutionContext {
|
|
|
983
1630
|
/**
|
|
984
1631
|
* Discover files matching a glob pattern
|
|
985
1632
|
*/
|
|
986
|
-
async discoverFilesFromGlob(globPattern, ignorePatterns) {
|
|
1633
|
+
async discoverFilesFromGlob(globPattern, ignorePatterns, gitIgnoredFiles) {
|
|
987
1634
|
const workspaceRoot = this.environment.getWorkspaceRoot();
|
|
988
1635
|
const allIgnorePatterns = ["**/node_modules/**", "**/.git/**", ...ignorePatterns];
|
|
989
1636
|
const matches = await glob(globPattern, {
|
|
@@ -992,15 +1639,18 @@ var FileExecutionContext = class _FileExecutionContext {
|
|
|
992
1639
|
absolute: false,
|
|
993
1640
|
ignore: allIgnorePatterns
|
|
994
1641
|
});
|
|
995
|
-
|
|
996
|
-
|
|
1642
|
+
const filteredMatches = matches.filter((match) => !gitIgnoredFiles.has(match));
|
|
1643
|
+
this.eventEmitter.fileDiscoveryProgress(
|
|
1644
|
+
`Found ${filteredMatches.length} files matching pattern`
|
|
1645
|
+
);
|
|
1646
|
+
return filteredMatches;
|
|
997
1647
|
}
|
|
998
1648
|
/**
|
|
999
1649
|
* Discover all files in a directory
|
|
1000
1650
|
*/
|
|
1001
|
-
async discoverFilesFromDirectory(dirPath, ignorePatterns) {
|
|
1651
|
+
async discoverFilesFromDirectory(dirPath, ignorePatterns, gitIgnoredFiles) {
|
|
1002
1652
|
const workspaceRoot = this.environment.getWorkspaceRoot();
|
|
1003
|
-
const globPattern =
|
|
1653
|
+
const globPattern = path3.join(dirPath, "**/*").replace(/\\/g, "/");
|
|
1004
1654
|
const allIgnorePatterns = ["**/node_modules/**", "**/.git/**", ...ignorePatterns];
|
|
1005
1655
|
const matches = await glob(globPattern, {
|
|
1006
1656
|
cwd: workspaceRoot,
|
|
@@ -1008,22 +1658,25 @@ var FileExecutionContext = class _FileExecutionContext {
|
|
|
1008
1658
|
absolute: false,
|
|
1009
1659
|
ignore: allIgnorePatterns
|
|
1010
1660
|
});
|
|
1011
|
-
|
|
1012
|
-
|
|
1661
|
+
const filteredMatches = matches.filter((match) => !gitIgnoredFiles.has(match));
|
|
1662
|
+
this.eventEmitter.fileDiscoveryProgress(`Found ${filteredMatches.length} files in directory`);
|
|
1663
|
+
return filteredMatches;
|
|
1013
1664
|
}
|
|
1014
1665
|
/**
|
|
1015
1666
|
* Discover all files from directories using glob patterns (scan mode)
|
|
1016
1667
|
*/
|
|
1017
|
-
async discoverAllFiles(directories, ignorePatterns) {
|
|
1668
|
+
async discoverAllFiles(directories, ignorePatterns, gitIgnoredFiles) {
|
|
1018
1669
|
const allFiles = [];
|
|
1019
1670
|
const workspaceRoot = this.environment.getWorkspaceRoot();
|
|
1020
1671
|
const allIgnorePatterns = ["**/node_modules/**", "**/.git/**", ...ignorePatterns];
|
|
1021
1672
|
for (const dir of directories) {
|
|
1022
1673
|
const stats = fs2.statSync(dir);
|
|
1023
1674
|
if (!stats.isDirectory()) {
|
|
1024
|
-
const
|
|
1025
|
-
|
|
1026
|
-
|
|
1675
|
+
const relativePath = path3.relative(workspaceRoot, dir);
|
|
1676
|
+
const isGitIgnored = gitIgnoredFiles.has(relativePath);
|
|
1677
|
+
const shouldIgnore = matchesAnyPattern(relativePath, ignorePatterns);
|
|
1678
|
+
if (!isGitIgnored && !shouldIgnore) {
|
|
1679
|
+
allFiles.push(relativePath);
|
|
1027
1680
|
}
|
|
1028
1681
|
continue;
|
|
1029
1682
|
}
|
|
@@ -1035,19 +1688,13 @@ var FileExecutionContext = class _FileExecutionContext {
|
|
|
1035
1688
|
ignore: allIgnorePatterns
|
|
1036
1689
|
});
|
|
1037
1690
|
const relativePaths = matches.map((match) => {
|
|
1038
|
-
const absolutePath =
|
|
1039
|
-
return
|
|
1040
|
-
});
|
|
1691
|
+
const absolutePath = path3.resolve(dir, match);
|
|
1692
|
+
return path3.relative(workspaceRoot, absolutePath);
|
|
1693
|
+
}).filter((relativePath) => !gitIgnoredFiles.has(relativePath));
|
|
1041
1694
|
allFiles.push(...relativePaths);
|
|
1042
1695
|
}
|
|
1043
1696
|
return [...new Set(allFiles)];
|
|
1044
1697
|
}
|
|
1045
|
-
/**
|
|
1046
|
-
* Pattern matching function that uses consistent options
|
|
1047
|
-
*/
|
|
1048
|
-
matchesPattern(filePath, pattern) {
|
|
1049
|
-
return minimatch(filePath, pattern, { dot: true });
|
|
1050
|
-
}
|
|
1051
1698
|
/**
|
|
1052
1699
|
* Check if a file should be processed
|
|
1053
1700
|
*/
|
|
@@ -1055,9 +1702,6 @@ var FileExecutionContext = class _FileExecutionContext {
|
|
|
1055
1702
|
if (this.mode === "check") {
|
|
1056
1703
|
return true;
|
|
1057
1704
|
}
|
|
1058
|
-
if (!this.diffMode) {
|
|
1059
|
-
return true;
|
|
1060
|
-
}
|
|
1061
1705
|
const { filePath } = options;
|
|
1062
1706
|
return this.diffMode.changedFiles.some((changedFile) => {
|
|
1063
1707
|
return filePath === changedFile || filePath.endsWith(changedFile) || changedFile.endsWith(filePath);
|
|
@@ -1070,9 +1714,6 @@ var FileExecutionContext = class _FileExecutionContext {
|
|
|
1070
1714
|
if (this.mode === "check") {
|
|
1071
1715
|
return true;
|
|
1072
1716
|
}
|
|
1073
|
-
if (!this.diffMode) {
|
|
1074
|
-
return true;
|
|
1075
|
-
}
|
|
1076
1717
|
const { filePath, startLine, endLine } = options;
|
|
1077
1718
|
const fileChange = this.diffMode.fileChangeMap.get(filePath);
|
|
1078
1719
|
if (!fileChange) {
|
|
@@ -1093,9 +1734,6 @@ var FileExecutionContext = class _FileExecutionContext {
|
|
|
1093
1734
|
if (this.mode === "check") {
|
|
1094
1735
|
return true;
|
|
1095
1736
|
}
|
|
1096
|
-
if (!this.diffMode) {
|
|
1097
|
-
return true;
|
|
1098
|
-
}
|
|
1099
1737
|
const { match } = options;
|
|
1100
1738
|
return this.isFileValid({ filePath: match.filePath }) && this.isLineRangeValid({
|
|
1101
1739
|
filePath: match.filePath,
|
|
@@ -1141,48 +1779,34 @@ var FileExecutionContext = class _FileExecutionContext {
|
|
|
1141
1779
|
|
|
1142
1780
|
// src/steps/FileFilterStep.ts
|
|
1143
1781
|
import * as fs3 from "fs";
|
|
1144
|
-
import * as
|
|
1145
|
-
import { minimatch as minimatch2 } from "minimatch";
|
|
1782
|
+
import * as path4 from "path";
|
|
1146
1783
|
var FileFilterStep = class {
|
|
1147
1784
|
environment;
|
|
1148
1785
|
constructor(environment) {
|
|
1149
1786
|
this.environment = environment;
|
|
1150
1787
|
}
|
|
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
1788
|
/**
|
|
1161
1789
|
* Evaluate a single file filter condition for a given file path
|
|
1162
1790
|
*/
|
|
1163
1791
|
evaluateCondition(condition, filePath) {
|
|
1164
1792
|
const workspaceRoot = this.environment.getWorkspaceRoot();
|
|
1165
|
-
const absoluteFilePath =
|
|
1793
|
+
const absoluteFilePath = path4.resolve(workspaceRoot, filePath);
|
|
1166
1794
|
if ("fs.siblingExists" in condition) {
|
|
1167
1795
|
const { filename } = condition["fs.siblingExists"];
|
|
1168
|
-
const dir =
|
|
1169
|
-
const siblingPath =
|
|
1796
|
+
const dir = path4.dirname(absoluteFilePath);
|
|
1797
|
+
const siblingPath = path4.join(dir, filename);
|
|
1170
1798
|
return fs3.existsSync(siblingPath);
|
|
1171
1799
|
}
|
|
1172
|
-
if ("fs.pathMatches" in condition) {
|
|
1173
|
-
const { pattern } = condition["fs.pathMatches"];
|
|
1174
|
-
return this.matchesPattern(filePath, pattern);
|
|
1175
|
-
}
|
|
1176
1800
|
if ("fs.ancestorHas" in condition) {
|
|
1177
1801
|
const { filename } = condition["fs.ancestorHas"];
|
|
1178
|
-
let currentDir =
|
|
1179
|
-
const root =
|
|
1802
|
+
let currentDir = path4.dirname(absoluteFilePath);
|
|
1803
|
+
const root = path4.parse(currentDir).root;
|
|
1180
1804
|
while (currentDir !== root) {
|
|
1181
|
-
const targetPath =
|
|
1805
|
+
const targetPath = path4.join(currentDir, filename);
|
|
1182
1806
|
if (fs3.existsSync(targetPath)) {
|
|
1183
1807
|
return true;
|
|
1184
1808
|
}
|
|
1185
|
-
const parentDir =
|
|
1809
|
+
const parentDir = path4.dirname(currentDir);
|
|
1186
1810
|
if (parentDir === currentDir) break;
|
|
1187
1811
|
currentDir = parentDir;
|
|
1188
1812
|
}
|
|
@@ -1190,12 +1814,12 @@ var FileFilterStep = class {
|
|
|
1190
1814
|
}
|
|
1191
1815
|
if ("fs.siblingAny" in condition) {
|
|
1192
1816
|
const { pattern } = condition["fs.siblingAny"];
|
|
1193
|
-
const dir =
|
|
1817
|
+
const dir = path4.dirname(absoluteFilePath);
|
|
1194
1818
|
if (!fs3.existsSync(dir)) {
|
|
1195
1819
|
return false;
|
|
1196
1820
|
}
|
|
1197
1821
|
const siblings = fs3.readdirSync(dir);
|
|
1198
|
-
return siblings.some((sibling) =>
|
|
1822
|
+
return siblings.some((sibling) => matchesAnyPattern(sibling, [pattern]));
|
|
1199
1823
|
}
|
|
1200
1824
|
return false;
|
|
1201
1825
|
}
|
|
@@ -1204,13 +1828,13 @@ var FileFilterStep = class {
|
|
|
1204
1828
|
*/
|
|
1205
1829
|
evaluateConditions(conditions, filePath) {
|
|
1206
1830
|
const results = [];
|
|
1207
|
-
if (conditions.all) {
|
|
1831
|
+
if (conditions.all && conditions.all.length > 0) {
|
|
1208
1832
|
results.push(conditions.all.every((condition) => this.evaluateCondition(condition, filePath)));
|
|
1209
1833
|
}
|
|
1210
|
-
if (conditions.any) {
|
|
1834
|
+
if (conditions.any && conditions.any.length > 0) {
|
|
1211
1835
|
results.push(conditions.any.some((condition) => this.evaluateCondition(condition, filePath)));
|
|
1212
1836
|
}
|
|
1213
|
-
if (conditions.not) {
|
|
1837
|
+
if (conditions.not && conditions.not.length > 0) {
|
|
1214
1838
|
results.push(!conditions.not.some((condition) => this.evaluateCondition(condition, filePath)));
|
|
1215
1839
|
}
|
|
1216
1840
|
return results.length === 0 ? true : results.every((result) => result === true);
|
|
@@ -1224,18 +1848,15 @@ var FileFilterStep = class {
|
|
|
1224
1848
|
let filteredPaths = filePaths;
|
|
1225
1849
|
if ((_a = options.include) == null ? void 0 : _a.length) {
|
|
1226
1850
|
filteredPaths = filteredPaths.filter(
|
|
1227
|
-
(filePath) =>
|
|
1851
|
+
(filePath) => matchesAnyPattern(filePath, options.include)
|
|
1228
1852
|
);
|
|
1229
1853
|
}
|
|
1230
1854
|
if ((_b = options.ignore) == null ? void 0 : _b.length) {
|
|
1231
|
-
filteredPaths = filteredPaths.filter(
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
);
|
|
1235
|
-
return !matchesIgnore;
|
|
1236
|
-
});
|
|
1855
|
+
filteredPaths = filteredPaths.filter(
|
|
1856
|
+
(filePath) => !matchesAnyPattern(filePath, options.ignore)
|
|
1857
|
+
);
|
|
1237
1858
|
}
|
|
1238
|
-
if (options.conditions) {
|
|
1859
|
+
if (Object.keys(options.conditions || {}).length > 0 && filteredPaths.length > 0) {
|
|
1239
1860
|
filteredPaths = filteredPaths.filter(
|
|
1240
1861
|
(filePath) => this.evaluateConditions(options.conditions, filePath)
|
|
1241
1862
|
);
|
|
@@ -1251,126 +1872,6 @@ var FileFilterStep = class {
|
|
|
1251
1872
|
// src/steps/FindMatchesStep.ts
|
|
1252
1873
|
import path8 from "path";
|
|
1253
1874
|
|
|
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
1875
|
// src/providers/AstGrepAstProvider.ts
|
|
1375
1876
|
import { readFile as readFile2 } from "fs/promises";
|
|
1376
1877
|
import path5 from "path";
|
|
@@ -2130,7 +2631,7 @@ var FindMatchesStep = class {
|
|
|
2130
2631
|
|
|
2131
2632
|
// src/steps/GotoDefinitionStep.ts
|
|
2132
2633
|
import path9 from "path";
|
|
2133
|
-
import
|
|
2634
|
+
import ignore2 from "ignore";
|
|
2134
2635
|
var GotoDefinitionStep = class {
|
|
2135
2636
|
maxDepth = 1;
|
|
2136
2637
|
environment;
|
|
@@ -2297,7 +2798,8 @@ var GotoDefinitionStep = class {
|
|
|
2297
2798
|
return relativePath === spec.path;
|
|
2298
2799
|
}
|
|
2299
2800
|
if (spec.glob) {
|
|
2300
|
-
|
|
2801
|
+
const ig = ignore2().add(spec.glob);
|
|
2802
|
+
return ig.ignores(relativePath);
|
|
2301
2803
|
}
|
|
2302
2804
|
if (spec.regex) {
|
|
2303
2805
|
const regex = new RegExp(spec.regex);
|
|
@@ -2310,70 +2812,29 @@ var GotoDefinitionStep = class {
|
|
|
2310
2812
|
// src/providers/WispbitViolationValidationProvider.ts
|
|
2311
2813
|
var WispbitViolationValidationProvider = class {
|
|
2312
2814
|
config;
|
|
2313
|
-
|
|
2815
|
+
useInternalEndpoint;
|
|
2816
|
+
constructor(config, useInternalEndpoint = false) {
|
|
2314
2817
|
this.config = config;
|
|
2818
|
+
this.useInternalEndpoint = useInternalEndpoint;
|
|
2315
2819
|
}
|
|
2316
2820
|
async validateViolations(params) {
|
|
2317
|
-
|
|
2318
|
-
const
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
const
|
|
2324
|
-
const
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
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
|
-
})
|
|
2821
|
+
const apiClient = this.config.getApiClient();
|
|
2822
|
+
const rulePayload = {
|
|
2823
|
+
internalId: params.rule.internalId,
|
|
2824
|
+
internalVersionId: params.rule.internalId,
|
|
2825
|
+
contents: params.rule.prompt
|
|
2826
|
+
};
|
|
2827
|
+
const method = this.useInternalEndpoint ? apiClient.validateViolationInternal.bind(apiClient) : apiClient.validateViolation.bind(apiClient);
|
|
2828
|
+
const result = await method({
|
|
2829
|
+
rule: rulePayload,
|
|
2830
|
+
matches: params.matches,
|
|
2831
|
+
powerlint_version: this.config.getLocalVersion(),
|
|
2832
|
+
schema_version: this.config.getSchemaVersion()
|
|
2344
2833
|
});
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
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
|
|
2834
|
+
return {
|
|
2835
|
+
validMatches: result.validMatches,
|
|
2836
|
+
skippedMatches: result.skippedMatches
|
|
2375
2837
|
};
|
|
2376
|
-
return hashString(JSON.stringify(matchData)).substring(0, 16);
|
|
2377
2838
|
}
|
|
2378
2839
|
};
|
|
2379
2840
|
|
|
@@ -2390,31 +2851,20 @@ var LLMStep = class {
|
|
|
2390
2851
|
this.storage = new Storage(environment);
|
|
2391
2852
|
}
|
|
2392
2853
|
/**
|
|
2393
|
-
* Generate a
|
|
2394
|
-
* Hash is based on filePath + range + text
|
|
2854
|
+
* Generate a cache key for a single match LLM validation
|
|
2395
2855
|
*/
|
|
2396
|
-
|
|
2856
|
+
generateMatchCacheKey(match) {
|
|
2397
2857
|
const matchData = {
|
|
2398
2858
|
filePath: match.filePath,
|
|
2399
2859
|
startLine: match.range.start.line,
|
|
2400
2860
|
startColumn: match.range.start.column || 0,
|
|
2401
2861
|
endLine: match.range.end.line,
|
|
2402
2862
|
endColumn: match.range.end.column || 0,
|
|
2403
|
-
text: match.text
|
|
2863
|
+
text: match.text,
|
|
2864
|
+
prompt: match.text
|
|
2865
|
+
// Include prompt in cache key
|
|
2404
2866
|
};
|
|
2405
|
-
return
|
|
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`;
|
|
2867
|
+
return JSON.stringify(matchData);
|
|
2418
2868
|
}
|
|
2419
2869
|
/**
|
|
2420
2870
|
* Execute the actual LLM validation for uncached matches
|
|
@@ -2424,38 +2874,42 @@ var LLMStep = class {
|
|
|
2424
2874
|
*/
|
|
2425
2875
|
async executeLLMValidation(rule, uncachedMatches) {
|
|
2426
2876
|
const llmStartTime = Date.now();
|
|
2427
|
-
const
|
|
2428
|
-
|
|
2877
|
+
const validationProvider = new WispbitViolationValidationProvider(
|
|
2878
|
+
this.config,
|
|
2879
|
+
this.config.shouldUseInternalEndpoint()
|
|
2880
|
+
);
|
|
2429
2881
|
const validationParams = {
|
|
2430
2882
|
rule,
|
|
2431
2883
|
matches: uncachedMatches
|
|
2432
2884
|
};
|
|
2433
2885
|
this.eventEmitter.startLLMValidation(rule.id, uncachedMatches.length);
|
|
2434
|
-
const
|
|
2435
|
-
const newViolationMatches =
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
fromCache: false,
|
|
2445
|
-
llmValidation: {
|
|
2446
|
-
isViolation: true,
|
|
2447
|
-
confidence: result.confidence,
|
|
2448
|
-
reason: result.reason
|
|
2449
|
-
}
|
|
2450
|
-
}
|
|
2451
|
-
});
|
|
2886
|
+
const validationResponse = await validationProvider.validateViolations(validationParams);
|
|
2887
|
+
const newViolationMatches = validationResponse.validMatches.map((match) => ({
|
|
2888
|
+
...match,
|
|
2889
|
+
metadata: {
|
|
2890
|
+
fromCache: false,
|
|
2891
|
+
llmValidation: {
|
|
2892
|
+
isViolation: true,
|
|
2893
|
+
confidence: 1,
|
|
2894
|
+
reason: "Confirmed violation"
|
|
2895
|
+
}
|
|
2452
2896
|
}
|
|
2453
|
-
|
|
2897
|
+
}));
|
|
2898
|
+
for (const match of validationResponse.validMatches) {
|
|
2899
|
+
const cacheKey = this.generateMatchCacheKey(match);
|
|
2900
|
+
const decision = {
|
|
2901
|
+
isViolation: true,
|
|
2902
|
+
confidence: 1,
|
|
2903
|
+
reason: "Confirmed violation"
|
|
2904
|
+
};
|
|
2905
|
+
await this.storage.saveCache(rule.id, cacheKey, decision);
|
|
2906
|
+
}
|
|
2907
|
+
for (const match of validationResponse.skippedMatches) {
|
|
2908
|
+
const cacheKey = this.generateMatchCacheKey(match);
|
|
2454
2909
|
const decision = {
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
reason: result.reason
|
|
2910
|
+
isViolation: false,
|
|
2911
|
+
confidence: 1,
|
|
2912
|
+
reason: "Not a violation"
|
|
2459
2913
|
};
|
|
2460
2914
|
await this.storage.saveCache(rule.id, cacheKey, decision);
|
|
2461
2915
|
}
|
|
@@ -2479,12 +2933,10 @@ var LLMStep = class {
|
|
|
2479
2933
|
*/
|
|
2480
2934
|
async execute(rule, previousMatches) {
|
|
2481
2935
|
const startTime = Date.now();
|
|
2482
|
-
const promptHash = this.hashPrompt(rule.prompt);
|
|
2483
2936
|
const cachedMatches = [];
|
|
2484
2937
|
const uncachedMatches = [];
|
|
2485
2938
|
for (const match of previousMatches) {
|
|
2486
|
-
const
|
|
2487
|
-
const cacheKey = this.generateMatchCacheKey(matchId, promptHash);
|
|
2939
|
+
const cacheKey = this.generateMatchCacheKey(match);
|
|
2488
2940
|
const cachedDecision = await this.storage.readCache(rule.id, cacheKey);
|
|
2489
2941
|
if (cachedDecision) {
|
|
2490
2942
|
if (cachedDecision.isViolation) {
|
|
@@ -2551,7 +3003,7 @@ var RuleExecutor = class {
|
|
|
2551
3003
|
/**
|
|
2552
3004
|
* Execute a single step
|
|
2553
3005
|
*/
|
|
2554
|
-
async executeStep(rule, stepName, stepConfig, filePaths, matches, options) {
|
|
3006
|
+
async executeStep(rule, stepName, stepConfig, filePaths, matches, options, _parentStepName) {
|
|
2555
3007
|
const stepType = stepConfig.type;
|
|
2556
3008
|
if (stepType === "ast-grep") {
|
|
2557
3009
|
const { type: _type, language, ...schema } = stepConfig;
|
|
@@ -2568,13 +3020,34 @@ var RuleExecutor = class {
|
|
|
2568
3020
|
for (let i = 0; i < stepConfig.steps.length; i++) {
|
|
2569
3021
|
const subStep = stepConfig.steps[i];
|
|
2570
3022
|
const subStepName = `${stepName}[${i}]`;
|
|
3023
|
+
this.eventEmitter.startStep(
|
|
3024
|
+
rule.id,
|
|
3025
|
+
subStepName,
|
|
3026
|
+
subStep.type,
|
|
3027
|
+
{ filePaths, matches },
|
|
3028
|
+
stepName
|
|
3029
|
+
// parent step name
|
|
3030
|
+
);
|
|
3031
|
+
const subStepStartTime = Date.now();
|
|
2571
3032
|
const subResult = await this.executeStep(
|
|
2572
3033
|
rule,
|
|
2573
3034
|
subStepName,
|
|
2574
3035
|
subStep,
|
|
2575
3036
|
filePaths,
|
|
2576
3037
|
matches,
|
|
2577
|
-
options
|
|
3038
|
+
options,
|
|
3039
|
+
stepName
|
|
3040
|
+
// parent step name
|
|
3041
|
+
);
|
|
3042
|
+
const subStepExecutionTime = Date.now() - subStepStartTime;
|
|
3043
|
+
this.eventEmitter.completeStep(
|
|
3044
|
+
rule.id,
|
|
3045
|
+
subStepName,
|
|
3046
|
+
subStep.type,
|
|
3047
|
+
subResult,
|
|
3048
|
+
subStepExecutionTime,
|
|
3049
|
+
stepName
|
|
3050
|
+
// parent step name
|
|
2578
3051
|
);
|
|
2579
3052
|
if (subResult.matches) {
|
|
2580
3053
|
allMatches = allMatches.concat(subResult.matches);
|
|
@@ -2643,7 +3116,7 @@ var RuleExecutor = class {
|
|
|
2643
3116
|
options.mode,
|
|
2644
3117
|
this.eventEmitter,
|
|
2645
3118
|
options.filePath,
|
|
2646
|
-
options.
|
|
3119
|
+
options.diffOptions
|
|
2647
3120
|
);
|
|
2648
3121
|
this.currentMode = options.mode;
|
|
2649
3122
|
}
|
|
@@ -2680,13 +3153,7 @@ var RuleExecutor = class {
|
|
|
2680
3153
|
options
|
|
2681
3154
|
);
|
|
2682
3155
|
const stepExecutionTime = Date.now() - stepStartTime;
|
|
2683
|
-
this.eventEmitter.
|
|
2684
|
-
ruleId,
|
|
2685
|
-
stepName,
|
|
2686
|
-
stepType: stepConfig.type,
|
|
2687
|
-
outputs: result,
|
|
2688
|
-
executionTime: stepExecutionTime
|
|
2689
|
-
});
|
|
3156
|
+
this.eventEmitter.completeStep(ruleId, stepName, stepConfig.type, result, stepExecutionTime);
|
|
2690
3157
|
if (result.filePaths !== void 0) {
|
|
2691
3158
|
currentFilePaths = this.executionContext.filterFiles(result.filePaths);
|
|
2692
3159
|
currentMatches = this.executionContext.filterMatchesByFilePaths(
|
|
@@ -2732,9 +3199,9 @@ var SKIPPED_COLOR = "#9b59b6";
|
|
|
2732
3199
|
var BRAND_COLOR = "#fbbf24";
|
|
2733
3200
|
function formatClickableRuleId(ruleId, internalId) {
|
|
2734
3201
|
if (internalId) {
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
3202
|
+
const url = `https://app.wispbit.com/rules/${internalId}`;
|
|
3203
|
+
const link = `\x1B]8;;${url}\x07${chalk.dim.underline(ruleId)}\x1B]8;;\x07`;
|
|
3204
|
+
return link;
|
|
2738
3205
|
}
|
|
2739
3206
|
return chalk.dim(ruleId);
|
|
2740
3207
|
}
|
|
@@ -2855,12 +3322,12 @@ function printSummary(results, summary) {
|
|
|
2855
3322
|
`;
|
|
2856
3323
|
} else {
|
|
2857
3324
|
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 <=
|
|
3325
|
+
const shouldShowCodeSnippets = sortedMatches.length < 10 && sortedMatches.some((match) => match.text && match.text.split("\n").length <= 20);
|
|
2859
3326
|
if (shouldShowCodeSnippets) {
|
|
2860
3327
|
sortedMatches.forEach((match, index) => {
|
|
2861
3328
|
output += ` ${match.filePath}
|
|
2862
3329
|
`;
|
|
2863
|
-
if (match.text && match.text.split("\n").length <=
|
|
3330
|
+
if (match.text && match.text.split("\n").length <= 20) {
|
|
2864
3331
|
const lines = match.text.split("\n");
|
|
2865
3332
|
lines.forEach((line, lineIndex) => {
|
|
2866
3333
|
const lineNumber = match.line + lineIndex;
|
|
@@ -2888,24 +3355,22 @@ function printSummary(results, summary) {
|
|
|
2888
3355
|
`;
|
|
2889
3356
|
}
|
|
2890
3357
|
});
|
|
3358
|
+
console.log(output);
|
|
2891
3359
|
const total = violationCount + suggestionCount;
|
|
2892
|
-
if (total === 0) {
|
|
2893
|
-
console.log(`
|
|
2894
|
-
no results`);
|
|
2895
|
-
}
|
|
2896
3360
|
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
3361
|
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
3362
|
const filesText = summary.totalFiles ? `${summary.totalFiles} ${pluralize("file", summary.totalFiles)}` : "0 files";
|
|
2899
3363
|
const rulesText = `${summary.totalRules} ${pluralize("rule", summary.totalRules)}`;
|
|
2900
3364
|
const timeText = summary.executionTime ? `${Math.round(summary.executionTime)}ms` : "";
|
|
2901
3365
|
const detailsText = [filesText, rulesText, timeText].filter(Boolean).join(", ");
|
|
2902
|
-
|
|
3366
|
+
let summaryOutput = "";
|
|
3367
|
+
summaryOutput += `${violationText}
|
|
2903
3368
|
`;
|
|
2904
|
-
|
|
3369
|
+
summaryOutput += `${suggestionText}
|
|
2905
3370
|
`;
|
|
2906
|
-
|
|
3371
|
+
summaryOutput += `${chalk.dim("summary".padStart(12))} ${detailsText}
|
|
2907
3372
|
`;
|
|
2908
|
-
console.log(total > 0 ? chalk.reset(
|
|
3373
|
+
console.log(total > 0 ? chalk.reset(summaryOutput) : summaryOutput);
|
|
2909
3374
|
if (violationCount > 0) {
|
|
2910
3375
|
process.exit(1);
|
|
2911
3376
|
}
|
|
@@ -3336,6 +3801,9 @@ async function saveApiKey(apiKey) {
|
|
|
3336
3801
|
await fs5.writeFile(configPath, JSON.stringify(newConfig, null, 2));
|
|
3337
3802
|
}
|
|
3338
3803
|
async function loadApiKey() {
|
|
3804
|
+
if (process.env.WISPBIT_API_KEY) {
|
|
3805
|
+
return process.env.WISPBIT_API_KEY;
|
|
3806
|
+
}
|
|
3339
3807
|
const configPath = getConfigFilePath();
|
|
3340
3808
|
try {
|
|
3341
3809
|
const configContent = await fs5.readFile(configPath, "utf-8");
|
|
@@ -3406,13 +3874,13 @@ https://wispbit.com/
|
|
|
3406
3874
|
Usage:
|
|
3407
3875
|
$ wispbit [diff-options] (run diff by default)
|
|
3408
3876
|
$ wispbit check [file-path/directory] [check-options]
|
|
3409
|
-
$ wispbit diff [diff-options]
|
|
3877
|
+
$ wispbit diff [commit] [diff-options]
|
|
3410
3878
|
$ wispbit list
|
|
3411
3879
|
$ wispbit cache purge
|
|
3412
3880
|
|
|
3413
3881
|
Commands:
|
|
3414
|
-
check [file-path/directory]
|
|
3415
|
-
diff
|
|
3882
|
+
check [file-path/directory] Run linting rules against a specific file/folder or entire codebase
|
|
3883
|
+
diff [commit] Run linting rules on changed files
|
|
3416
3884
|
list List all available rules with their ID, message, and severity
|
|
3417
3885
|
cache purge Purge the cache directory (indexes, caching, etc.)
|
|
3418
3886
|
|
|
@@ -3424,10 +3892,21 @@ Options for check:
|
|
|
3424
3892
|
Options for diff:
|
|
3425
3893
|
--rule <ruleId> Optional rule ID to run specific rule
|
|
3426
3894
|
--json [format] Output in JSON format (pretty, stream, compact)
|
|
3427
|
-
--
|
|
3428
|
-
|
|
3895
|
+
--include <types> Include change types (comma-separated): committed, staged, unstaged, untracked
|
|
3896
|
+
\u2022 committed: All committed changes (modified, deleted, renamed)
|
|
3897
|
+
\u2022 staged: Only staged changes ready to commit
|
|
3898
|
+
\u2022 unstaged: Only unstaged tracked file modifications
|
|
3899
|
+
\u2022 untracked: Only new untracked files
|
|
3900
|
+
(default: all four types)
|
|
3429
3901
|
-d, --debug Enable debug output
|
|
3430
3902
|
|
|
3903
|
+
Diff examples:
|
|
3904
|
+
$ wispbit diff Show all changes (default: committed,staged,unstaged,untracked)
|
|
3905
|
+
$ wispbit diff --include staged Show only staged changes
|
|
3906
|
+
$ wispbit diff --include staged,untracked Show staged and untracked changes
|
|
3907
|
+
$ wispbit diff HEAD~3 Compare working directory against 3 commits ago
|
|
3908
|
+
$ wispbit diff main..HEAD Compare against main branch including worktree changes
|
|
3909
|
+
$ wispbit abc123 --include committed Compare against specific commit SHA, show only committed
|
|
3431
3910
|
|
|
3432
3911
|
Global options:
|
|
3433
3912
|
-v, --version Show version number
|
|
@@ -3443,10 +3922,8 @@ Global options:
|
|
|
3443
3922
|
json: {
|
|
3444
3923
|
type: "string"
|
|
3445
3924
|
},
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
},
|
|
3449
|
-
head: {
|
|
3925
|
+
// Diff include
|
|
3926
|
+
include: {
|
|
3450
3927
|
type: "string"
|
|
3451
3928
|
},
|
|
3452
3929
|
// Debug option
|
|
@@ -3472,14 +3949,43 @@ async function executeCommand(options) {
|
|
|
3472
3949
|
const environment = new Environment();
|
|
3473
3950
|
const config = await ensureConfigured();
|
|
3474
3951
|
const { ruleId, json: json2, mode, filePath } = options;
|
|
3952
|
+
let { commitSelector, diffInclude } = options;
|
|
3953
|
+
if (mode === "diff") {
|
|
3954
|
+
const repoRoot = environment.getWorkspaceRoot();
|
|
3955
|
+
if (!commitSelector) {
|
|
3956
|
+
const defaults = await getDefaultCommitSelector(repoRoot);
|
|
3957
|
+
commitSelector = defaults.commitSelector;
|
|
3958
|
+
if (!diffInclude) {
|
|
3959
|
+
diffInclude = defaults.include;
|
|
3960
|
+
}
|
|
3961
|
+
} else {
|
|
3962
|
+
if (!diffInclude) {
|
|
3963
|
+
const defaults = await getDefaultCommitSelector(repoRoot);
|
|
3964
|
+
diffInclude = defaults.include;
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
3967
|
+
}
|
|
3968
|
+
if (mode === "diff" && commitSelector && diffInclude) {
|
|
3969
|
+
const repoRoot = environment.getWorkspaceRoot();
|
|
3970
|
+
const currentBranch = await getCurrentBranch(repoRoot);
|
|
3971
|
+
const validationError = validateWorktreeIncludes(commitSelector, diffInclude, currentBranch);
|
|
3972
|
+
if (validationError) {
|
|
3973
|
+
console.error(chalk4.red(validationError));
|
|
3974
|
+
process.exit(1);
|
|
3975
|
+
}
|
|
3976
|
+
}
|
|
3475
3977
|
let jsonOutput = false;
|
|
3476
3978
|
let jsonFormat = "pretty";
|
|
3477
|
-
if (json2) {
|
|
3979
|
+
if (json2 !== void 0) {
|
|
3478
3980
|
jsonOutput = true;
|
|
3479
|
-
if (json2 === "
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3981
|
+
if (typeof json2 === "string") {
|
|
3982
|
+
if (json2 === "stream") {
|
|
3983
|
+
jsonFormat = "stream";
|
|
3984
|
+
} else if (json2 === "compact") {
|
|
3985
|
+
jsonFormat = "compact";
|
|
3986
|
+
} else {
|
|
3987
|
+
jsonFormat = "pretty";
|
|
3988
|
+
}
|
|
3483
3989
|
} else {
|
|
3484
3990
|
jsonFormat = "pretty";
|
|
3485
3991
|
}
|
|
@@ -3508,26 +4014,30 @@ async function executeCommand(options) {
|
|
|
3508
4014
|
}
|
|
3509
4015
|
throw error;
|
|
3510
4016
|
}
|
|
3511
|
-
if (!
|
|
4017
|
+
if (!jsonOutput) {
|
|
3512
4018
|
if (mode === "check") {
|
|
3513
4019
|
const modeBox = chalk4.bgHex("#eab308").black(" CHECK ");
|
|
3514
4020
|
const targetInfo = chalk4.dim(` (${filePath || "all files"})`);
|
|
3515
4021
|
console.log(`${modeBox}${targetInfo}`);
|
|
3516
4022
|
} else {
|
|
3517
|
-
const baseCommit = cli.flags.base || "origin/main";
|
|
3518
|
-
const headCommit = cli.flags.head || "HEAD";
|
|
3519
4023
|
const modeBox = chalk4.bgHex("#4a90e2").black(" DIFF ");
|
|
3520
|
-
const
|
|
3521
|
-
|
|
4024
|
+
const truncatedRange = (commitSelector || "").replace(
|
|
4025
|
+
/\b[0-9a-f]{30,}\b/gi,
|
|
4026
|
+
(match) => match.substring(0, 7)
|
|
4027
|
+
);
|
|
4028
|
+
const includeInfo = diffInclude ? ` ${truncatedRange} ${chalk4.dim(`[${diffInclude.join("/")}]`)}` : ` ${truncatedRange}`;
|
|
4029
|
+
console.log(`${modeBox}${includeInfo}`);
|
|
3522
4030
|
}
|
|
3523
4031
|
}
|
|
3524
4032
|
const eventEmitter = new ExecutionEventEmitter();
|
|
3525
4033
|
if (mode === "check") {
|
|
3526
4034
|
eventEmitter.setExecutionMode("check", { filePath });
|
|
3527
4035
|
} else {
|
|
3528
|
-
const
|
|
3529
|
-
|
|
3530
|
-
|
|
4036
|
+
const includeDisplay = diffInclude ? diffInclude.join(", ") : "all";
|
|
4037
|
+
eventEmitter.setExecutionMode("diff", {
|
|
4038
|
+
baseCommit: commitSelector || "",
|
|
4039
|
+
headCommit: includeDisplay
|
|
4040
|
+
});
|
|
3531
4041
|
}
|
|
3532
4042
|
const cleanupTerminalReporter = !options.json ? setupTerminalReporter(eventEmitter, options.debug || false) : void 0;
|
|
3533
4043
|
const executionStartTime = Date.now();
|
|
@@ -3539,7 +4049,10 @@ async function executeCommand(options) {
|
|
|
3539
4049
|
const ruleResults = await ruleExecutor.execute(rules, {
|
|
3540
4050
|
mode,
|
|
3541
4051
|
filePath,
|
|
3542
|
-
|
|
4052
|
+
diffOptions: mode === "diff" ? {
|
|
4053
|
+
include: diffInclude,
|
|
4054
|
+
commitSelector
|
|
4055
|
+
} : void 0
|
|
3543
4056
|
});
|
|
3544
4057
|
const results = [];
|
|
3545
4058
|
for (const ruleResult of ruleResults) {
|
|
@@ -3574,9 +4087,16 @@ async function executeCommand(options) {
|
|
|
3574
4087
|
if (mode === "check") {
|
|
3575
4088
|
executionModeInfo = { mode: "check", filePath };
|
|
3576
4089
|
} else {
|
|
3577
|
-
const
|
|
3578
|
-
|
|
3579
|
-
|
|
4090
|
+
const truncatedRange = (commitSelector || "").replace(
|
|
4091
|
+
/\b[0-9a-f]{30,}\b/gi,
|
|
4092
|
+
(match) => match.substring(0, 7)
|
|
4093
|
+
);
|
|
4094
|
+
const includeDisplay = diffInclude ? diffInclude.join(", ") : "all";
|
|
4095
|
+
executionModeInfo = {
|
|
4096
|
+
mode: "diff",
|
|
4097
|
+
baseCommit: truncatedRange,
|
|
4098
|
+
headCommit: includeDisplay
|
|
4099
|
+
};
|
|
3580
4100
|
}
|
|
3581
4101
|
const executionTime = Date.now() - executionStartTime;
|
|
3582
4102
|
printSummary(results, {
|
|
@@ -3616,11 +4136,30 @@ async function main() {
|
|
|
3616
4136
|
break;
|
|
3617
4137
|
}
|
|
3618
4138
|
case "diff": {
|
|
4139
|
+
let diffInclude;
|
|
4140
|
+
if (cli.flags.include) {
|
|
4141
|
+
const includeString = cli.flags.include;
|
|
4142
|
+
const includes = includeString.split(",").map((f) => f.trim());
|
|
4143
|
+
const validIncludes = ["committed", "staged", "unstaged", "untracked"];
|
|
4144
|
+
const invalidIncludes = includes.filter((f) => !validIncludes.includes(f));
|
|
4145
|
+
if (invalidIncludes.length > 0) {
|
|
4146
|
+
console.error(
|
|
4147
|
+
chalk4.red(
|
|
4148
|
+
`Invalid include(s): ${invalidIncludes.join(", ")}. Valid includes: ${validIncludes.join(", ")}`
|
|
4149
|
+
)
|
|
4150
|
+
);
|
|
4151
|
+
process.exit(1);
|
|
4152
|
+
}
|
|
4153
|
+
diffInclude = includes;
|
|
4154
|
+
}
|
|
4155
|
+
const commitSelector = cli.input[1];
|
|
3619
4156
|
await executeCommand({
|
|
3620
4157
|
ruleId: cli.flags.rule,
|
|
3621
4158
|
json: cli.flags.json,
|
|
3622
4159
|
debug: cli.flags.debug,
|
|
3623
|
-
mode: "diff"
|
|
4160
|
+
mode: "diff",
|
|
4161
|
+
diffInclude,
|
|
4162
|
+
commitSelector
|
|
3624
4163
|
});
|
|
3625
4164
|
break;
|
|
3626
4165
|
}
|
|
@@ -3645,18 +4184,31 @@ ${chalk4.green("Cache purged successfully.")} Removed ${result.deletedCount} ite
|
|
|
3645
4184
|
break;
|
|
3646
4185
|
}
|
|
3647
4186
|
default: {
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
4187
|
+
let diffInclude;
|
|
4188
|
+
if (cli.flags.include) {
|
|
4189
|
+
const includeString = cli.flags.include;
|
|
4190
|
+
const includes = includeString.split(",").map((f) => f.trim());
|
|
4191
|
+
const validIncludes = ["committed", "staged", "unstaged", "untracked"];
|
|
4192
|
+
const invalidIncludes = includes.filter((f) => !validIncludes.includes(f));
|
|
4193
|
+
if (invalidIncludes.length > 0) {
|
|
4194
|
+
console.error(
|
|
4195
|
+
chalk4.red(
|
|
4196
|
+
`Invalid include(s): ${invalidIncludes.join(", ")}. Valid includes: ${validIncludes.join(", ")}`
|
|
4197
|
+
)
|
|
4198
|
+
);
|
|
4199
|
+
process.exit(1);
|
|
4200
|
+
}
|
|
4201
|
+
diffInclude = includes;
|
|
3659
4202
|
}
|
|
4203
|
+
const commitSelector = cli.input[0];
|
|
4204
|
+
await executeCommand({
|
|
4205
|
+
ruleId: cli.flags.rule,
|
|
4206
|
+
json: cli.flags.json,
|
|
4207
|
+
debug: cli.flags.debug,
|
|
4208
|
+
mode: "diff",
|
|
4209
|
+
diffInclude,
|
|
4210
|
+
commitSelector
|
|
4211
|
+
});
|
|
3660
4212
|
break;
|
|
3661
4213
|
}
|
|
3662
4214
|
}
|