@wispbit/local 1.0.25

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