pi-lens 3.8.3 → 3.8.5
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/index.ts +106 -89
- package/package.json +4 -1
- package/scripts/check-extensions.mjs +87 -0
package/index.ts
CHANGED
|
@@ -1013,106 +1013,123 @@ export default function (pi: ExtensionAPI) {
|
|
|
1013
1013
|
dbg("session_start: no project rules found");
|
|
1014
1014
|
}
|
|
1015
1015
|
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1016
|
+
// Fire-and-forget background cache warming — none of these add to `parts`
|
|
1017
|
+
Promise.resolve()
|
|
1018
|
+
.then(async () => {
|
|
1019
|
+
// TODO baseline
|
|
1020
|
+
const todoResult = todoScanner.scanDirectory(cwd);
|
|
1021
|
+
dbg(
|
|
1022
|
+
`session_start TODO scan: ${todoResult.items.length} items (baseline stored)`,
|
|
1023
|
+
);
|
|
1024
|
+
cacheManager.writeCache(
|
|
1025
|
+
"todo-baseline",
|
|
1026
|
+
{ items: todoResult.items },
|
|
1027
|
+
cwd,
|
|
1028
|
+
);
|
|
1025
1029
|
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
dbg(
|
|
1029
|
-
`session_start: skipping heavy scans (${startupScan.reason ?? "unknown"})`,
|
|
1030
|
-
);
|
|
1031
|
-
} else {
|
|
1032
|
-
// Dead code scan — use cache if fresh (cached for on-demand reporting)
|
|
1033
|
-
if (await knipClient.ensureAvailable()) {
|
|
1034
|
-
const cached = cacheManager.readCache<
|
|
1035
|
-
ReturnType<KnipClient["analyze"]>
|
|
1036
|
-
>("knip", cwd);
|
|
1037
|
-
if (cached) {
|
|
1030
|
+
// Heavy startup scans — gated on project root + file count safety check
|
|
1031
|
+
if (!startupScan.canWarmCaches) {
|
|
1038
1032
|
dbg(
|
|
1039
|
-
`session_start
|
|
1033
|
+
`session_start: skipping heavy scans (${startupScan.reason ?? "unknown"})`,
|
|
1040
1034
|
);
|
|
1041
|
-
|
|
1042
|
-
const startMs = Date.now();
|
|
1043
|
-
const knipResult = knipClient.analyze(cwd);
|
|
1044
|
-
cacheManager.writeCache("knip", knipResult, cwd, {
|
|
1045
|
-
scanDurationMs: Date.now() - startMs,
|
|
1046
|
-
});
|
|
1047
|
-
dbg(`session_start Knip scan done`);
|
|
1035
|
+
return;
|
|
1048
1036
|
}
|
|
1049
|
-
} else {
|
|
1050
|
-
dbg(`session_start Knip: not available`);
|
|
1051
|
-
}
|
|
1052
1037
|
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1038
|
+
// Dead code scan — use cache if fresh (cached for on-demand reporting)
|
|
1039
|
+
if (await knipClient.ensureAvailable()) {
|
|
1040
|
+
const cached = cacheManager.readCache<
|
|
1041
|
+
ReturnType<KnipClient["analyze"]>
|
|
1042
|
+
>("knip", cwd);
|
|
1043
|
+
if (cached) {
|
|
1044
|
+
dbg(
|
|
1045
|
+
`session_start Knip: cache hit (${Math.round((Date.now() - new Date(cached.meta.timestamp).getTime()) / 1000)}s ago)`,
|
|
1046
|
+
);
|
|
1047
|
+
} else {
|
|
1048
|
+
const startMs = Date.now();
|
|
1049
|
+
const knipResult = knipClient.analyze(cwd);
|
|
1050
|
+
cacheManager.writeCache("knip", knipResult, cwd, {
|
|
1051
|
+
scanDurationMs: Date.now() - startMs,
|
|
1052
|
+
});
|
|
1053
|
+
dbg(`session_start Knip scan done`);
|
|
1054
|
+
}
|
|
1060
1055
|
} else {
|
|
1061
|
-
|
|
1062
|
-
const jscpdResult = jscpdClient.scan(cwd);
|
|
1063
|
-
cacheManager.writeCache("jscpd", jscpdResult, cwd, {
|
|
1064
|
-
scanDurationMs: Date.now() - startMs,
|
|
1065
|
-
});
|
|
1066
|
-
dbg(`session_start jscpd scan done`);
|
|
1056
|
+
dbg(`session_start Knip: not available`);
|
|
1067
1057
|
}
|
|
1068
|
-
} else {
|
|
1069
|
-
dbg(`session_start jscpd: not available`);
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
// Type-coverage runs on-demand via /lens-booboo only (not at session_start)
|
|
1073
|
-
|
|
1074
|
-
// Scan for exported functions (cached for duplicate detection on write)
|
|
1075
|
-
if (await astGrepClient.ensureAvailable()) {
|
|
1076
|
-
const exports = await astGrepClient.scanExports(cwd, "typescript");
|
|
1077
|
-
dbg(`session_start exports scan: ${exports.size} functions found`);
|
|
1078
|
-
for (const [name, file] of exports) {
|
|
1079
|
-
cachedExports.set(name, file);
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
1058
|
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
) {
|
|
1091
|
-
cachedProjectIndex = existing;
|
|
1092
|
-
dbg(
|
|
1093
|
-
`session_start: loaded fresh project index (${existing.entries.size} entries)`,
|
|
1094
|
-
);
|
|
1095
|
-
} else {
|
|
1096
|
-
const sourceFiles = getSourceFiles(scanRoot, true);
|
|
1097
|
-
const tsFiles = sourceFiles.filter(
|
|
1098
|
-
(f) => f.endsWith(".ts") || f.endsWith(".tsx"),
|
|
1099
|
-
);
|
|
1100
|
-
if (tsFiles.length > 0 && tsFiles.length <= 500) {
|
|
1101
|
-
cachedProjectIndex = await buildProjectIndex(scanRoot, tsFiles);
|
|
1102
|
-
await saveIndex(cachedProjectIndex, scanRoot);
|
|
1103
|
-
dbg(
|
|
1104
|
-
`session_start: built project index (${cachedProjectIndex.entries.size} entries from ${tsFiles.length} files)`,
|
|
1105
|
-
);
|
|
1059
|
+
// Duplicate code detection — use cache if fresh, auto-install if needed (cached for on-demand reporting)
|
|
1060
|
+
if (await jscpdClient.ensureAvailable()) {
|
|
1061
|
+
const cached = cacheManager.readCache<
|
|
1062
|
+
ReturnType<JscpdClient["scan"]>
|
|
1063
|
+
>("jscpd", cwd);
|
|
1064
|
+
if (cached) {
|
|
1065
|
+
dbg(`session_start jscpd: cache hit`);
|
|
1106
1066
|
} else {
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1067
|
+
const startMs = Date.now();
|
|
1068
|
+
const jscpdResult = jscpdClient.scan(cwd);
|
|
1069
|
+
cacheManager.writeCache("jscpd", jscpdResult, cwd, {
|
|
1070
|
+
scanDurationMs: Date.now() - startMs,
|
|
1071
|
+
});
|
|
1072
|
+
dbg(`session_start jscpd scan done`);
|
|
1110
1073
|
}
|
|
1074
|
+
} else {
|
|
1075
|
+
dbg(`session_start jscpd: not available`);
|
|
1111
1076
|
}
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1077
|
+
|
|
1078
|
+
// Type-coverage runs on-demand via /lens-booboo only (not at session_start)
|
|
1079
|
+
|
|
1080
|
+
// Parallelize: exports scan + project index load (independent operations)
|
|
1081
|
+
const [exportsResult, indexResult] = await Promise.allSettled([
|
|
1082
|
+
// Scan for exported functions (cached for duplicate detection on write)
|
|
1083
|
+
astGrepClient.ensureAvailable().then(async (available) => {
|
|
1084
|
+
if (!available) {
|
|
1085
|
+
dbg(`session_start ast-grep: not available`);
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
const exports = await astGrepClient.scanExports(cwd, "typescript");
|
|
1089
|
+
dbg(`session_start exports scan: ${exports.size} functions found`);
|
|
1090
|
+
for (const [name, file] of exports) {
|
|
1091
|
+
cachedExports.set(name, file);
|
|
1092
|
+
}
|
|
1093
|
+
}),
|
|
1094
|
+
// Build similarity index for pre-write structural duplicate detection
|
|
1095
|
+
(async () => {
|
|
1096
|
+
const existing = await loadIndex(scanRoot);
|
|
1097
|
+
if (
|
|
1098
|
+
existing &&
|
|
1099
|
+
existing.entries.size > 0 &&
|
|
1100
|
+
(await isIndexFresh(scanRoot))
|
|
1101
|
+
) {
|
|
1102
|
+
cachedProjectIndex = existing;
|
|
1103
|
+
dbg(
|
|
1104
|
+
`session_start: loaded fresh project index (${existing.entries.size} entries)`,
|
|
1105
|
+
);
|
|
1106
|
+
} else {
|
|
1107
|
+
const sourceFiles = getSourceFiles(scanRoot, true);
|
|
1108
|
+
const tsFiles = sourceFiles.filter(
|
|
1109
|
+
(f) => f.endsWith(".ts") || f.endsWith(".tsx"),
|
|
1110
|
+
);
|
|
1111
|
+
if (tsFiles.length > 0 && tsFiles.length <= 500) {
|
|
1112
|
+
cachedProjectIndex = await buildProjectIndex(scanRoot, tsFiles);
|
|
1113
|
+
await saveIndex(cachedProjectIndex, scanRoot);
|
|
1114
|
+
dbg(
|
|
1115
|
+
`session_start: built project index (${cachedProjectIndex.entries.size} entries from ${tsFiles.length} files)`,
|
|
1116
|
+
);
|
|
1117
|
+
} else {
|
|
1118
|
+
dbg(
|
|
1119
|
+
`session_start: skipped project index (${tsFiles.length} files)`,
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
})(),
|
|
1124
|
+
]);
|
|
1125
|
+
if (exportsResult.status === "rejected")
|
|
1126
|
+
dbg(`session_start: exports scan failed: ${exportsResult.reason}`);
|
|
1127
|
+
if (indexResult.status === "rejected")
|
|
1128
|
+
dbg(`session_start: project index build failed: ${indexResult.reason}`);
|
|
1129
|
+
|
|
1130
|
+
dbg(`session_start: background cache warming complete`);
|
|
1131
|
+
})
|
|
1132
|
+
.catch((err) => dbg(`session_start: background warm error: ${err}`));
|
|
1116
1133
|
|
|
1117
1134
|
dbg(
|
|
1118
1135
|
`session_start: scans complete (${parts.length} part(s)), cached for commands`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-lens",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo",
|
|
6
6
|
"repository": {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"rust:test": "cargo test --manifest-path rust/Cargo.toml --all-targets --all-features",
|
|
21
21
|
"rust:build": "cargo build --manifest-path rust/Cargo.toml --release",
|
|
22
22
|
"rust:build:debug": "cargo build --manifest-path rust/Cargo.toml",
|
|
23
|
+
"check": "node scripts/check-extensions.mjs",
|
|
23
24
|
"download-grammars": "node scripts/download-grammars.js",
|
|
24
25
|
"postinstall": "node scripts/download-grammars.js"
|
|
25
26
|
},
|
|
@@ -47,6 +48,7 @@
|
|
|
47
48
|
"rules/",
|
|
48
49
|
"skills/",
|
|
49
50
|
"scripts/download-grammars.js",
|
|
51
|
+
"scripts/check-extensions.mjs",
|
|
50
52
|
"rust/src/",
|
|
51
53
|
"rust/Cargo.toml",
|
|
52
54
|
"default-architect.yaml",
|
|
@@ -67,6 +69,7 @@
|
|
|
67
69
|
"dependencies": {
|
|
68
70
|
"@sinclair/typebox": "^0.34.0",
|
|
69
71
|
"cross-spawn": "^7.0.6",
|
|
72
|
+
"typescript": "^5.0.0",
|
|
70
73
|
"vscode-jsonrpc": "^8.2.1"
|
|
71
74
|
},
|
|
72
75
|
"optionalDependencies": {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verifies all relative imports in the published package resolve to real files.
|
|
3
|
+
* Catches "Cannot find module" errors from missing files in the npm tarball.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node scripts/check-extensions.mjs # from source (uses npm pack --dry-run)
|
|
7
|
+
* node scripts/check-extensions.mjs <dir> # from installed location
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
11
|
+
import { join, resolve, dirname } from "node:path";
|
|
12
|
+
import { execSync } from "node:child_process";
|
|
13
|
+
|
|
14
|
+
const installDir = resolve(process.argv[2] ?? ".");
|
|
15
|
+
const fromSource = process.argv[2] == null;
|
|
16
|
+
|
|
17
|
+
function getFiles() {
|
|
18
|
+
if (fromSource) {
|
|
19
|
+
// Use npm pack --dry-run to get exactly the files that would be published
|
|
20
|
+
const out = execSync("npm pack --dry-run 2>&1", { encoding: "utf8" });
|
|
21
|
+
return out
|
|
22
|
+
.split("\n")
|
|
23
|
+
.map(l => l.match(/npm notice [\d.]+\w+\s+(.+)/)?.[1]?.trim())
|
|
24
|
+
.filter(f => f && (f.endsWith(".ts") || f.endsWith(".mjs")))
|
|
25
|
+
.map(f => join(installDir, f));
|
|
26
|
+
}
|
|
27
|
+
// Installed location: walk all .ts/.mjs files
|
|
28
|
+
const files = [];
|
|
29
|
+
function walk(dir) {
|
|
30
|
+
for (const entry of readdirSync(dir)) {
|
|
31
|
+
const full = join(dir, entry);
|
|
32
|
+
if (entry === "node_modules") continue;
|
|
33
|
+
if (statSync(full).isDirectory()) walk(full);
|
|
34
|
+
else if (entry.endsWith(".ts") || entry.endsWith(".mjs")) files.push(full);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
walk(installDir);
|
|
38
|
+
return files;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveImport(fromFile, importPath) {
|
|
42
|
+
const base = join(dirname(fromFile), importPath);
|
|
43
|
+
for (const candidate of [
|
|
44
|
+
base,
|
|
45
|
+
base.replace(/\.js$/, ".ts"), // .js → .ts (TypeScript ESM convention)
|
|
46
|
+
base + ".ts",
|
|
47
|
+
join(base, "index.ts"),
|
|
48
|
+
]) {
|
|
49
|
+
try { if (statSync(candidate).isFile()) return candidate; } catch {}
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const files = getFiles();
|
|
55
|
+
console.log(`Checking ${files.length} file(s) in: ${installDir}\n`);
|
|
56
|
+
|
|
57
|
+
let totalImports = 0;
|
|
58
|
+
let failed = 0;
|
|
59
|
+
const seen = new Set();
|
|
60
|
+
|
|
61
|
+
for (const file of files) {
|
|
62
|
+
const src = readFileSync(file, "utf8");
|
|
63
|
+
const relFile = file.slice(installDir.length + 1).replace(/\\/g, "/");
|
|
64
|
+
// Strip comments before matching imports
|
|
65
|
+
const stripped = src.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
66
|
+
const importRe = /from\s+['"](\.[^'"]+)['"]/g;
|
|
67
|
+
let match;
|
|
68
|
+
while ((match = importRe.exec(stripped)) !== null) {
|
|
69
|
+
const importPath = match[1];
|
|
70
|
+
const key = `${relFile}:${importPath}`;
|
|
71
|
+
if (seen.has(key)) continue;
|
|
72
|
+
seen.add(key);
|
|
73
|
+
totalImports++;
|
|
74
|
+
if (!resolveImport(file, importPath)) {
|
|
75
|
+
console.error(` ✗ ${relFile}`);
|
|
76
|
+
console.error(` imports '${importPath}' → NOT FOUND`);
|
|
77
|
+
failed++;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log(`Checked ${totalImports} relative import(s) across ${files.length} file(s).`);
|
|
83
|
+
console.log(failed === 0
|
|
84
|
+
? `\nAll imports resolve OK ✓`
|
|
85
|
+
: `\n${failed} import(s) could not be resolved ✗`);
|
|
86
|
+
|
|
87
|
+
process.exit(failed > 0 ? 1 : 0);
|