pi-lens 3.6.7 → 3.7.1
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 +78 -0
- package/clients/ast-grep-client.ts +31 -9
- package/clients/dispatch/dispatcher.ts +165 -116
- package/clients/dispatch/integration.ts +4 -11
- package/clients/dispatch/plan.ts +16 -1
- package/clients/dispatch/runners/eslint.ts +157 -0
- package/clients/dispatch/runners/golangci-lint.ts +133 -0
- package/clients/dispatch/runners/index.ts +6 -0
- package/clients/dispatch/runners/lsp.ts +5 -1
- package/clients/dispatch/runners/pyright.ts +5 -1
- package/clients/dispatch/runners/rubocop.ts +141 -0
- package/clients/dispatch/runners/type-safety.ts +5 -1
- package/clients/file-kinds.ts +5 -1
- package/clients/formatters.ts +223 -22
- package/clients/installer/index.ts +18 -3
- package/clients/latency-logger.ts +9 -0
- package/clients/lsp/interactive-install.ts +177 -22
- package/clients/lsp/server.ts +142 -50
- package/clients/pipeline.ts +16 -11
- package/clients/test-runner-client.ts +86 -1
- package/index.ts +32 -0
- package/package.json +1 -1
- package/skills/ast-grep/SKILL.md +49 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* golangci-lint runner for dispatch system
|
|
3
|
+
*
|
|
4
|
+
* Runs golangci-lint when a .golangci.yml config is present.
|
|
5
|
+
* golangci-lint is the standard meta-linter for Go projects — it runs
|
|
6
|
+
* staticcheck, errcheck, gosimple, and many others in one pass.
|
|
7
|
+
*
|
|
8
|
+
* Gate: skips when no .golangci.yml/.golangci.yaml config is found (project
|
|
9
|
+
* relies on go-vet only). This avoids noisy default-rule runs on projects
|
|
10
|
+
* that haven't opted in.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import { safeSpawnAsync } from "../../safe-spawn.js";
|
|
16
|
+
import { stripAnsi } from "../../sanitize.js";
|
|
17
|
+
import type {
|
|
18
|
+
Diagnostic,
|
|
19
|
+
DispatchContext,
|
|
20
|
+
RunnerDefinition,
|
|
21
|
+
RunnerResult,
|
|
22
|
+
} from "../types.js";
|
|
23
|
+
|
|
24
|
+
const GOLANGCI_CONFIGS = [
|
|
25
|
+
".golangci.yml",
|
|
26
|
+
".golangci.yaml",
|
|
27
|
+
".golangci.toml",
|
|
28
|
+
".golangci.json",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function hasGolangciConfig(cwd: string): boolean {
|
|
32
|
+
for (const cfg of GOLANGCI_CONFIGS) {
|
|
33
|
+
if (fs.existsSync(path.join(cwd, cfg))) return true;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface GolangciIssue {
|
|
39
|
+
FromLinter: string;
|
|
40
|
+
Text: string;
|
|
41
|
+
Severity?: string;
|
|
42
|
+
Pos: {
|
|
43
|
+
Filename: string;
|
|
44
|
+
Line: number;
|
|
45
|
+
Column: number;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface GolangciOutput {
|
|
50
|
+
Issues: GolangciIssue[] | null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseGolangciJson(raw: string, filePath: string): Diagnostic[] {
|
|
54
|
+
try {
|
|
55
|
+
const output: GolangciOutput = JSON.parse(raw);
|
|
56
|
+
if (!output.Issues) return [];
|
|
57
|
+
|
|
58
|
+
const absFile = path.resolve(filePath);
|
|
59
|
+
|
|
60
|
+
return output.Issues.filter(
|
|
61
|
+
(issue) => path.resolve(issue.Pos.Filename) === absFile,
|
|
62
|
+
).map((issue) => {
|
|
63
|
+
const severity = issue.Severity === "error" ? "error" : "warning";
|
|
64
|
+
return {
|
|
65
|
+
id: `golangci:${issue.FromLinter}:${issue.Pos.Line}`,
|
|
66
|
+
message: `${issue.FromLinter}: ${issue.Text}`,
|
|
67
|
+
filePath,
|
|
68
|
+
line: issue.Pos.Line,
|
|
69
|
+
column: issue.Pos.Column,
|
|
70
|
+
severity,
|
|
71
|
+
semantic: severity === "error" ? "blocking" : "warning",
|
|
72
|
+
tool: "golangci-lint",
|
|
73
|
+
rule: issue.FromLinter,
|
|
74
|
+
} satisfies Diagnostic;
|
|
75
|
+
});
|
|
76
|
+
} catch {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const golangciRunner: RunnerDefinition = {
|
|
82
|
+
id: "golangci-lint",
|
|
83
|
+
appliesTo: ["go"],
|
|
84
|
+
priority: 20,
|
|
85
|
+
enabledByDefault: true,
|
|
86
|
+
|
|
87
|
+
async run(ctx: DispatchContext): Promise<RunnerResult> {
|
|
88
|
+
const cwd = ctx.cwd || process.cwd();
|
|
89
|
+
|
|
90
|
+
// Only run if project has opted in via config file
|
|
91
|
+
if (!hasGolangciConfig(cwd)) {
|
|
92
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check availability
|
|
96
|
+
const versionCheck = await safeSpawnAsync("golangci-lint", ["version"], {
|
|
97
|
+
timeout: 10000,
|
|
98
|
+
cwd,
|
|
99
|
+
});
|
|
100
|
+
if (versionCheck.error || versionCheck.status !== 0) {
|
|
101
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Run on the specific file. golangci-lint accepts file paths directly.
|
|
105
|
+
const result = await safeSpawnAsync(
|
|
106
|
+
"golangci-lint",
|
|
107
|
+
["run", "--out-format=json", ctx.filePath],
|
|
108
|
+
{ timeout: 60000, cwd },
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const raw = stripAnsi(result.stdout + result.stderr);
|
|
112
|
+
|
|
113
|
+
if (result.status === 0) {
|
|
114
|
+
return { status: "succeeded", diagnostics: [], semantic: "none" };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const diagnostics = parseGolangciJson(result.stdout, ctx.filePath);
|
|
118
|
+
|
|
119
|
+
if (diagnostics.length === 0) {
|
|
120
|
+
// Non-zero exit but no parseable issues — likely a config/tool error
|
|
121
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const hasErrors = diagnostics.some((d) => d.semantic === "blocking");
|
|
125
|
+
return {
|
|
126
|
+
status: "failed",
|
|
127
|
+
diagnostics,
|
|
128
|
+
semantic: hasErrors ? "blocking" : "warning",
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export default golangciRunner;
|
|
@@ -7,11 +7,14 @@ import architectRunner from "./architect.js";
|
|
|
7
7
|
import astGrepNapiRunner from "./ast-grep-napi.js";
|
|
8
8
|
import biomeRunner from "./biome.js";
|
|
9
9
|
import configValidationRunner from "./config-validation.js";
|
|
10
|
+
import eslintRunner from "./eslint.js";
|
|
10
11
|
import goVetRunner from "./go-vet.js";
|
|
12
|
+
import golangciRunner from "./golangci-lint.js";
|
|
11
13
|
import lspRunner from "./lsp.js";
|
|
12
14
|
import oxlintRunner from "./oxlint.js";
|
|
13
15
|
import pyrightRunner from "./pyright.js";
|
|
14
16
|
import pythonSlopRunner from "./python-slop.js";
|
|
17
|
+
import rubocopRunner from "./rubocop.js";
|
|
15
18
|
import ruffRunner from "./ruff.js";
|
|
16
19
|
import rustClippyRunner from "./rust-clippy.js";
|
|
17
20
|
import shellcheckRunner from "./shellcheck.js";
|
|
@@ -44,6 +47,9 @@ registerRunner(shellcheckRunner); // Shell script linting (priority 20)
|
|
|
44
47
|
// CLI ast-grep kept for ast_grep_search/ast_grep_replace tools only
|
|
45
48
|
registerRunner(similarityRunner); // Semantic reuse detection (priority 35)
|
|
46
49
|
registerRunner(architectRunner); // Architectural rules (priority 40)
|
|
50
|
+
registerRunner(eslintRunner); // ESLint (priority 12, jsts, config-gated)
|
|
51
|
+
registerRunner(golangciRunner); // golangci-lint (priority 20, go, config-gated)
|
|
52
|
+
registerRunner(rubocopRunner); // RuboCop lint (priority 10, ruby)
|
|
47
53
|
registerRunner(spellcheckRunner); // Spellcheck for markdown/docs (priority 30)
|
|
48
54
|
registerRunner(goVetRunner); // Go analysis (priority 50)
|
|
49
55
|
registerRunner(rustClippyRunner); // Rust analysis (priority 50)
|
|
@@ -117,7 +117,11 @@ const lspRunner: RunnerDefinition = {
|
|
|
117
117
|
return {
|
|
118
118
|
status: hasErrors ? "failed" : "succeeded",
|
|
119
119
|
diagnostics,
|
|
120
|
-
semantic: hasErrors
|
|
120
|
+
semantic: hasErrors
|
|
121
|
+
? "blocking"
|
|
122
|
+
: diagnostics.length > 0
|
|
123
|
+
? "warning"
|
|
124
|
+
: "none",
|
|
121
125
|
};
|
|
122
126
|
},
|
|
123
127
|
};
|
|
@@ -82,7 +82,11 @@ const pyrightRunner: RunnerDefinition = {
|
|
|
82
82
|
return {
|
|
83
83
|
status: hasErrors ? "failed" : "succeeded",
|
|
84
84
|
diagnostics,
|
|
85
|
-
semantic: hasErrors
|
|
85
|
+
semantic: hasErrors
|
|
86
|
+
? "blocking"
|
|
87
|
+
: diagnostics.length > 0
|
|
88
|
+
? "warning"
|
|
89
|
+
: "none",
|
|
86
90
|
};
|
|
87
91
|
} catch {
|
|
88
92
|
// JSON parse error
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RuboCop runner for dispatch system
|
|
3
|
+
*
|
|
4
|
+
* Runs rubocop in lint-only mode (no auto-correct) on Ruby files.
|
|
5
|
+
* Auto-correct is handled by the formatter pipeline — this runner
|
|
6
|
+
* only reports remaining offenses after formatting.
|
|
7
|
+
*
|
|
8
|
+
* Supports bundle exec (preferred in Bundler projects).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import { safeSpawnAsync } from "../../safe-spawn.js";
|
|
14
|
+
import type {
|
|
15
|
+
Diagnostic,
|
|
16
|
+
DispatchContext,
|
|
17
|
+
RunnerDefinition,
|
|
18
|
+
RunnerResult,
|
|
19
|
+
} from "../types.js";
|
|
20
|
+
|
|
21
|
+
function findRubocop(cwd: string): { cmd: string; args: string[] } {
|
|
22
|
+
// Prefer bundle exec if Gemfile exists
|
|
23
|
+
const gemfile = path.join(cwd, "Gemfile");
|
|
24
|
+
if (fs.existsSync(gemfile)) {
|
|
25
|
+
try {
|
|
26
|
+
const content = fs.readFileSync(gemfile, "utf-8");
|
|
27
|
+
if (content.includes("rubocop")) {
|
|
28
|
+
return { cmd: "bundle", args: ["exec", "rubocop"] };
|
|
29
|
+
}
|
|
30
|
+
} catch {}
|
|
31
|
+
}
|
|
32
|
+
return { cmd: "rubocop", args: [] };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface RubocopOffense {
|
|
36
|
+
severity: string;
|
|
37
|
+
message: string;
|
|
38
|
+
cop_name: string;
|
|
39
|
+
correctable: boolean;
|
|
40
|
+
location: {
|
|
41
|
+
line: number;
|
|
42
|
+
column: number;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface RubocopFile {
|
|
47
|
+
path: string;
|
|
48
|
+
offenses: RubocopOffense[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface RubocopOutput {
|
|
52
|
+
files: RubocopFile[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const SEVERITY_MAP: Record<string, "error" | "warning" | "info"> = {
|
|
56
|
+
fatal: "error",
|
|
57
|
+
error: "error",
|
|
58
|
+
warning: "warning",
|
|
59
|
+
convention: "warning",
|
|
60
|
+
refactor: "info",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
function parseRubocopJson(raw: string, filePath: string): Diagnostic[] {
|
|
64
|
+
try {
|
|
65
|
+
const output: RubocopOutput = JSON.parse(raw);
|
|
66
|
+
const diagnostics: Diagnostic[] = [];
|
|
67
|
+
|
|
68
|
+
for (const file of output.files) {
|
|
69
|
+
for (const offense of file.offenses) {
|
|
70
|
+
const severity = SEVERITY_MAP[offense.severity] ?? "warning";
|
|
71
|
+
diagnostics.push({
|
|
72
|
+
id: `rubocop:${offense.cop_name}:${offense.location.line}`,
|
|
73
|
+
message: `${offense.cop_name}: ${offense.message}`,
|
|
74
|
+
filePath,
|
|
75
|
+
line: offense.location.line,
|
|
76
|
+
column: offense.location.column,
|
|
77
|
+
severity,
|
|
78
|
+
semantic: severity === "error" ? "blocking" : "warning",
|
|
79
|
+
tool: "rubocop",
|
|
80
|
+
rule: offense.cop_name,
|
|
81
|
+
fixable: offense.correctable,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return diagnostics;
|
|
87
|
+
} catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const rubocopRunner: RunnerDefinition = {
|
|
93
|
+
id: "rubocop",
|
|
94
|
+
appliesTo: ["ruby"],
|
|
95
|
+
priority: 10,
|
|
96
|
+
enabledByDefault: true,
|
|
97
|
+
|
|
98
|
+
async run(ctx: DispatchContext): Promise<RunnerResult> {
|
|
99
|
+
const cwd = ctx.cwd || process.cwd();
|
|
100
|
+
const { cmd, args } = findRubocop(cwd);
|
|
101
|
+
|
|
102
|
+
// Check availability
|
|
103
|
+
const versionCheck = await safeSpawnAsync(cmd, [...args, "--version"], {
|
|
104
|
+
timeout: 10000,
|
|
105
|
+
cwd,
|
|
106
|
+
});
|
|
107
|
+
if (versionCheck.error || versionCheck.status !== 0) {
|
|
108
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Lint only — no auto-correct (formatter handles that)
|
|
112
|
+
const result = await safeSpawnAsync(
|
|
113
|
+
cmd,
|
|
114
|
+
[...args, "--format", "json", "--no-color", ctx.filePath],
|
|
115
|
+
{ timeout: 30000, cwd },
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// rubocop exits 0 = no offenses, 1 = offenses found, 2 = fatal error
|
|
119
|
+
if (result.status === 2) {
|
|
120
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (result.status === 0) {
|
|
124
|
+
return { status: "succeeded", diagnostics: [], semantic: "none" };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const diagnostics = parseRubocopJson(result.stdout, ctx.filePath);
|
|
128
|
+
if (diagnostics.length === 0) {
|
|
129
|
+
return { status: "succeeded", diagnostics: [], semantic: "none" };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const hasErrors = diagnostics.some((d) => d.semantic === "blocking");
|
|
133
|
+
return {
|
|
134
|
+
status: "failed",
|
|
135
|
+
diagnostics,
|
|
136
|
+
semantic: hasErrors ? "blocking" : "warning",
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export default rubocopRunner;
|
|
@@ -51,7 +51,11 @@ const typeSafetyRunner: RunnerDefinition = {
|
|
|
51
51
|
return {
|
|
52
52
|
status: hasErrors ? "failed" : "succeeded",
|
|
53
53
|
diagnostics,
|
|
54
|
-
semantic: hasErrors
|
|
54
|
+
semantic: hasErrors
|
|
55
|
+
? "blocking"
|
|
56
|
+
: diagnostics.length > 0
|
|
57
|
+
? "warning"
|
|
58
|
+
: "none",
|
|
55
59
|
};
|
|
56
60
|
},
|
|
57
61
|
};
|
package/clients/file-kinds.ts
CHANGED
|
@@ -20,7 +20,8 @@ export type FileKind =
|
|
|
20
20
|
| "json" // JSON (.json)
|
|
21
21
|
| "markdown" // Markdown (.md, .mdx)
|
|
22
22
|
| "css" // CSS (.css, .scss, .less)
|
|
23
|
-
| "yaml"
|
|
23
|
+
| "yaml" // YAML (.yaml, .yml)
|
|
24
|
+
| "ruby"; // Ruby (.rb, .rake, .gemspec, .ru)
|
|
24
25
|
|
|
25
26
|
// --- Extension Maps ---
|
|
26
27
|
|
|
@@ -50,6 +51,7 @@ const KIND_EXTENSIONS: Record<FileKind, readonly string[]> = {
|
|
|
50
51
|
markdown: [".md", ".mdx"],
|
|
51
52
|
css: [".css", ".scss", ".sass", ".less"],
|
|
52
53
|
yaml: [".yaml", ".yml"],
|
|
54
|
+
ruby: [".rb", ".rake", ".gemspec", ".ru"],
|
|
53
55
|
};
|
|
54
56
|
|
|
55
57
|
// Reverse map: extension → file kind (for fast lookup)
|
|
@@ -161,6 +163,7 @@ export function getFileKindLabel(kind: FileKind): string {
|
|
|
161
163
|
markdown: "Markdown",
|
|
162
164
|
css: "CSS",
|
|
163
165
|
yaml: "YAML",
|
|
166
|
+
ruby: "Ruby",
|
|
164
167
|
};
|
|
165
168
|
return labels[kind] ?? kind;
|
|
166
169
|
}
|
|
@@ -211,6 +214,7 @@ export function getLanguageId(kind: FileKind): string {
|
|
|
211
214
|
markdown: "markdown",
|
|
212
215
|
css: "css",
|
|
213
216
|
yaml: "yaml",
|
|
217
|
+
ruby: "ruby",
|
|
214
218
|
};
|
|
215
219
|
return languageIds[kind] ?? "plaintext";
|
|
216
220
|
}
|