pi-lens 1.1.1 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/clients/todo-scanner.ts +48 -0
- package/index.ts +495 -430
- package/package.json +48 -48
package/clients/todo-scanner.ts
CHANGED
|
@@ -31,6 +31,49 @@ export interface TodoScanResult {
|
|
|
31
31
|
export class TodoScanner {
|
|
32
32
|
private readonly pattern = /\b(TODO|FIXME|HACK|XXX|NOTE|DEPRECATED|BUG)\b\s*[\(:]?\s*(.+)/gi;
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Check if a match position is inside a comment context.
|
|
36
|
+
* Handles: // line comments, star-slash block comments, * JSDoc lines, # Python comments
|
|
37
|
+
*/
|
|
38
|
+
private isInComment(line: string, matchIndex: number): boolean {
|
|
39
|
+
const trimmed = line.trimStart();
|
|
40
|
+
|
|
41
|
+
// Line starts with comment markers — entire line is a comment
|
|
42
|
+
if (/^\/\/|^\/\*|^\*|^#/.test(trimmed)) return true;
|
|
43
|
+
|
|
44
|
+
// Check if there's a // before the match position (not inside a string)
|
|
45
|
+
const beforeMatch = line.slice(0, matchIndex);
|
|
46
|
+
const lineCommentPos = beforeMatch.lastIndexOf("//");
|
|
47
|
+
if (lineCommentPos !== -1) {
|
|
48
|
+
// Count quotes before // to see if it's inside a string
|
|
49
|
+
const beforeComment = beforeMatch.slice(0, lineCommentPos);
|
|
50
|
+
const singleQuotes = (beforeComment.match(/'/g) || []).length;
|
|
51
|
+
const doubleQuotes = (beforeComment.match(/"/g) || []).length;
|
|
52
|
+
const backticks = (beforeComment.match(/`/g) || []).length;
|
|
53
|
+
if (singleQuotes % 2 === 0 && doubleQuotes % 2 === 0 && backticks % 2 === 0) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check for /* ... */ block comment before match
|
|
59
|
+
const blockOpen = beforeMatch.lastIndexOf("/*");
|
|
60
|
+
const blockClose = beforeMatch.lastIndexOf("*/");
|
|
61
|
+
if (blockOpen !== -1 && blockClose < blockOpen) return true;
|
|
62
|
+
|
|
63
|
+
// Check for # comment (Python)
|
|
64
|
+
const hashPos = beforeMatch.lastIndexOf("#");
|
|
65
|
+
if (hashPos !== -1) {
|
|
66
|
+
const beforeHash = beforeMatch.slice(0, hashPos);
|
|
67
|
+
const singleQuotes = (beforeHash.match(/'/g) || []).length;
|
|
68
|
+
const doubleQuotes = (beforeHash.match(/"/g) || []).length;
|
|
69
|
+
if (singleQuotes % 2 === 0 && doubleQuotes % 2 === 0) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
34
77
|
/**
|
|
35
78
|
* Scan a single file for TODOs
|
|
36
79
|
*/
|
|
@@ -47,6 +90,9 @@ export class TodoScanner {
|
|
|
47
90
|
const matches = line.matchAll(this.pattern);
|
|
48
91
|
|
|
49
92
|
for (const match of matches) {
|
|
93
|
+
// Skip matches that aren't inside comments
|
|
94
|
+
if (!this.isInComment(line, match.index ?? 0)) continue;
|
|
95
|
+
|
|
50
96
|
const type = match[1].toUpperCase() as TodoItem["type"];
|
|
51
97
|
const message = (match[2] || "").trim().replace(/\s*\*\/\s*$/, ""); // Strip closing comment
|
|
52
98
|
|
|
@@ -82,6 +128,8 @@ export class TodoScanner {
|
|
|
82
128
|
if (["node_modules", ".git", "dist", "build", ".next", "coverage"].includes(entry.name)) continue;
|
|
83
129
|
scan(fullPath);
|
|
84
130
|
} else if (extensions.some(ext => entry.name.endsWith(ext))) {
|
|
131
|
+
// Skip this scanner file — its own type literals and regex cause false positives
|
|
132
|
+
if (entry.name === "todo-scanner.ts" || entry.name === "todo-scanner.js") continue;
|
|
85
133
|
items.push(...this.scanFile(fullPath));
|
|
86
134
|
}
|
|
87
135
|
}
|
package/index.ts
CHANGED
|
@@ -25,453 +25,518 @@
|
|
|
25
25
|
* - pip: ruff
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
import
|
|
28
|
+
import * as nodeFs from "node:fs";
|
|
29
|
+
import * as os from "node:os";
|
|
30
|
+
import * as path from "node:path";
|
|
31
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
29
32
|
import { isToolCallEventType } from "@mariozechner/pi-coding-agent";
|
|
30
|
-
|
|
31
|
-
import { TypeScriptClient } from "./clients/typescript-client.js";
|
|
32
33
|
import { AstGrepClient } from "./clients/ast-grep-client.js";
|
|
33
|
-
import { RuffClient } from "./clients/ruff-client.js";
|
|
34
34
|
import { BiomeClient } from "./clients/biome-client.js";
|
|
35
|
+
import { DependencyChecker } from "./clients/dependency-checker.js";
|
|
36
|
+
import { JscpdClient } from "./clients/jscpd-client.js";
|
|
35
37
|
import { KnipClient } from "./clients/knip-client.js";
|
|
38
|
+
import { RuffClient } from "./clients/ruff-client.js";
|
|
36
39
|
import { TodoScanner } from "./clients/todo-scanner.js";
|
|
37
|
-
import { JscpdClient } from "./clients/jscpd-client.js";
|
|
38
40
|
import { TypeCoverageClient } from "./clients/type-coverage-client.js";
|
|
39
|
-
import {
|
|
40
|
-
import * as path from "node:path";
|
|
41
|
-
import * as nodeFs from "node:fs";
|
|
41
|
+
import { TypeScriptClient } from "./clients/typescript-client.js";
|
|
42
42
|
|
|
43
|
-
const DEBUG_LOG = "
|
|
43
|
+
const DEBUG_LOG = path.join(os.homedir(), "pi-lens-debug.log");
|
|
44
44
|
function dbg(msg: string) {
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
const line = `[${new Date().toISOString()}] ${msg}\n`;
|
|
46
|
+
try {
|
|
47
|
+
nodeFs.appendFileSync(DEBUG_LOG, line);
|
|
48
|
+
} catch (e) {
|
|
49
|
+
console.error("[pi-lens-debug] write failed:", e);
|
|
50
|
+
}
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
// --- State ---
|
|
50
54
|
|
|
51
|
-
let
|
|
55
|
+
let _verbose = false;
|
|
52
56
|
|
|
53
57
|
function log(msg: string) {
|
|
54
|
-
|
|
58
|
+
console.log(`[pi-lens] ${msg}`);
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
// --- Extension ---
|
|
58
62
|
|
|
59
63
|
export default function (pi: ExtensionAPI) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
64
|
+
log("Extension loaded");
|
|
65
|
+
|
|
66
|
+
const tsClient = new TypeScriptClient();
|
|
67
|
+
const astGrepClient = new AstGrepClient();
|
|
68
|
+
const ruffClient = new RuffClient();
|
|
69
|
+
const biomeClient = new BiomeClient();
|
|
70
|
+
const knipClient = new KnipClient();
|
|
71
|
+
const todoScanner = new TodoScanner();
|
|
72
|
+
const jscpdClient = new JscpdClient();
|
|
73
|
+
const typeCoverageClient = new TypeCoverageClient();
|
|
74
|
+
const depChecker = new DependencyChecker();
|
|
75
|
+
|
|
76
|
+
// --- Flags ---
|
|
77
|
+
|
|
78
|
+
pi.registerFlag("lens-verbose", {
|
|
79
|
+
description: "Enable verbose pi-lens logging",
|
|
80
|
+
type: "boolean",
|
|
81
|
+
default: false,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
pi.registerFlag("no-biome", {
|
|
85
|
+
description: "Disable Biome linting/formatting",
|
|
86
|
+
type: "boolean",
|
|
87
|
+
default: false,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
pi.registerFlag("no-ast-grep", {
|
|
91
|
+
description: "Disable ast-grep structural analysis",
|
|
92
|
+
type: "boolean",
|
|
93
|
+
default: false,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
pi.registerFlag("no-ruff", {
|
|
97
|
+
description: "Disable Ruff Python linting",
|
|
98
|
+
type: "boolean",
|
|
99
|
+
default: false,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
pi.registerFlag("no-lsp", {
|
|
103
|
+
description: "Disable TypeScript LSP",
|
|
104
|
+
type: "boolean",
|
|
105
|
+
default: false,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
pi.registerFlag("no-madge", {
|
|
109
|
+
description: "Disable circular dependency checking via madge",
|
|
110
|
+
type: "boolean",
|
|
111
|
+
default: false,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
pi.registerFlag("autofix-biome", {
|
|
115
|
+
description:
|
|
116
|
+
"Auto-fix Biome lint/format issues on write (applies --write --unsafe)",
|
|
117
|
+
type: "boolean",
|
|
118
|
+
default: true,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
pi.registerFlag("autofix-ruff", {
|
|
122
|
+
description: "Auto-fix Ruff lint/format issues on write",
|
|
123
|
+
type: "boolean",
|
|
124
|
+
default: true,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// --- Commands ---
|
|
128
|
+
|
|
129
|
+
pi.registerCommand("find-todos", {
|
|
130
|
+
description:
|
|
131
|
+
"Scan for TODO/FIXME/HACK annotations. Usage: /find-todos [path]",
|
|
132
|
+
handler: async (args, ctx) => {
|
|
133
|
+
const targetPath = args.trim() || ctx.cwd || process.cwd();
|
|
134
|
+
ctx.ui.notify("🔍 Scanning for TODOs...", "info");
|
|
135
|
+
|
|
136
|
+
const result = todoScanner.scanDirectory(targetPath);
|
|
137
|
+
const report = todoScanner.formatResult(result);
|
|
138
|
+
|
|
139
|
+
if (report) {
|
|
140
|
+
ctx.ui.notify(report, "info");
|
|
141
|
+
} else {
|
|
142
|
+
ctx.ui.notify("✓ No TODOs found", "info");
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
pi.registerCommand("dead-code", {
|
|
148
|
+
description: "Check for unused exports, files, and dependencies",
|
|
149
|
+
handler: async (args, ctx) => {
|
|
150
|
+
if (!knipClient.isAvailable()) {
|
|
151
|
+
ctx.ui.notify("Knip not installed. Run: npm install -D knip", "error");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
ctx.ui.notify("🔍 Analyzing for dead code...", "info");
|
|
156
|
+
const result = knipClient.analyze(args.trim() || ctx.cwd);
|
|
157
|
+
const report = knipClient.formatResult(result);
|
|
158
|
+
|
|
159
|
+
if (report) {
|
|
160
|
+
ctx.ui.notify(report, "info");
|
|
161
|
+
} else {
|
|
162
|
+
ctx.ui.notify("✓ No dead code found", "info");
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
pi.registerCommand("check-deps", {
|
|
168
|
+
description: "Check for circular dependencies in the project",
|
|
169
|
+
handler: async (args, ctx) => {
|
|
170
|
+
if (!depChecker.isAvailable()) {
|
|
171
|
+
ctx.ui.notify(
|
|
172
|
+
"Madge not installed. Run: npm install -D madge",
|
|
173
|
+
"error",
|
|
174
|
+
);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
ctx.ui.notify("🔍 Scanning dependencies...", "info");
|
|
179
|
+
const { circular } = depChecker.scanProject(args.trim() || ctx.cwd);
|
|
180
|
+
const report = depChecker.formatScanResult(circular);
|
|
181
|
+
|
|
182
|
+
if (report) {
|
|
183
|
+
ctx.ui.notify(report, "warning");
|
|
184
|
+
} else {
|
|
185
|
+
ctx.ui.notify("✓ No circular dependencies found", "info");
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
pi.registerCommand("format", {
|
|
191
|
+
description:
|
|
192
|
+
"Apply Biome formatting to files. Usage: /format [file-path] or /format --all",
|
|
193
|
+
handler: async (args, ctx) => {
|
|
194
|
+
if (!biomeClient.isAvailable()) {
|
|
195
|
+
ctx.ui.notify(
|
|
196
|
+
"Biome not installed. Run: npm install -D @biomejs/biome",
|
|
197
|
+
"error",
|
|
198
|
+
);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const arg = args.trim();
|
|
203
|
+
|
|
204
|
+
if (!arg || arg === "--all") {
|
|
205
|
+
ctx.ui.notify("🔍 Formatting all files...", "info");
|
|
206
|
+
|
|
207
|
+
let formatted = 0;
|
|
208
|
+
let skipped = 0;
|
|
209
|
+
|
|
210
|
+
const formatDir = (dir: string) => {
|
|
211
|
+
if (!require("node:fs").existsSync(dir)) return;
|
|
212
|
+
const entries = require("node:fs").readdirSync(dir, {
|
|
213
|
+
withFileTypes: true,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
for (const entry of entries) {
|
|
217
|
+
const fullPath = path.join(dir, entry.name);
|
|
218
|
+
if (entry.isDirectory()) {
|
|
219
|
+
if (
|
|
220
|
+
["node_modules", ".git", "dist", "build", ".next"].includes(
|
|
221
|
+
entry.name,
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
continue;
|
|
225
|
+
formatDir(fullPath);
|
|
226
|
+
} else if (/\.(ts|tsx|js|jsx|json|css)$/.test(entry.name)) {
|
|
227
|
+
const result = biomeClient.formatFile(fullPath);
|
|
228
|
+
if (result.changed) formatted++;
|
|
229
|
+
else if (result.success) skipped++;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
formatDir(ctx.cwd || process.cwd());
|
|
235
|
+
ctx.ui.notify(
|
|
236
|
+
`✓ Formatted ${formatted} file(s), ${skipped} already clean`,
|
|
237
|
+
"info",
|
|
238
|
+
);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const filePath = path.resolve(arg);
|
|
243
|
+
const result = biomeClient.formatFile(filePath);
|
|
244
|
+
|
|
245
|
+
if (result.success && result.changed) {
|
|
246
|
+
ctx.ui.notify(`✓ Formatted ${path.basename(filePath)}`, "info");
|
|
247
|
+
} else if (result.success) {
|
|
248
|
+
ctx.ui.notify(`✓ ${path.basename(filePath)} already clean`, "info");
|
|
249
|
+
} else {
|
|
250
|
+
ctx.ui.notify(`⚠️ Format failed: ${result.error}`, "error");
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Delivered once into the first tool_result of the session, then cleared
|
|
256
|
+
let sessionSummary: string | null = null;
|
|
257
|
+
|
|
258
|
+
// --- Events ---
|
|
259
|
+
|
|
260
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
261
|
+
_verbose = !!pi.getFlag("lens-verbose");
|
|
262
|
+
dbg("session_start fired");
|
|
263
|
+
|
|
264
|
+
// Log available tools
|
|
265
|
+
const tools: string[] = [];
|
|
266
|
+
tools.push("TypeScript LSP"); // Always available
|
|
267
|
+
if (biomeClient.isAvailable()) tools.push("Biome");
|
|
268
|
+
if (astGrepClient.isAvailable()) tools.push("ast-grep");
|
|
269
|
+
if (ruffClient.isAvailable()) tools.push("Ruff");
|
|
270
|
+
if (knipClient.isAvailable()) tools.push("Knip");
|
|
271
|
+
if (depChecker.isAvailable()) tools.push("Madge");
|
|
272
|
+
if (jscpdClient.isAvailable()) tools.push("jscpd");
|
|
273
|
+
if (typeCoverageClient.isAvailable()) tools.push("type-coverage");
|
|
274
|
+
|
|
275
|
+
log(`Active tools: ${tools.join(", ")}`);
|
|
276
|
+
dbg(`session_start tools: ${tools.join(", ")}`);
|
|
277
|
+
|
|
278
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
279
|
+
dbg(`session_start cwd: ${cwd}`);
|
|
280
|
+
const parts: string[] = [];
|
|
281
|
+
|
|
282
|
+
// TODO/FIXME scan — fast, no deps
|
|
283
|
+
const todoResult = todoScanner.scanDirectory(cwd);
|
|
284
|
+
const todoReport = todoScanner.formatResult(todoResult);
|
|
285
|
+
dbg(`session_start TODO scan: ${todoResult.items.length} items`);
|
|
286
|
+
if (todoReport) parts.push(todoReport);
|
|
287
|
+
|
|
288
|
+
// Dead code scan — only if knip is available
|
|
289
|
+
if (knipClient.isAvailable()) {
|
|
290
|
+
const knipResult = knipClient.analyze(cwd);
|
|
291
|
+
const knipReport = knipClient.formatResult(knipResult);
|
|
292
|
+
dbg(`session_start Knip scan done`);
|
|
293
|
+
if (knipReport) parts.push(knipReport);
|
|
294
|
+
} else {
|
|
295
|
+
dbg(`session_start Knip: not available`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Duplicate code detection
|
|
299
|
+
if (jscpdClient.isAvailable()) {
|
|
300
|
+
const jscpdResult = jscpdClient.scan(cwd);
|
|
301
|
+
const jscpdReport = jscpdClient.formatResult(jscpdResult);
|
|
302
|
+
dbg(`session_start jscpd scan done`);
|
|
303
|
+
if (jscpdReport) parts.push(jscpdReport);
|
|
304
|
+
} else {
|
|
305
|
+
dbg(`session_start jscpd: not available`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// TypeScript type coverage
|
|
309
|
+
if (typeCoverageClient.isAvailable()) {
|
|
310
|
+
const tcResult = typeCoverageClient.scan(cwd);
|
|
311
|
+
const tcReport = typeCoverageClient.formatResult(tcResult);
|
|
312
|
+
dbg(`session_start type-coverage scan done`);
|
|
313
|
+
if (tcReport) parts.push(tcReport);
|
|
314
|
+
} else {
|
|
315
|
+
dbg(`session_start type-coverage: not available`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (parts.length > 0) {
|
|
319
|
+
sessionSummary = `[Session Start]\n${parts.join("\n\n")}`;
|
|
320
|
+
dbg(`session_start summary queued (${parts.length} parts)`);
|
|
321
|
+
} else {
|
|
322
|
+
dbg(`session_start: no parts, no summary`);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// --- Pre-write proactive hints ---
|
|
327
|
+
// Stored during tool_call, prepended to tool_result output so the agent sees them.
|
|
328
|
+
const preWriteHints = new Map<string, string>();
|
|
329
|
+
|
|
330
|
+
pi.on("tool_call", async (event, _ctx) => {
|
|
331
|
+
const filePath = isToolCallEventType("write", event)
|
|
332
|
+
? (event.input as { path: string }).path
|
|
333
|
+
: isToolCallEventType("edit", event)
|
|
334
|
+
? (event.input as { path: string }).path
|
|
335
|
+
: undefined;
|
|
336
|
+
|
|
337
|
+
if (!filePath) return;
|
|
338
|
+
|
|
339
|
+
const fs = require("node:fs") as typeof import("node:fs");
|
|
340
|
+
dbg(
|
|
341
|
+
`tool_call fired for: ${filePath} (exists: ${fs.existsSync(filePath)})`,
|
|
342
|
+
);
|
|
343
|
+
if (!fs.existsSync(filePath)) return;
|
|
344
|
+
|
|
345
|
+
const hints: string[] = [];
|
|
346
|
+
|
|
347
|
+
if (/\.(ts|tsx|js|jsx)$/.test(filePath) && !pi.getFlag("no-lsp")) {
|
|
348
|
+
tsClient.updateFile(filePath, fs.readFileSync(filePath, "utf-8"));
|
|
349
|
+
const diags = tsClient.getDiagnostics(filePath);
|
|
350
|
+
if (diags.length > 0) {
|
|
351
|
+
hints.push(
|
|
352
|
+
`⚠ Pre-write: file already has ${diags.length} TypeScript error(s) — fix before adding more`,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (
|
|
358
|
+
/\.(ts|tsx|js|jsx)$/.test(filePath) &&
|
|
359
|
+
!pi.getFlag("no-biome") &&
|
|
360
|
+
biomeClient.isAvailable()
|
|
361
|
+
) {
|
|
362
|
+
const diags = biomeClient.checkFile(filePath);
|
|
363
|
+
if (diags.length > 0) {
|
|
364
|
+
hints.push(
|
|
365
|
+
`⚠ Pre-write: file already has ${diags.length} Biome issue(s)`,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (!pi.getFlag("no-ast-grep") && astGrepClient.isAvailable()) {
|
|
371
|
+
const diags = astGrepClient.scanFile(filePath);
|
|
372
|
+
if (diags.length > 0) {
|
|
373
|
+
hints.push(
|
|
374
|
+
`⚠ Pre-write: file already has ${diags.length} structural violations`,
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
dbg(` pre-write hints: ${hints.length} — ${hints.join(" | ") || "none"}`);
|
|
380
|
+
if (hints.length > 0) {
|
|
381
|
+
preWriteHints.set(filePath, hints.join("\n"));
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// Real-time feedback on file writes/edits
|
|
386
|
+
pi.on("tool_result", async (event) => {
|
|
387
|
+
if (event.toolName !== "write" && event.toolName !== "edit") return;
|
|
388
|
+
|
|
389
|
+
const filePath = (event.input as { path?: string }).path;
|
|
390
|
+
if (!filePath) return;
|
|
391
|
+
|
|
392
|
+
dbg(`tool_result fired for: ${filePath}`);
|
|
393
|
+
dbg(` cwd: ${process.cwd()}`);
|
|
394
|
+
dbg(
|
|
395
|
+
` __dirname: ${typeof __dirname !== "undefined" ? __dirname : "undefined"}`,
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
// Deliver session-start summary (TODOs, dead code) once into the first tool_result
|
|
399
|
+
const sessionDump = sessionSummary;
|
|
400
|
+
sessionSummary = null;
|
|
401
|
+
|
|
402
|
+
// Prepend any pre-write hints collected during tool_call
|
|
403
|
+
const preHint = preWriteHints.get(filePath);
|
|
404
|
+
preWriteHints.delete(filePath);
|
|
405
|
+
|
|
406
|
+
let lspOutput = sessionDump ? `\n\n${sessionDump}` : "";
|
|
407
|
+
if (preHint) lspOutput += `\n\n${preHint}`;
|
|
408
|
+
|
|
409
|
+
// TypeScript LSP diagnostics
|
|
410
|
+
if (!pi.getFlag("no-lsp") && tsClient.isTypeScriptFile(filePath)) {
|
|
411
|
+
const fs = require("node:fs");
|
|
412
|
+
if (fs.existsSync(filePath)) {
|
|
413
|
+
tsClient.updateFile(filePath, fs.readFileSync(filePath, "utf-8"));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const diags = tsClient.getDiagnostics(filePath);
|
|
417
|
+
if (diags.length > 0) {
|
|
418
|
+
lspOutput += `\n\n[TypeScript] ${diags.length} issue(s):\n`;
|
|
419
|
+
for (const d of diags.slice(0, 10)) {
|
|
420
|
+
const label = d.severity === 2 ? "Warning" : "Error";
|
|
421
|
+
lspOutput += ` [${label}] L${d.range.start.line + 1}: ${d.message}\n`;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Python — Ruff linting + formatting
|
|
427
|
+
if (!pi.getFlag("no-ruff") && ruffClient.isPythonFile(filePath)) {
|
|
428
|
+
const diags = ruffClient.checkFile(filePath);
|
|
429
|
+
const fmtReport = ruffClient.checkFormatting(filePath);
|
|
430
|
+
const fixable = diags.filter((d) => d.fixable);
|
|
431
|
+
const hasFormatIssues = !!fmtReport;
|
|
432
|
+
|
|
433
|
+
if (pi.getFlag("autofix-ruff")) {
|
|
434
|
+
// Apply fixes then re-check to show what remains
|
|
435
|
+
let fixed = 0;
|
|
436
|
+
let formatted = false;
|
|
437
|
+
if (fixable.length > 0) {
|
|
438
|
+
const fixResult = ruffClient.fixFile(filePath);
|
|
439
|
+
if (fixResult.success && fixResult.changed)
|
|
440
|
+
fixed = fixResult.fixed ?? fixable.length;
|
|
441
|
+
}
|
|
442
|
+
const fmtResult = ruffClient.formatFile(filePath);
|
|
443
|
+
if (fmtResult.success && fmtResult.changed) formatted = true;
|
|
444
|
+
|
|
445
|
+
if (fixed > 0 || formatted) {
|
|
446
|
+
lspOutput += `\n\n[Ruff] Auto-fixed: ${fixed} lint issue(s)${formatted ? ", reformatted" : ""} — file updated on disk`;
|
|
447
|
+
// Re-check remaining issues
|
|
448
|
+
const remaining = ruffClient.checkFile(filePath);
|
|
449
|
+
const remainingFmt = ruffClient.checkFormatting(filePath);
|
|
450
|
+
if (remaining.length > 0 || remainingFmt) {
|
|
451
|
+
lspOutput += `\n\n${ruffClient.formatDiagnostics(remaining)}`;
|
|
452
|
+
if (remainingFmt) lspOutput += `\n\n${remainingFmt}`;
|
|
453
|
+
} else {
|
|
454
|
+
lspOutput += `\n\n[Ruff] ✓ All issues resolved`;
|
|
455
|
+
}
|
|
456
|
+
} else {
|
|
457
|
+
if (diags.length > 0)
|
|
458
|
+
lspOutput += `\n\n${ruffClient.formatDiagnostics(diags)}`;
|
|
459
|
+
if (fmtReport) lspOutput += `\n\n${fmtReport}`;
|
|
460
|
+
}
|
|
461
|
+
} else {
|
|
462
|
+
if (diags.length > 0)
|
|
463
|
+
lspOutput += `\n\n${ruffClient.formatDiagnostics(diags)}`;
|
|
464
|
+
if (fmtReport) lspOutput += `\n\n${fmtReport}`;
|
|
465
|
+
if (fixable.length > 0 || hasFormatIssues) {
|
|
466
|
+
lspOutput += `\n\n[Ruff] ${fixable.length} fixable — enable --autofix-ruff flag to auto-fix`;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ast-grep structural analysis
|
|
472
|
+
const astAvailable = astGrepClient.isAvailable();
|
|
473
|
+
dbg(
|
|
474
|
+
` ast-grep available: ${astAvailable}, no-ast-grep: ${pi.getFlag("no-ast-grep")}`,
|
|
475
|
+
);
|
|
476
|
+
if (!pi.getFlag("no-ast-grep") && astAvailable) {
|
|
477
|
+
const astDiags = astGrepClient.scanFile(filePath);
|
|
478
|
+
dbg(` ast-grep diags: ${astDiags.length}`);
|
|
479
|
+
if (astDiags.length > 0) {
|
|
480
|
+
lspOutput += `\n\n${astGrepClient.formatDiagnostics(astDiags)}`;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Biome: lint + format check
|
|
485
|
+
const biomeAvailable = biomeClient.isAvailable();
|
|
486
|
+
dbg(
|
|
487
|
+
` biome available: ${biomeAvailable}, supported: ${biomeClient.isSupportedFile(filePath)}, no-biome: ${pi.getFlag("no-biome")}`,
|
|
488
|
+
);
|
|
489
|
+
if (!pi.getFlag("no-biome") && biomeClient.isSupportedFile(filePath)) {
|
|
490
|
+
const biomeDiags = biomeClient.checkFile(filePath);
|
|
491
|
+
dbg(` biome diags: ${biomeDiags.length}`);
|
|
492
|
+
if (pi.getFlag("autofix-biome") && biomeDiags.length > 0) {
|
|
493
|
+
// Always attempt fix — let Biome decide what it can do
|
|
494
|
+
const fixResult = biomeClient.fixFile(filePath);
|
|
495
|
+
if (fixResult.success && fixResult.changed) {
|
|
496
|
+
lspOutput += `\n\n[Biome] Auto-fixed ${fixResult.fixed} issue(s) — file updated on disk`;
|
|
497
|
+
const remaining = biomeClient.checkFile(filePath);
|
|
498
|
+
if (remaining.length > 0) {
|
|
499
|
+
lspOutput += `\n\n${biomeClient.formatDiagnostics(remaining, filePath)}`;
|
|
500
|
+
} else {
|
|
501
|
+
lspOutput += `\n\n[Biome] ✓ All issues resolved`;
|
|
502
|
+
}
|
|
503
|
+
} else {
|
|
504
|
+
// Nothing fixable — show diagnostics as-is
|
|
505
|
+
lspOutput += `\n\n${biomeClient.formatDiagnostics(biomeDiags, filePath)}`;
|
|
506
|
+
}
|
|
507
|
+
} else if (biomeDiags.length > 0) {
|
|
508
|
+
const fixable = biomeDiags.filter((d) => d.fixable);
|
|
509
|
+
lspOutput += `\n\n${biomeClient.formatDiagnostics(biomeDiags, filePath)}`;
|
|
510
|
+
if (fixable.length > 0) {
|
|
511
|
+
lspOutput += `\n\n[Biome] ${fixable.length} fixable — enable --autofix-biome flag or run /format`;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Circular dependency check (cached, only when imports change)
|
|
517
|
+
if (
|
|
518
|
+
!pi.getFlag("no-madge") &&
|
|
519
|
+
depChecker.isAvailable() &&
|
|
520
|
+
/\.(ts|tsx|js|jsx)$/.test(filePath)
|
|
521
|
+
) {
|
|
522
|
+
const depResult = depChecker.checkFile(filePath);
|
|
523
|
+
if (depResult.hasCircular && depResult.circular.length > 0) {
|
|
524
|
+
const circularDeps = depResult.circular
|
|
525
|
+
.flatMap((d) => d.path)
|
|
526
|
+
.filter(
|
|
527
|
+
(p: string) => !filePath.endsWith(require("node:path").basename(p)),
|
|
528
|
+
);
|
|
529
|
+
const uniqueDeps = [...new Set(circularDeps)];
|
|
530
|
+
if (uniqueDeps.length > 0) {
|
|
531
|
+
lspOutput += `\n\n${depChecker.formatWarning(filePath, uniqueDeps)}`;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (!lspOutput) return;
|
|
537
|
+
|
|
538
|
+
return {
|
|
539
|
+
content: [...event.content, { type: "text" as const, text: lspOutput }],
|
|
540
|
+
};
|
|
541
|
+
});
|
|
477
542
|
}
|
package/package.json
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
2
|
+
"name": "pi-lens",
|
|
3
|
+
"version": "1.1.2",
|
|
4
|
+
"description": "Real-time code feedback for pi — TypeScript LSP, Biome, ast-grep, Ruff, TODO scanner, dead code, duplicate detection, type coverage",
|
|
5
|
+
"main": "index.ts",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsc",
|
|
8
|
+
"watch": "tsc --watch"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"pi",
|
|
12
|
+
"pi-extension",
|
|
13
|
+
"pi-package",
|
|
14
|
+
"linter",
|
|
15
|
+
"biome",
|
|
16
|
+
"ast-grep",
|
|
17
|
+
"ruff",
|
|
18
|
+
"typescript",
|
|
19
|
+
"code-quality",
|
|
20
|
+
"feedback",
|
|
21
|
+
"type-coverage",
|
|
22
|
+
"jscpd",
|
|
23
|
+
"knip"
|
|
24
|
+
],
|
|
25
|
+
"author": "R3LiC",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"files": [
|
|
28
|
+
"index.ts",
|
|
29
|
+
"clients/",
|
|
30
|
+
"rules/",
|
|
31
|
+
"tsconfig.json",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@mariozechner/pi-coding-agent": "*"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"vscode-languageserver-protocol": "^3.17.5",
|
|
39
|
+
"vscode-languageserver-types": "^3.17.5"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^22.10.5",
|
|
43
|
+
"typescript": "^5.0.0"
|
|
44
|
+
},
|
|
45
|
+
"pi": {
|
|
46
|
+
"extensions": [
|
|
47
|
+
"./index.ts"
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
50
|
}
|