pi-lens 3.8.71 → 3.8.73
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/CHANGELOG.md +152 -1
- package/README.md +31 -22
- package/config/markdownlint/core.json +6 -0
- package/dist/clients/atomic-write.js +83 -0
- package/dist/clients/config-enabled-shape.js +39 -0
- package/dist/clients/deps/js-yaml.js +1 -1
- package/dist/clients/diagnostic-dispositions.js +64 -2
- package/dist/clients/dispatch/integration.js +2 -1
- package/dist/clients/dispatch/runners/ast-grep-napi.js +60 -7
- package/dist/clients/dispatch/runners/lsp.js +54 -25
- package/dist/clients/dispatch/runners/markdownlint.js +7 -4
- package/dist/clients/dispatch/runners/pyright.js +1 -0
- package/dist/clients/dispatch/runners/tree-sitter.js +1 -1
- package/dist/clients/env-utils.js +38 -0
- package/dist/clients/file-utils.js +21 -3
- package/dist/clients/instance-registry.js +32 -21
- package/dist/clients/jscpd-client.js +52 -39
- package/dist/clients/language-profile.js +12 -19
- package/dist/clients/lens-config.js +106 -27
- package/dist/clients/lens-engine.js +129 -3
- package/dist/clients/lsp/cascade-tier.js +1 -38
- package/dist/clients/lsp/client.js +3 -1
- package/dist/clients/lsp/index.js +707 -109
- package/dist/clients/lsp/server-strategies.js +3 -224
- package/dist/clients/lsp/server.js +1 -1
- package/dist/clients/lsp/wait-policy/capability-snapshot.js +1 -0
- package/dist/clients/lsp/wait-policy/classification.js +63 -0
- package/dist/clients/lsp/wait-policy/index.js +10 -0
- package/dist/clients/lsp/wait-policy/strategies.js +244 -0
- package/dist/clients/lsp-budget.js +54 -16
- package/dist/clients/mcp/analyze.js +1 -1
- package/dist/clients/mcp/host-shim.js +12 -8
- package/dist/clients/mcp/ipc.js +118 -0
- package/dist/clients/mcp/session.js +2 -2
- package/dist/clients/pipeline.js +11 -6
- package/dist/clients/project-diagnostics/fresh-fetch.js +20 -1
- package/dist/clients/project-diagnostics/scanner.js +37 -6
- package/dist/clients/project-lens-config.js +167 -3
- package/dist/clients/project-report.js +627 -0
- package/dist/clients/project-scale.js +239 -0
- package/dist/clients/project-scan-policy.js +11 -1
- package/dist/clients/recent-touches.js +6 -11
- package/dist/clients/review-graph/builder.js +170 -13
- package/dist/clients/review-graph/import-resolvers.js +149 -13
- package/dist/clients/review-graph/tsconfig-paths.js +242 -0
- package/dist/clients/review-graph/workspace-modules.js +21 -9
- package/dist/clients/rules-scanner.js +15 -2
- package/dist/clients/runtime-agent-end.js +104 -17
- package/dist/clients/runtime-config.js +4 -0
- package/dist/clients/runtime-coordinator.js +50 -1
- package/dist/clients/runtime-session.js +41 -3
- package/dist/clients/runtime-tool-result.js +4 -3
- package/dist/clients/runtime-turn.js +4 -1
- package/dist/clients/safe-spawn.js +264 -40
- package/dist/clients/session-lifecycle.js +36 -0
- package/dist/clients/session-state-store.js +8 -3
- package/dist/clients/source-filter.js +136 -61
- package/dist/clients/source-walker.js +89 -0
- package/dist/clients/startup-scan.js +189 -111
- package/dist/clients/subagent-mode.js +14 -3
- package/dist/clients/todo-scanner.js +5 -1
- package/dist/clients/tool-policy.js +26 -4
- package/dist/clients/warm-attach.js +204 -0
- package/dist/clients/word-index.js +36 -5
- package/dist/clients/workspace-topology.js +266 -0
- package/dist/index.js +15205 -11330
- package/dist/mcp/server.js +125 -10
- package/dist/tools/ast-grep-search.js +21 -0
- package/dist/tools/lens-diagnostic-mark.js +198 -7
- package/dist/tools/lens-diagnostics.js +119 -21
- package/dist/tools/lsp-diagnostics.js +143 -10
- package/dist/tools/lsp-navigation.js +11 -2
- package/dist/tools/project-report.js +94 -0
- package/dist/tools/symbol-search.js +32 -8
- package/docs/environment-variables.md +10 -0
- package/docs/features.md +7 -1
- package/docs/globalconfig.md +70 -0
- package/docs/servercapabilities.md +1 -1
- package/docs/usage.md +13 -2
- package/package.json +8 -9
- package/rules/tree-sitter-queries/typescript/no-equality-in-for-condition.yml +1 -0
- package/rules/tree-sitter-queries/typescript/no-jump-in-finally.yml +1 -0
- package/scripts/download-grammars.js +1 -0
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import * as fs from "node:fs";
|
|
24
24
|
import * as path from "node:path";
|
|
25
|
+
import { writeFileAtomic, writeFileAtomicAsync } from "./atomic-write.js";
|
|
25
26
|
import { getGlobalPiLensDir } from "./file-utils.js";
|
|
26
27
|
// #735: reuse the #449/#525 reaper's exact conservative liveness check
|
|
27
28
|
// (`process.kill(pid, 0)`, ESRCH-only-means-dead) rather than inventing a
|
|
@@ -34,6 +35,7 @@ import { getGlobalPiLensDir } from "./file-utils.js";
|
|
|
34
35
|
// import is actually invoked.
|
|
35
36
|
import { realIsPidAlive } from "./instance-reaper.js";
|
|
36
37
|
import { normalizeFilePath } from "./path-utils.js";
|
|
38
|
+
import { getSubagentIdentity, isSubagentSession, } from "./subagent-mode.js";
|
|
37
39
|
function registryPath() {
|
|
38
40
|
return path.join(getGlobalPiLensDir(), "instances.json");
|
|
39
41
|
}
|
|
@@ -87,44 +89,32 @@ export async function readInstanceRegistry() {
|
|
|
87
89
|
const file = await readRegistryAsync();
|
|
88
90
|
return file.instances;
|
|
89
91
|
}
|
|
90
|
-
// --- Write (atomic tmp + rename
|
|
92
|
+
// --- Write (atomic tmp + rename via clients/atomic-write.ts, #762) ---
|
|
91
93
|
async function writeRegistryAsync(file) {
|
|
92
94
|
const dir = getGlobalPiLensDir();
|
|
93
95
|
const target = registryPath();
|
|
94
|
-
const tmpPath = `${target}.tmp-${process.pid}`;
|
|
95
96
|
try {
|
|
96
97
|
await fs.promises.mkdir(dir, { recursive: true });
|
|
97
|
-
await fs.promises.writeFile(tmpPath, JSON.stringify(file), "utf-8");
|
|
98
|
-
await fs.promises.rename(tmpPath, target);
|
|
99
98
|
}
|
|
100
99
|
catch {
|
|
101
|
-
// Best-effort observability substrate — a failed
|
|
100
|
+
// Best-effort observability substrate — a failed mkdir just means this
|
|
102
101
|
// update is lost, never a thrown error for the caller.
|
|
103
|
-
|
|
104
|
-
await fs.promises.rm(tmpPath, { force: true });
|
|
105
|
-
}
|
|
106
|
-
catch {
|
|
107
|
-
// ignore
|
|
108
|
-
}
|
|
102
|
+
return;
|
|
109
103
|
}
|
|
104
|
+
// bestEffort (default): a failed write just means this update is lost,
|
|
105
|
+
// never a thrown error for the caller.
|
|
106
|
+
await writeFileAtomicAsync(target, JSON.stringify(file));
|
|
110
107
|
}
|
|
111
108
|
function writeRegistrySync(file) {
|
|
112
109
|
const dir = getGlobalPiLensDir();
|
|
113
110
|
const target = registryPath();
|
|
114
|
-
const tmpPath = `${target}.tmp-${process.pid}`;
|
|
115
111
|
try {
|
|
116
112
|
fs.mkdirSync(dir, { recursive: true });
|
|
117
|
-
fs.writeFileSync(tmpPath, JSON.stringify(file), "utf-8");
|
|
118
|
-
fs.renameSync(tmpPath, target);
|
|
119
113
|
}
|
|
120
114
|
catch {
|
|
121
|
-
|
|
122
|
-
fs.rmSync(tmpPath, { force: true });
|
|
123
|
-
}
|
|
124
|
-
catch {
|
|
125
|
-
// ignore
|
|
126
|
-
}
|
|
115
|
+
return;
|
|
127
116
|
}
|
|
117
|
+
writeFileAtomic(target, JSON.stringify(file));
|
|
128
118
|
}
|
|
129
119
|
// --- Mutations (all read-modify-write whole file) ---
|
|
130
120
|
/** Create/overwrite this process's entry. */
|
|
@@ -137,6 +127,15 @@ export async function registerInstance(projectRoot) {
|
|
|
137
127
|
const now = new Date().toISOString();
|
|
138
128
|
const others = file.instances.filter((entry) => entry.pid !== pid);
|
|
139
129
|
const existing = file.instances.find((entry) => entry.pid === pid);
|
|
130
|
+
const identity = isSubagentSession() ? getSubagentIdentity() : undefined;
|
|
131
|
+
const subagent = identity
|
|
132
|
+
? {
|
|
133
|
+
marker: identity.marker,
|
|
134
|
+
agentType: identity.agentName,
|
|
135
|
+
parentPid: identity.parentPid,
|
|
136
|
+
runId: identity.runId,
|
|
137
|
+
}
|
|
138
|
+
: undefined;
|
|
140
139
|
others.push({
|
|
141
140
|
pid,
|
|
142
141
|
startedAt: existing?.startedAt ?? now,
|
|
@@ -145,6 +144,7 @@ export async function registerInstance(projectRoot) {
|
|
|
145
144
|
lspChildCount: existing?.lspChildren?.length ?? 0,
|
|
146
145
|
rssBytes: process.memoryUsage().rss,
|
|
147
146
|
heartbeatAt: now,
|
|
147
|
+
...(subagent ? { subagent } : {}),
|
|
148
148
|
});
|
|
149
149
|
await writeRegistryAsync({ instances: others });
|
|
150
150
|
}
|
|
@@ -226,8 +226,19 @@ export async function recordLspChild(entry) {
|
|
|
226
226
|
}
|
|
227
227
|
await writeRegistryAsync(file);
|
|
228
228
|
}
|
|
229
|
+
// LSP client shutdown intentionally does not await registry removal (the
|
|
230
|
+
// process-exiting path must stay non-blocking), but concurrent removals still
|
|
231
|
+
// need to serialize their read-modify-write sequence or siblings can be lost
|
|
232
|
+
// to last-writer-wins. Process kills remain fully concurrent; this queue only
|
|
233
|
+
// covers the small best-effort registry mutation.
|
|
234
|
+
let lspChildRemovalTail = Promise.resolve();
|
|
229
235
|
/** Remove an LSP child (by pid) from this process's entry. */
|
|
230
|
-
export
|
|
236
|
+
export function removeLspChild(pid) {
|
|
237
|
+
const removal = lspChildRemovalTail.then(() => removeLspChildNow(pid));
|
|
238
|
+
lspChildRemovalTail = removal.catch(() => { });
|
|
239
|
+
return removal;
|
|
240
|
+
}
|
|
241
|
+
async function removeLspChildNow(pid) {
|
|
231
242
|
if (!isInstanceRegistryEnabled())
|
|
232
243
|
return;
|
|
233
244
|
const selfPid = process.pid;
|
|
@@ -11,9 +11,12 @@ import * as fs from "node:fs";
|
|
|
11
11
|
import { mkdtempSync } from "node:fs";
|
|
12
12
|
import * as os from "node:os";
|
|
13
13
|
import * as path from "node:path";
|
|
14
|
-
import { getExcludedDirGlobs, getGlobalPiLensDir, getProjectIgnoreGlobs, getProjectIgnoreMatcher,
|
|
14
|
+
import { getExcludedDirGlobs, getGlobalPiLensDir, getProjectIgnoreGlobs, getProjectIgnoreMatcher, } from "./file-utils.js";
|
|
15
15
|
import { findNodeToolBinary } from "./package-manager.js";
|
|
16
|
+
import { isAtOrAboveHomeDir } from "./path-utils.js";
|
|
17
|
+
import { getJscpdMaxEntriesDerived } from "./project-scale.js";
|
|
16
18
|
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
19
|
+
import { shouldRecurseIntoDir, walkTreeStackSync } from "./source-walker.js";
|
|
17
20
|
const EMPTY_RESULT = {
|
|
18
21
|
success: false,
|
|
19
22
|
clones: [],
|
|
@@ -45,46 +48,42 @@ export class JscpdClient {
|
|
|
45
48
|
* them.
|
|
46
49
|
*/
|
|
47
50
|
hasSourceFilesRecursive(rootDir) {
|
|
48
|
-
const stack = [rootDir];
|
|
49
51
|
const ignoreMatcher = getProjectIgnoreMatcher(rootDir);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
52
|
+
const state = { visited: 0 };
|
|
53
|
+
// #776: derived from the `maxProjectFiles` scale knob (project-scale.ts),
|
|
54
|
+
// reproducing this same 6,000 default at the default base.
|
|
55
|
+
const MAX_ENTRIES = getJscpdMaxEntriesDerived(rootDir);
|
|
56
|
+
// #761: the traversal loop, readdir-safety, and directory-recursion
|
|
57
|
+
// decision are the shared `walkTreeStackSync` engine; this client keeps
|
|
58
|
+
// its own per-entry policy (skip ALL symlinks — files too — before the
|
|
59
|
+
// dir/file branch, its own multi-language source regex, and a
|
|
60
|
+
// per-directory entry budget via `shouldStop`, matching the pre-#761
|
|
61
|
+
// `while (stack.length > 0 && visited < MAX_ENTRIES)` condition: the
|
|
62
|
+
// current directory's entry loop always finishes, but no further
|
|
63
|
+
// directory is popped once the budget is spent).
|
|
64
|
+
return walkTreeStackSync(rootDir, (entry, fullPath) => {
|
|
65
|
+
state.visited += 1;
|
|
66
|
+
if (entry.isSymbolicLink())
|
|
67
|
+
return "skip";
|
|
68
|
+
if (entry.isDirectory()) {
|
|
69
|
+
return shouldRecurseIntoDir(entry, fullPath, {
|
|
70
|
+
ignoreMatcher,
|
|
71
|
+
followSymlinks: true,
|
|
72
|
+
})
|
|
73
|
+
? "recurse"
|
|
74
|
+
: "skip";
|
|
62
75
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (ignoreMatcher.isIgnored(fullPath, true))
|
|
72
|
-
continue;
|
|
73
|
-
stack.push(fullPath);
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
if (!entry.isFile())
|
|
77
|
-
continue;
|
|
78
|
-
if (ignoreMatcher.isIgnored(path.join(dir, entry.name), false))
|
|
79
|
-
continue;
|
|
80
|
-
if (/\.(ts|tsx|js|jsx|mjs|cjs|py|pyi|java|go|rs|rb|php|swift|kt|kts|dart|lua|scala|c|h|cpp|cc|cxx|hpp|hxx|cs|m|mm)$/.test(entry.name)) {
|
|
81
|
-
if (entry.name.endsWith(".d.ts"))
|
|
82
|
-
continue;
|
|
83
|
-
return true;
|
|
84
|
-
}
|
|
76
|
+
if (!entry.isFile())
|
|
77
|
+
return "skip";
|
|
78
|
+
if (ignoreMatcher.isIgnored(fullPath, false))
|
|
79
|
+
return "skip";
|
|
80
|
+
if (/\.(ts|tsx|js|jsx|mjs|cjs|py|pyi|java|go|rs|rb|php|swift|kt|kts|dart|lua|scala|c|h|cpp|cc|cxx|hpp|hxx|cs|m|mm)$/.test(entry.name)) {
|
|
81
|
+
if (entry.name.endsWith(".d.ts"))
|
|
82
|
+
return "skip";
|
|
83
|
+
return "stop";
|
|
85
84
|
}
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
return "skip";
|
|
86
|
+
}, { shouldStop: () => state.visited >= MAX_ENTRIES });
|
|
88
87
|
}
|
|
89
88
|
/**
|
|
90
89
|
* Check if jscpd is available, auto-install if not
|
|
@@ -144,9 +143,23 @@ export class JscpdClient {
|
|
|
144
143
|
* Scan a directory for duplicate code blocks.
|
|
145
144
|
* Uses a temp output dir to capture JSON report.
|
|
146
145
|
* @param isTsProject - If true, excludes .js files (they're compiled artifacts in TS projects)
|
|
146
|
+
*
|
|
147
|
+
* Root contract (#747/#250): unlike `KnipClient`/`DeadCodeClient`, this
|
|
148
|
+
* client does NOT resolve a project root via `findNearestMarkerRoot`, so it
|
|
149
|
+
* has historically relied on every caller to guard the scan root itself. The
|
|
150
|
+
* `isAtOrAboveHomeDir` refusal below is belt-and-braces internal protection
|
|
151
|
+
* so a FUTURE caller that forgets that contract can't spawn a whole-$HOME
|
|
152
|
+
* jscpd walk (the observed OOM: a jscpd run from a WSL home reached 44 GB
|
|
153
|
+
* RSS). `options.homeDir` overrides `os.homedir()` for tests.
|
|
147
154
|
*/
|
|
148
|
-
async scan(cwd, minLines = 5, minTokens = 50, isTsProject = false) {
|
|
155
|
+
async scan(cwd, minLines = 5, minTokens = 50, isTsProject = false, options = {}) {
|
|
149
156
|
const targetDir = path.resolve(cwd);
|
|
157
|
+
// #747/#250: never walk from a cwd at/above $HOME — from there jscpd's
|
|
158
|
+
// tokenizer would run across every unrelated repo under home.
|
|
159
|
+
if (isAtOrAboveHomeDir(targetDir, options.homeDir)) {
|
|
160
|
+
this.log(`Refusing scan of unsafe root at/above home: ${targetDir}`);
|
|
161
|
+
return { ...EMPTY_RESULT };
|
|
162
|
+
}
|
|
150
163
|
// Return early for non-existent or empty directories before probing/installing.
|
|
151
164
|
if (!fs.existsSync(targetDir)) {
|
|
152
165
|
return { ...EMPTY_RESULT };
|
|
@@ -5,6 +5,7 @@ import { getProjectIgnoreMatcher } from "./file-utils.js";
|
|
|
5
5
|
import { LANGUAGE_POLICY, } from "./language-policy.js";
|
|
6
6
|
import { getSourceFiles } from "./scan-utils.js";
|
|
7
7
|
import { readDirEntriesSafe, shouldRecurseIntoDir } from "./source-walker.js";
|
|
8
|
+
import { findNearestDirWithAnyBasename } from "./workspace-topology.js";
|
|
8
9
|
export const SUPPORTED_FILE_KINDS = [
|
|
9
10
|
"jsts",
|
|
10
11
|
"python",
|
|
@@ -114,24 +115,6 @@ const ROOT_MARKERS_BY_KIND = {
|
|
|
114
115
|
nix: ["flake.nix"],
|
|
115
116
|
toml: ["pyproject.toml", "Cargo.toml", "taplo.toml"],
|
|
116
117
|
};
|
|
117
|
-
function nearestRoot(start, markers) {
|
|
118
|
-
let dir = path.resolve(start);
|
|
119
|
-
const { root } = path.parse(dir);
|
|
120
|
-
while (true) {
|
|
121
|
-
for (const marker of markers) {
|
|
122
|
-
if (fs.existsSync(path.join(dir, marker))) {
|
|
123
|
-
return dir;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
if (dir === root)
|
|
127
|
-
break;
|
|
128
|
-
const parent = path.dirname(dir);
|
|
129
|
-
if (parent === dir)
|
|
130
|
-
break;
|
|
131
|
-
dir = parent;
|
|
132
|
-
}
|
|
133
|
-
return undefined;
|
|
134
|
-
}
|
|
135
118
|
// Process-lifetime memo keyed on projectRoot. Only populated when the
|
|
136
119
|
// caller did not pass an explicit `sourceFiles` array — the explicit-array
|
|
137
120
|
// case is used by the warmup pipeline to inject pre-collected files and
|
|
@@ -225,7 +208,17 @@ export function resolveLanguageRootForFile(filePath, workspaceRoot) {
|
|
|
225
208
|
if (!markers || markers.length === 0) {
|
|
226
209
|
return path.resolve(workspaceRoot);
|
|
227
210
|
}
|
|
228
|
-
|
|
211
|
+
// #807: nearest-marker discovery now shares workspace-topology.ts's single
|
|
212
|
+
// walk/cache/invalidation seam instead of this file's own per-kind
|
|
213
|
+
// `existsSync` climb loop. `findNearestDirWithAnyBasename` additionally
|
|
214
|
+
// rejects a marker at/above `$HOME` (the pre-#807 loop didn't), matching
|
|
215
|
+
// the AGENTS.md walker invariant — harmless here because the
|
|
216
|
+
// workspace-relative check right below already discards any candidate
|
|
217
|
+
// outside `workspaceRoot`, and `workspaceRoot` is never at/above `$HOME`
|
|
218
|
+
// in practice. This keeps resolveLanguageRootForFile's OWN stricter
|
|
219
|
+
// workspace-relative policy check unchanged: the topology service only
|
|
220
|
+
// supplies discovery, not this function's policy.
|
|
221
|
+
const found = findNearestDirWithAnyBasename(startDir, markers);
|
|
229
222
|
if (!found)
|
|
230
223
|
return path.resolve(workspaceRoot);
|
|
231
224
|
const workspace = path.resolve(workspaceRoot);
|
|
@@ -1,18 +1,38 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import { parseEnabledShape } from "./config-enabled-shape.js";
|
|
5
|
+
import { findNestedProjectMutationValue, } from "./project-lens-config.js";
|
|
4
6
|
export function getPiLensGlobalConfigPath(homeDir = os.homedir()) {
|
|
5
7
|
const override = process.env.PI_LENS_CONFIG_PATH;
|
|
6
8
|
if (override)
|
|
7
9
|
return path.resolve(override);
|
|
8
10
|
return path.join(homeDir, ".pi-lens", "config.json");
|
|
9
11
|
}
|
|
12
|
+
const warnedInvalidGlobalConfigs = new Set();
|
|
13
|
+
/**
|
|
14
|
+
* Same warn-once-per-(path, reason) contract as project-lens-config.ts's
|
|
15
|
+
* `warnInvalidConfigOnce` — a malformed global config value is logged once
|
|
16
|
+
* and then treated as absent, rather than silently dropped (#792).
|
|
17
|
+
*/
|
|
18
|
+
function warnInvalidGlobalConfigOnce(configPath, reason) {
|
|
19
|
+
const key = `${configPath}:${reason}`;
|
|
20
|
+
if (warnedInvalidGlobalConfigs.has(key))
|
|
21
|
+
return;
|
|
22
|
+
warnedInvalidGlobalConfigs.add(key);
|
|
23
|
+
console.error(`[pi-lens] ignoring invalid global config ${configPath}: ${reason}`);
|
|
24
|
+
}
|
|
25
|
+
/** For tests that need to force the warn-once cache to reset between cases. */
|
|
26
|
+
export function resetGlobalConfigWarnCache() {
|
|
27
|
+
warnedInvalidGlobalConfigs.clear();
|
|
28
|
+
}
|
|
10
29
|
export function loadPiLensGlobalConfig(configPath = getPiLensGlobalConfigPath()) {
|
|
11
30
|
try {
|
|
12
31
|
const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
13
32
|
if (!parsed || typeof parsed !== "object")
|
|
14
33
|
return undefined;
|
|
15
34
|
const raw = parsed;
|
|
35
|
+
const warnInvalid = (reason) => warnInvalidGlobalConfigOnce(configPath, reason);
|
|
16
36
|
const dispatchRaw = raw.dispatch;
|
|
17
37
|
const dispatch = dispatchRaw && typeof dispatchRaw === "object"
|
|
18
38
|
? dispatchRaw
|
|
@@ -21,19 +41,17 @@ export function loadPiLensGlobalConfig(configPath = getPiLensGlobalConfigPath())
|
|
|
21
41
|
const widget = widgetRaw && typeof widgetRaw === "object"
|
|
22
42
|
? widgetRaw
|
|
23
43
|
: undefined;
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
44
|
+
const format = parseEnabledShape(raw.format, "format", warnInvalid);
|
|
45
|
+
const autofix = parseEnabledShape(raw.autofix, "autofix", warnInvalid);
|
|
46
|
+
const formatModeRaw = raw.format;
|
|
47
|
+
const formatModeSource = formatModeRaw && typeof formatModeRaw === "object"
|
|
48
|
+
? formatModeRaw
|
|
27
49
|
: undefined;
|
|
28
50
|
const actionableWarningsRaw = raw.actionableWarnings;
|
|
29
51
|
const actionableWarnings = actionableWarningsRaw && typeof actionableWarningsRaw === "object"
|
|
30
52
|
? actionableWarningsRaw
|
|
31
53
|
: undefined;
|
|
32
|
-
const
|
|
33
|
-
const actionableWarningsAutoFix = actionableWarningsAutoFixRaw &&
|
|
34
|
-
typeof actionableWarningsAutoFixRaw === "object"
|
|
35
|
-
? actionableWarningsAutoFixRaw
|
|
36
|
-
: undefined;
|
|
54
|
+
const actionableWarningsAutoFix = parseEnabledShape(actionableWarnings?.autoFix, "actionableWarnings.autoFix", warnInvalid);
|
|
37
55
|
const contextInjectionRaw = raw.contextInjection;
|
|
38
56
|
const contextInjection = contextInjectionRaw && typeof contextInjectionRaw === "object"
|
|
39
57
|
? contextInjectionRaw
|
|
@@ -42,8 +60,9 @@ export function loadPiLensGlobalConfig(configPath = getPiLensGlobalConfigPath())
|
|
|
42
60
|
const turnSummary = turnSummaryRaw && typeof turnSummaryRaw === "object"
|
|
43
61
|
? turnSummaryRaw
|
|
44
62
|
: undefined;
|
|
45
|
-
const formatMode =
|
|
46
|
-
|
|
63
|
+
const formatMode = formatModeSource?.mode === "immediate" ||
|
|
64
|
+
formatModeSource?.mode === "deferred"
|
|
65
|
+
? formatModeSource.mode
|
|
47
66
|
: undefined;
|
|
48
67
|
const ignore = Array.isArray(raw.ignore)
|
|
49
68
|
? raw.ignore.filter((p) => typeof p === "string")
|
|
@@ -66,10 +85,11 @@ export function loadPiLensGlobalConfig(configPath = getPiLensGlobalConfigPath())
|
|
|
66
85
|
: undefined,
|
|
67
86
|
format: format
|
|
68
87
|
? {
|
|
69
|
-
enabled:
|
|
88
|
+
enabled: format.enabled,
|
|
70
89
|
mode: formatMode,
|
|
71
90
|
}
|
|
72
91
|
: undefined,
|
|
92
|
+
autofix: autofix ? { enabled: autofix.enabled } : undefined,
|
|
73
93
|
actionableWarnings: actionableWarnings
|
|
74
94
|
? {
|
|
75
95
|
enabled: typeof actionableWarnings.enabled === "boolean"
|
|
@@ -82,11 +102,7 @@ export function loadPiLensGlobalConfig(configPath = getPiLensGlobalConfigPath())
|
|
|
82
102
|
? actionableWarnings.deltaOnly
|
|
83
103
|
: undefined,
|
|
84
104
|
autoFix: actionableWarningsAutoFix
|
|
85
|
-
? {
|
|
86
|
-
enabled: typeof actionableWarningsAutoFix.enabled === "boolean"
|
|
87
|
-
? actionableWarningsAutoFix.enabled
|
|
88
|
-
: undefined,
|
|
89
|
-
}
|
|
105
|
+
? { enabled: actionableWarningsAutoFix.enabled }
|
|
90
106
|
: undefined,
|
|
91
107
|
}
|
|
92
108
|
: undefined,
|
|
@@ -119,6 +135,9 @@ export function getGlobalWidgetDefaultVisible(configPath) {
|
|
|
119
135
|
export function getGlobalAutoformatEnabled(configPath) {
|
|
120
136
|
return loadPiLensGlobalConfig(configPath)?.format?.enabled !== false;
|
|
121
137
|
}
|
|
138
|
+
export function getGlobalAutofixEnabled(configPath) {
|
|
139
|
+
return loadPiLensGlobalConfig(configPath)?.autofix?.enabled !== false;
|
|
140
|
+
}
|
|
122
141
|
export function getGlobalImmediateFormatDefault(configPath) {
|
|
123
142
|
return loadPiLensGlobalConfig(configPath)?.format?.mode === "immediate";
|
|
124
143
|
}
|
|
@@ -128,32 +147,92 @@ export function getGlobalContextInjectionEnabled(configPath) {
|
|
|
128
147
|
export function getGlobalTurnSummaryEnabled(configPath) {
|
|
129
148
|
return loadPiLensGlobalConfig(configPath)?.turnSummary?.enabled === true;
|
|
130
149
|
}
|
|
131
|
-
|
|
150
|
+
/**
|
|
151
|
+
* Resolve a flag AND report which config tier decided it — same precedence
|
|
152
|
+
* as {@link resolvePiLensFlag} (which now delegates here), just also
|
|
153
|
+
* returning the `source` so callers can log e.g.
|
|
154
|
+
* "(--no-autofix, source=project)" instead of a bare boolean (#792).
|
|
155
|
+
*
|
|
156
|
+
* Precedence (unchanged, maintainer decision — project wins over global,
|
|
157
|
+
* including re-enabling; only an explicit CLI disabling flag outranks
|
|
158
|
+
* project config): cli → project → global → default.
|
|
159
|
+
*/
|
|
160
|
+
export function resolvePiLensFlagWithSource(name, value, config, projectConfig, editedFilePath, projectRoot) {
|
|
132
161
|
if (value)
|
|
133
|
-
return value;
|
|
162
|
+
return { value, source: "cli" };
|
|
163
|
+
const mutationFlag = name === "no-autoformat" ||
|
|
164
|
+
name === "no-autofix" ||
|
|
165
|
+
name === "lens-actionable-warning-autofix"
|
|
166
|
+
? name
|
|
167
|
+
: undefined;
|
|
168
|
+
const nested = mutationFlag && editedFilePath && projectRoot
|
|
169
|
+
? findNestedProjectMutationValue(mutationFlag, editedFilePath, projectRoot)
|
|
170
|
+
: undefined;
|
|
171
|
+
const nestedSource = nested
|
|
172
|
+
? path.resolve(nested.dir) === path.resolve(projectRoot)
|
|
173
|
+
? "project"
|
|
174
|
+
: `nested-project:${nested.dir}`
|
|
175
|
+
: undefined;
|
|
134
176
|
if (name === "no-autoformat") {
|
|
135
|
-
|
|
177
|
+
if (nested)
|
|
178
|
+
return { value: !nested.value, source: nestedSource };
|
|
179
|
+
if (projectConfig?.format?.enabled !== undefined) {
|
|
180
|
+
return { value: !projectConfig.format.enabled, source: "project" };
|
|
181
|
+
}
|
|
182
|
+
if (config?.format?.enabled === false) {
|
|
183
|
+
return { value: true, source: "global" };
|
|
184
|
+
}
|
|
185
|
+
return { value: false, source: "default" };
|
|
186
|
+
}
|
|
187
|
+
if (name === "no-autofix") {
|
|
188
|
+
if (nested)
|
|
189
|
+
return { value: !nested.value, source: nestedSource };
|
|
190
|
+
if (projectConfig?.autofix?.enabled !== undefined) {
|
|
191
|
+
return { value: !projectConfig.autofix.enabled, source: "project" };
|
|
192
|
+
}
|
|
193
|
+
if (config?.autofix?.enabled === false) {
|
|
194
|
+
return { value: true, source: "global" };
|
|
195
|
+
}
|
|
196
|
+
return { value: false, source: "default" };
|
|
136
197
|
}
|
|
137
198
|
if (name === "immediate-format") {
|
|
138
|
-
|
|
199
|
+
const immediate = config?.format?.mode === "immediate";
|
|
200
|
+
return { value: immediate, source: immediate ? "global" : "default" };
|
|
139
201
|
}
|
|
140
202
|
if (name === "lens-actionable-warnings") {
|
|
141
|
-
|
|
203
|
+
const enabled = config?.actionableWarnings?.enabled === true;
|
|
204
|
+
return { value: enabled, source: enabled ? "global" : "default" };
|
|
142
205
|
}
|
|
143
206
|
if (name === "lens-actionable-warning-actions") {
|
|
144
|
-
|
|
207
|
+
const enabled = config?.actionableWarnings?.includeLspCodeActions === true;
|
|
208
|
+
return { value: enabled, source: enabled ? "global" : "default" };
|
|
145
209
|
}
|
|
146
210
|
if (name === "lens-actionable-warning-autofix") {
|
|
147
|
-
|
|
211
|
+
if (nested)
|
|
212
|
+
return { value: nested.value, source: nestedSource };
|
|
213
|
+
if (projectConfig?.actionableWarnings?.autoFix?.enabled !== undefined) {
|
|
214
|
+
return {
|
|
215
|
+
value: projectConfig.actionableWarnings.autoFix.enabled,
|
|
216
|
+
source: "project",
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
const enabled = config?.actionableWarnings?.autoFix?.enabled === true;
|
|
220
|
+
return { value: enabled, source: enabled ? "global" : "default" };
|
|
148
221
|
}
|
|
149
222
|
if (name === "lens-actionable-warning-all") {
|
|
150
|
-
|
|
223
|
+
const all = config?.actionableWarnings?.deltaOnly === false;
|
|
224
|
+
return { value: all, source: all ? "global" : "default" };
|
|
151
225
|
}
|
|
152
226
|
if (name === "no-lens-context") {
|
|
153
|
-
|
|
227
|
+
const disabled = config?.contextInjection?.enabled === false;
|
|
228
|
+
return { value: disabled, source: disabled ? "global" : "default" };
|
|
154
229
|
}
|
|
155
230
|
if (name === "lens-turn-summary") {
|
|
156
|
-
|
|
231
|
+
const enabled = config?.turnSummary?.enabled === true;
|
|
232
|
+
return { value: enabled, source: enabled ? "global" : "default" };
|
|
157
233
|
}
|
|
158
|
-
return value;
|
|
234
|
+
return { value, source: "default" };
|
|
235
|
+
}
|
|
236
|
+
export function resolvePiLensFlag(name, value, config, projectConfig, editedFilePath, projectRoot) {
|
|
237
|
+
return resolvePiLensFlagWithSource(name, value, config, projectConfig, editedFilePath, projectRoot).value;
|
|
159
238
|
}
|