@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.
Files changed (48) hide show
  1. package/dist/cli.js +1190 -638
  2. package/dist/cli.js.map +4 -4
  3. package/dist/index.js +10 -3857
  4. package/dist/index.js.map +4 -4
  5. package/dist/package.json +3 -2
  6. package/dist/src/api/WispbitApiClient.d.ts +185 -0
  7. package/dist/src/api/WispbitApiClient.d.ts.map +1 -0
  8. package/dist/src/cli.d.ts.map +1 -1
  9. package/dist/src/environment/Config.d.ts +15 -5
  10. package/dist/src/environment/Config.d.ts.map +1 -1
  11. package/dist/src/providers/ViolationValidationProvider.d.ts +7 -11
  12. package/dist/src/providers/ViolationValidationProvider.d.ts.map +1 -1
  13. package/dist/src/providers/WispbitRuleProvider.d.ts +0 -16
  14. package/dist/src/providers/WispbitRuleProvider.d.ts.map +1 -1
  15. package/dist/src/providers/WispbitViolationValidationProvider.d.ts +4 -7
  16. package/dist/src/providers/WispbitViolationValidationProvider.d.ts.map +1 -1
  17. package/dist/src/schemas.d.ts +13 -514
  18. package/dist/src/schemas.d.ts.map +1 -1
  19. package/dist/src/steps/ExecutionEventEmitter.d.ts +37 -2
  20. package/dist/src/steps/ExecutionEventEmitter.d.ts.map +1 -1
  21. package/dist/src/steps/FileExecutionContext.d.ts +14 -5
  22. package/dist/src/steps/FileExecutionContext.d.ts.map +1 -1
  23. package/dist/src/steps/FileFilterStep.d.ts +0 -7
  24. package/dist/src/steps/FileFilterStep.d.ts.map +1 -1
  25. package/dist/src/steps/GotoDefinitionStep.d.ts.map +1 -1
  26. package/dist/src/steps/LLMStep.d.ts +0 -10
  27. package/dist/src/steps/LLMStep.d.ts.map +1 -1
  28. package/dist/src/steps/RuleExecutor.d.ts +5 -1
  29. package/dist/src/steps/RuleExecutor.d.ts.map +1 -1
  30. package/dist/src/test/TestExecutor.d.ts +6 -1
  31. package/dist/src/test/TestExecutor.d.ts.map +1 -1
  32. package/dist/src/types.d.ts +1 -5
  33. package/dist/src/types.d.ts.map +1 -1
  34. package/dist/src/utils/debugLogger.d.ts +6 -0
  35. package/dist/src/utils/debugLogger.d.ts.map +1 -0
  36. package/dist/src/utils/formatters.d.ts.map +1 -1
  37. package/dist/src/utils/git.d.ts +31 -2
  38. package/dist/src/utils/git.d.ts.map +1 -1
  39. package/dist/src/utils/git.test.d.ts +2 -0
  40. package/dist/src/utils/git.test.d.ts.map +1 -0
  41. package/dist/src/utils/patternMatching.d.ts +2 -0
  42. package/dist/src/utils/patternMatching.d.ts.map +1 -0
  43. package/dist/src/utils/validateRule.d.ts +37 -1
  44. package/dist/src/utils/validateRule.d.ts.map +1 -1
  45. package/dist/src/validationSchemas.d.ts +553 -0
  46. package/dist/src/validationSchemas.d.ts.map +1 -0
  47. package/dist/tsconfig.tsbuildinfo +1 -1
  48. 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
- constructor(config) {
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
- ignoredGlobs,
112
- apiKey: finalApiKey || void 0,
113
- baseUrl: finalBaseUrl
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 isValidApiKey = await _Config.validateApiKey(finalApiKey, repositoryUrl, finalBaseUrl);
135
- if (!isValidApiKey) {
136
- return { failed: true, error: "INVALID_API_KEY" };
137
- }
138
- const response = await fetch(`${finalBaseUrl}/plv1/initialize`, {
139
- method: "POST",
140
- headers: {
141
- "Content-Type": "application/json",
142
- Authorization: `Bearer ${finalApiKey}`
143
- },
144
- body: JSON.stringify({
145
- repository_url: repositoryUrl,
146
- powerlint_version: getCurrentVersion(),
147
- schema_version: "v1"
148
- })
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
- var _a;
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
- const fetchUrlLine = stdout.split("\n").find((line) => line.includes("Fetch URL:"));
191
- if (fetchUrlLine) {
192
- return ((_a = fetchUrlLine.split("Fetch URL:").pop()) == null ? void 0 : _a.trim()) || null;
193
- }
194
- return null;
535
+ return stdout.trim() || null;
195
536
  }
196
537
  async function getDefaultBranch(repoRoot, remoteName = "origin") {
197
- var _a;
198
- const { stdout } = await execPromise(`git remote show ${remoteName}`, {
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
- const headBranchLine = stdout.split("\n").find((line) => line.includes("HEAD branch"));
202
- if (headBranchLine) {
203
- return ((_a = headBranchLine.split(":").pop()) == null ? void 0 : _a.trim()) || null;
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 getChangedFiles(repoRoot, base) {
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
- const compareTo = base ?? (defaultBranch ? `origin/${defaultBranch}` : "HEAD^");
215
- let currentCommit;
216
- try {
217
- const { stdout: remoteCommitOutput } = await execPromise(
218
- `git rev-parse origin/${currentBranch}`,
219
- { cwd: repoRoot }
220
- );
221
- currentCommit = remoteCommitOutput.trim();
222
- } catch (error) {
223
- const { stdout: currentCommitOutput } = await execPromise("git rev-parse HEAD", {
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
- currentCommit = currentCommitOutput.trim();
227
- }
228
- let mergeBase;
229
- try {
230
- const { stdout: mergeBaseOutput } = await execPromise(
231
- `git merge-base ${currentBranch} ${compareTo}`,
232
- { cwd: repoRoot }
233
- );
234
- mergeBase = mergeBaseOutput.trim();
235
- } catch (error) {
236
- mergeBase = "HEAD^";
764
+ const base = stdout.trim() ? originBranch : defaultBranch;
765
+ return {
766
+ commitSelector: `${base}...${currentBranch}`,
767
+ include: defaultInclude
768
+ };
237
769
  }
238
- const { stdout: statusOutput } = await execPromise("git status --porcelain", {
239
- cwd: repoRoot
240
- });
241
- const statusLines = statusOutput.split("\n").filter(Boolean);
242
- const fileStatuses = /* @__PURE__ */ new Map();
243
- statusLines.forEach((line) => {
244
- const statusCode = line.substring(0, 2).trim();
245
- const filename = line.substring(3);
246
- fileStatuses.set(filename, statusCode);
247
- });
248
- const { stdout: diffOutput } = await execPromise(`git diff ${mergeBase} --name-only`, {
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 allFiles = diffOutput.split("\n").filter(Boolean);
252
- const { stdout: deletedFilesOutput } = await execPromise("git ls-files --deleted", {
781
+ const currentBranch = currentBranchOutput.trim();
782
+ const { stdout: currentCommitOutput } = await execPromise("git rev-parse HEAD", {
253
783
  cwd: repoRoot
254
784
  });
255
- const deletedFiles = deletedFilesOutput.split("\n").filter(Boolean);
256
- allFiles.push(...deletedFiles.filter((file) => !allFiles.includes(file)));
257
- const { stdout: untrackedOutput } = await execPromise(
258
- "git ls-files --others --exclude-standard",
259
- {
260
- cwd: repoRoot
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 untrackedFiles = untrackedOutput.split("\n").filter(Boolean);
264
- allFiles.push(...untrackedFiles.filter((file) => !allFiles.includes(file)));
265
- const nonDeletedFiles = [];
266
- const deletedFilesSet = new Set(deletedFiles);
267
- const fileIsDeleted = /* @__PURE__ */ new Map();
268
- for (const file of allFiles) {
269
- const isDeleted = deletedFilesSet.has(file) || ((_a = fileStatuses.get(file)) == null ? void 0 : _a.includes("D")) || ((_b = fileStatuses.get(file)) == null ? void 0 : _b.includes("R"));
270
- fileIsDeleted.set(file, Boolean(isDeleted));
271
- if (!isDeleted) {
272
- nonDeletedFiles.push(file);
273
- }
274
- }
275
- const fileStats = /* @__PURE__ */ new Map();
276
- if (nonDeletedFiles.length > 0) {
277
- const { stdout: batchNumstatOutput } = await execPromise(
278
- `git diff ${mergeBase} --numstat -- ${nonDeletedFiles.map((f) => `'${f.replace(/'/g, "'\\''")}'`).join(" ")}`,
279
- { cwd: repoRoot }
280
- );
281
- const numstatLines = batchNumstatOutput.split("\n").filter(Boolean);
282
- numstatLines.forEach((line) => {
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 [additionsStr, deletionsStr, filename] = parts;
286
- fileStats.set(filename, {
287
- additions: parseInt(additionsStr) || 0,
288
- deletions: parseInt(deletionsStr) || 0
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
- const fileDiffs = /* @__PURE__ */ new Map();
294
- if (nonDeletedFiles.length > 0) {
295
- const { stdout: batchDiffOutput } = await execPromise(
296
- `git diff ${mergeBase} -- ${nonDeletedFiles.map((f) => `'${f.replace(/'/g, "'\\''")}'`).join(" ")}`,
297
- { cwd: repoRoot }
298
- );
299
- const diffSections = batchDiffOutput.split(/^diff --git /m).filter(Boolean);
300
- diffSections.forEach((section) => {
301
- const lines = section.split("\n");
302
- const firstLine = lines[0];
303
- const match = firstLine.match(/a\/(.+?) b\//);
304
- if (match) {
305
- const filename = match[1];
306
- const diffContent = lines.slice(1).filter((line) => {
307
- return !(line.startsWith("index ") || line.startsWith("--- ") || line.startsWith("+++ "));
308
- }).join("\n");
309
- fileDiffs.set(filename, diffContent);
310
- }
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
- const deletedFileContents = /* @__PURE__ */ new Map();
314
- const actualDeletedFiles = allFiles.filter((file) => fileIsDeleted.get(file));
315
- if (actualDeletedFiles.length > 0) {
316
- const deletedFilePromises = actualDeletedFiles.map(async (file) => {
317
- const { stdout: lastContent } = await execPromise(
318
- `git show '${mergeBase}:${file.replace(/'/g, "'\\''")}'`,
319
- {
320
- cwd: repoRoot
321
- }
322
- );
323
- return { file, content: lastContent };
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 results = await Promise.allSettled(deletedFilePromises);
326
- results.forEach((result, index) => {
327
- if (result.status === "fulfilled") {
328
- deletedFileContents.set(actualDeletedFiles[index], result.value.content);
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 fileChanges = [];
333
- for (const file of allFiles) {
334
- const isDeleted = fileIsDeleted.get(file);
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
- let diffOutput2 = "";
338
- if (isDeleted) {
339
- const lastContent = deletedFileContents.get(file);
340
- if (lastContent) {
341
- deletions = lastContent.split("\n").length;
342
- diffOutput2 = lastContent.split("\n").map((line) => `-${line}`).join("\n");
343
- }
344
- } else {
345
- const stats = fileStats.get(file);
346
- if (stats) {
347
- additions = stats.additions;
348
- deletions = stats.deletions;
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
- const status = isDeleted ? "removed" : additions > 0 && deletions === 0 ? "added" : "modified";
353
- fileChanges.push({
354
- filename: file,
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: diffOutput2,
357
- additions,
358
- deletions,
359
- sha: hashString(diffOutput2)
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
- diffCommit: mergeBase,
367
- diffBranch: compareTo
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 path from "path";
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 path.join(os.homedir(), ".powerlint");
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 path.join(
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 path.join(
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 path.join(indexDir, `${language.toLowerCase()}-${file}`);
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 path.join(this.getCacheDirectory(), "cache.json");
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: path.join(cacheDir, "cache.json")
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 requestData = {
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
- GetRulesSchema.parse(requestData);
656
- const response = await this.makeApiRequest("/plv1/get-rules", requestData);
657
- if (!Array.isArray(response.rules)) {
658
- throw new Error("Invalid response format from Wispbit API: expected rules array");
659
- }
660
- const rules = response.rules;
661
- return rules.map((rule) => ({
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", { ruleId, stepName, stepType, outputs, executionTime });
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 path2 from "path";
1414
+ import * as path3 from "path";
810
1415
  import { glob } from "glob";
811
- import { minimatch } from "minimatch";
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, baseSha) {
1442
+ static async initialize(config, environment, mode, eventEmitter, filePath, diffOptions) {
829
1443
  const context = new _FileExecutionContext(config, environment, mode, eventEmitter);
830
- const initialFiles = await context.discoverFiles(filePath, baseSha);
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, baseSha) {
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
- if (baseSha) {
903
- this.eventEmitter.fileDiscoveryProgress(`Getting changed files from ${baseSha}...`);
904
- } else {
905
- this.eventEmitter.fileDiscoveryProgress("Getting changed files from git...");
906
- }
907
- const gitChanges = await getChangedFiles(workspaceRoot, baseSha);
908
- this.diffMode = {
909
- gitChanges,
910
- changedFiles: gitChanges.files.map((f) => f.filename),
911
- fileChangeMap: new Map(gitChanges.files.map((file) => [file.filename, file]))
912
- };
913
- this.eventEmitter.fileDiscoveryProgress(
914
- `Found ${this.diffMode.changedFiles.length} changed files`
915
- );
916
- discoveredFiles = this.diffMode.changedFiles.filter((file) => fs2.existsSync(path2.resolve(workspaceRoot, file))).map((file) => file);
917
- this.eventEmitter.fileDiscoveryProgress("Applying ignore patterns...");
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((filePath2) => {
922
- const matchesIgnore = allIgnorePatterns.some(
923
- (pattern) => this.matchesPattern(filePath2, pattern)
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([workspaceRoot], allIgnorePatterns);
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 = path2.resolve(workspaceRoot, filePath);
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
- const matchesIgnore = allIgnorePatterns.some(
962
- (pattern) => this.matchesPattern(filePath, pattern)
963
- );
964
- if (matchesIgnore) {
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
- this.eventEmitter.fileDiscoveryProgress(`Found ${matches.length} files matching pattern`);
996
- return matches;
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 = path2.join(dirPath, "**/*").replace(/\\/g, "/");
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
- this.eventEmitter.fileDiscoveryProgress(`Found ${matches.length} files in directory`);
1012
- return matches;
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 shouldIgnore = ignorePatterns.some((pattern) => this.matchesPattern(dir, pattern));
1025
- if (!shouldIgnore) {
1026
- allFiles.push(path2.relative(workspaceRoot, dir));
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 = path2.resolve(dir, match);
1039
- return path2.relative(workspaceRoot, absolutePath);
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 path3 from "path";
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 = path3.resolve(workspaceRoot, filePath);
1793
+ const absoluteFilePath = path4.resolve(workspaceRoot, filePath);
1166
1794
  if ("fs.siblingExists" in condition) {
1167
1795
  const { filename } = condition["fs.siblingExists"];
1168
- const dir = path3.dirname(absoluteFilePath);
1169
- const siblingPath = path3.join(dir, filename);
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 = path3.dirname(absoluteFilePath);
1179
- const root = path3.parse(currentDir).root;
1802
+ let currentDir = path4.dirname(absoluteFilePath);
1803
+ const root = path4.parse(currentDir).root;
1180
1804
  while (currentDir !== root) {
1181
- const targetPath = path3.join(currentDir, filename);
1805
+ const targetPath = path4.join(currentDir, filename);
1182
1806
  if (fs3.existsSync(targetPath)) {
1183
1807
  return true;
1184
1808
  }
1185
- const parentDir = path3.dirname(currentDir);
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 = path3.dirname(absoluteFilePath);
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) => this.matchesPattern(sibling, pattern));
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) => options.include.some((pattern) => this.matchesPattern(filePath, pattern))
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((filePath) => {
1232
- const matchesIgnore = options.ignore.some(
1233
- (pattern) => this.matchesPattern(filePath, pattern)
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 { minimatch as minimatch3 } from "minimatch";
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
- return minimatch3(relativePath, spec.glob);
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
- constructor(config) {
2815
+ useInternalEndpoint;
2816
+ constructor(config, useInternalEndpoint = false) {
2314
2817
  this.config = config;
2818
+ this.useInternalEndpoint = useInternalEndpoint;
2315
2819
  }
2316
2820
  async validateViolations(params) {
2317
- var _a;
2318
- const matchesWithIds = params.matches.map((match) => ({
2319
- match,
2320
- matchId: this.generateMatchId(match)
2321
- }));
2322
- const baseUrl = this.config.getBaseUrl();
2323
- const apiKey = this.config.getApiKey();
2324
- const response = await fetch(`${baseUrl}/plv1/validate-violation`, {
2325
- method: "POST",
2326
- headers: {
2327
- "Content-Type": "application/json",
2328
- Authorization: `Bearer ${apiKey}`
2329
- },
2330
- body: JSON.stringify({
2331
- rule: params.rule,
2332
- matches: matchesWithIds.map(({ match, matchId }) => ({
2333
- matchId,
2334
- filePath: match.filePath,
2335
- range: match.range,
2336
- text: match.text,
2337
- language: match.language,
2338
- symbol: match.symbol,
2339
- source: match.source
2340
- })),
2341
- powerlint_version: this.config.getLocalVersion(),
2342
- schema_version: this.config.getSchemaVersion()
2343
- })
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
- if (!response.ok) {
2346
- const errorText = await response.text();
2347
- throw new Error(
2348
- `Wispbit validation failed: ${response.status} ${response.statusText} - ${errorText}`
2349
- );
2350
- }
2351
- const result = await response.json();
2352
- const results = [];
2353
- for (const { matchId } of matchesWithIds) {
2354
- const validationResult = (_a = result.results) == null ? void 0 : _a.find((r) => r.matchId === matchId);
2355
- results.push({
2356
- matchId,
2357
- isViolation: Boolean(validationResult == null ? void 0 : validationResult.isViolation),
2358
- reason: String((validationResult == null ? void 0 : validationResult.reason) || "No violation detected"),
2359
- confidence: typeof (validationResult == null ? void 0 : validationResult.confidence) === "number" ? validationResult.confidence : 1
2360
- });
2361
- }
2362
- return results;
2363
- }
2364
- /**
2365
- * Generate a unique ID for a single match
2366
- */
2367
- generateMatchId(match) {
2368
- const matchData = {
2369
- filePath: match.filePath,
2370
- startLine: match.range.start.line,
2371
- startColumn: match.range.start.column || 0,
2372
- endLine: match.range.end.line,
2373
- endColumn: match.range.end.column || 0,
2374
- text: match.text
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 unique ID for a single match
2394
- * Hash is based on filePath + range + text
2854
+ * Generate a cache key for a single match LLM validation
2395
2855
  */
2396
- generateMatchId(match) {
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 hashString(JSON.stringify(matchData)).substring(0, 16);
2406
- }
2407
- /**
2408
- * Generate a hash for a prompt string
2409
- */
2410
- hashPrompt(prompt) {
2411
- return hashString(prompt);
2412
- }
2413
- /**
2414
- * Generate a cache key for a single match LLM validation
2415
- */
2416
- generateMatchCacheKey(matchId, promptHash) {
2417
- return `match_${matchId}_prompt_${promptHash}.json`;
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 promptHash = this.hashPrompt(rule.prompt);
2428
- const validationProvider = new WispbitViolationValidationProvider(this.config);
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 validationResults = await validationProvider.validateViolations(validationParams);
2435
- const newViolationMatches = [];
2436
- for (const result of validationResults) {
2437
- const originalMatch = uncachedMatches.find(
2438
- (match) => this.generateMatchId(match) === result.matchId
2439
- );
2440
- if (originalMatch && result.isViolation) {
2441
- newViolationMatches.push({
2442
- ...originalMatch,
2443
- metadata: {
2444
- fromCache: false,
2445
- llmValidation: {
2446
- isViolation: true,
2447
- confidence: result.confidence,
2448
- reason: result.reason
2449
- }
2450
- }
2451
- });
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
- const cacheKey = this.generateMatchCacheKey(result.matchId, promptHash);
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
- matchId: result.matchId,
2456
- isViolation: result.isViolation,
2457
- confidence: result.confidence,
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 matchId = this.generateMatchId(match);
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.baseSha
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.emit("step:complete", {
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
- return chalk.underline.dim(
2736
- `\x1B]8;;https://app.wispbit.com/rules/${internalId}\x1B\\${ruleId}\x1B]8;;\x1B\\`
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 <= 10);
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 <= 10) {
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
- output += `${violationText}
3366
+ let summaryOutput = "";
3367
+ summaryOutput += `${violationText}
2903
3368
  `;
2904
- output += `${suggestionText}
3369
+ summaryOutput += `${suggestionText}
2905
3370
  `;
2906
- output += `${chalk.dim("summary".padStart(12))} ${detailsText}
3371
+ summaryOutput += `${chalk.dim("summary".padStart(12))} ${detailsText}
2907
3372
  `;
2908
- console.log(total > 0 ? chalk.reset(output) : output);
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] Run linting rules against a specific file/folder or entire codebase
3415
- diff Run linting rules only on changed files in the current PR
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
- --base <commit> Base commit/branch/SHA to compare against (defaults to origin/main)
3428
- --head <commit> Head commit/branch/SHA to compare against (defaults to current branch)
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
- base: {
3447
- type: "string"
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 === "stream") {
3480
- jsonFormat = "stream";
3481
- } else if (json2 === "compact") {
3482
- jsonFormat = "compact";
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 (!json2) {
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 branchInfo = chalk4.dim(` (${headCommit}..${baseCommit})`);
3521
- console.log(`${modeBox}${branchInfo}`);
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 baseCommit = cli.flags.base || "origin/main";
3529
- const headCommit = cli.flags.head || "current branch";
3530
- eventEmitter.setExecutionMode("diff", { baseCommit, headCommit });
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
- baseSha: cli.flags.base
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 baseCommit = cli.flags.base || "origin/main";
3578
- const headCommit = cli.flags.head || "current branch";
3579
- executionModeInfo = { mode: "diff", baseCommit, headCommit };
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
- if (!command) {
3649
- await executeCommand({
3650
- ruleId: cli.flags.rule,
3651
- json: cli.flags.json,
3652
- debug: cli.flags.debug,
3653
- mode: "diff"
3654
- });
3655
- } else {
3656
- console.error(chalk4.red("Unknown command:"), command);
3657
- cli.showHelp();
3658
- process.exit(1);
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
  }