ga-plugins-cli 0.1.0 → 0.1.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/dist/config-patcher.d.ts +20 -50
- package/dist/config-patcher.d.ts.map +1 -1
- package/dist/config-patcher.js +138 -102
- package/dist/config-patcher.js.map +1 -1
- package/dist/index.js +41 -5
- package/dist/index.js.map +1 -1
- package/dist/installer.d.ts +0 -18
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +19 -39
- package/dist/installer.js.map +1 -1
- package/dist/types.d.ts +10 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/uninstaller.d.ts +0 -23
- package/dist/uninstaller.d.ts.map +1 -1
- package/dist/uninstaller.js +22 -68
- package/dist/uninstaller.js.map +1 -1
- package/package.json +3 -2
- package/plugins/go-reviewer/.claude-plugin/plugin.json +12 -0
- package/plugins/go-reviewer/commands/go-review.md +424 -0
- package/plugins/go-reviewer/mcp-servers/go-reviewer-mcp/README.md +236 -0
- package/plugins/go-reviewer/mcp-servers/go-reviewer-mcp/main.go +678 -0
- package/plugins/go-scaffolder/.claude-plugin/plugin.json +12 -0
- package/plugins/go-scaffolder/commands/scaffold-service.md +802 -0
- package/plugins/go-scaffolder/reference-service/.env.example +27 -0
- package/plugins/go-scaffolder/reference-service/Dockerfile +55 -0
- package/plugins/go-scaffolder/reference-service/REFERENCE-SERVICE-NOTICE.md +104 -0
- package/plugins/go-scaffolder/reference-service/cmd/server/main.go +266 -0
- package/plugins/go-scaffolder/reference-service/config/config.go +67 -0
- package/plugins/go-scaffolder/reference-service/go.mod +17 -0
- package/plugins/go-scaffolder/reference-service/internal/domain/booking.go +118 -0
- package/plugins/go-scaffolder/reference-service/internal/handler/booking.go +242 -0
- package/plugins/go-scaffolder/reference-service/internal/handler/booking_test.go +451 -0
- package/plugins/go-scaffolder/reference-service/internal/repository/booking_postgres.go +124 -0
- package/plugins/go-scaffolder/reference-service/internal/usecase/booking.go +181 -0
- package/plugins/go-standards/.claude-plugin/plugin.json +22 -0
- package/plugins/go-standards/commands/go-standards-check.md +232 -0
- package/plugins/go-standards/skills/concurrency.md +336 -0
- package/plugins/go-standards/skills/config.md +267 -0
- package/plugins/go-standards/skills/error-handling.md +286 -0
- package/plugins/go-standards/skills/http-chi.md +390 -0
- package/plugins/go-standards/skills/logging-observability.md +340 -0
- package/plugins/go-standards/skills/naming-and-style.md +315 -0
- package/plugins/go-standards/skills/project-layout.md +313 -0
- package/plugins/go-standards/skills/testing.md +366 -0
- package/plugins/java2go-porter/.claude-plugin/plugin.json +21 -0
- package/plugins/java2go-porter/agents/analyzer.md +232 -0
- package/plugins/java2go-porter/agents/reviewer.md +241 -0
- package/plugins/java2go-porter/agents/test-pairer.md +365 -0
- package/plugins/java2go-porter/agents/translator.md +419 -0
- package/plugins/java2go-porter/commands/port-java-service.md +149 -0
- package/plugins/java2go-porter/skills/idiom-mapping.md +75 -0
- package/plugins/migration-safety/.claude-plugin/plugin.json +20 -0
- package/plugins/migration-safety/commands/gen-characterization-test.md +452 -0
- package/plugins/migration-safety/commands/strangler-plan.md +356 -0
- package/plugins/migration-safety/skills/strangler-fig.md +382 -0
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
// Package main implements a thin MCP (Model Context Protocol) shim server that runs
|
|
2
|
+
// external Go analysis binaries (golangci-lint, gopls, semgrep) and returns structured
|
|
3
|
+
// findings to Claude. It speaks JSON-RPC 2.0 over stdio.
|
|
4
|
+
//
|
|
5
|
+
// Graceful degradation: if a binary is missing on PATH, the tool returns
|
|
6
|
+
// {"skipped": true, "reason": "..."} instead of an error. The server never panics.
|
|
7
|
+
package main
|
|
8
|
+
|
|
9
|
+
import (
|
|
10
|
+
"bufio"
|
|
11
|
+
"bytes"
|
|
12
|
+
"encoding/json"
|
|
13
|
+
"fmt"
|
|
14
|
+
"io"
|
|
15
|
+
"log"
|
|
16
|
+
"os"
|
|
17
|
+
"os/exec"
|
|
18
|
+
"path/filepath"
|
|
19
|
+
"strconv"
|
|
20
|
+
"strings"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
// ─── MCP protocol types ───────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
// Request is an incoming JSON-RPC 2.0 request.
|
|
26
|
+
type Request struct {
|
|
27
|
+
JSONRPC string `json:"jsonrpc"`
|
|
28
|
+
ID json.RawMessage `json:"id"`
|
|
29
|
+
Method string `json:"method"`
|
|
30
|
+
Params json.RawMessage `json:"params,omitempty"`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Response is a JSON-RPC 2.0 response.
|
|
34
|
+
type Response struct {
|
|
35
|
+
JSONRPC string `json:"jsonrpc"`
|
|
36
|
+
ID json.RawMessage `json:"id"`
|
|
37
|
+
Result interface{} `json:"result,omitempty"`
|
|
38
|
+
Error *RPCError `json:"error,omitempty"`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// RPCError represents a JSON-RPC error object.
|
|
42
|
+
type RPCError struct {
|
|
43
|
+
Code int `json:"code"`
|
|
44
|
+
Message string `json:"message"`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ─── Domain types ─────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
// Finding represents a single diagnostic finding from any linter.
|
|
50
|
+
type Finding struct {
|
|
51
|
+
File string `json:"File"`
|
|
52
|
+
Line int `json:"Line"`
|
|
53
|
+
Rule string `json:"Rule"`
|
|
54
|
+
Severity string `json:"Severity"`
|
|
55
|
+
Message string `json:"Message"`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ToolResult is the standard response envelope for every tool call.
|
|
59
|
+
type ToolResult struct {
|
|
60
|
+
Skipped bool `json:"skipped"`
|
|
61
|
+
Reason string `json:"reason,omitempty"`
|
|
62
|
+
Findings []Finding `json:"findings,omitempty"`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── MCP initialization types ─────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
// ServerInfo describes this MCP server.
|
|
68
|
+
type ServerInfo struct {
|
|
69
|
+
Name string `json:"name"`
|
|
70
|
+
Version string `json:"version"`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// CapabilitiesResult is the response to initialize.
|
|
74
|
+
type CapabilitiesResult struct {
|
|
75
|
+
ProtocolVersion string `json:"protocolVersion"`
|
|
76
|
+
ServerInfo ServerInfo `json:"serverInfo"`
|
|
77
|
+
Capabilities struct {
|
|
78
|
+
Tools struct{} `json:"tools"`
|
|
79
|
+
} `json:"capabilities"`
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ToolDefinition describes a single MCP tool.
|
|
83
|
+
type ToolDefinition struct {
|
|
84
|
+
Name string `json:"name"`
|
|
85
|
+
Description string `json:"description"`
|
|
86
|
+
InputSchema json.RawMessage `json:"inputSchema"`
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ToolsListResult is the response to tools/list.
|
|
90
|
+
type ToolsListResult struct {
|
|
91
|
+
Tools []ToolDefinition `json:"tools"`
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ─── Input parameter types ────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
// LintParams are parameters for run_golangci_lint and run_gopls_check.
|
|
97
|
+
type LintParams struct {
|
|
98
|
+
Path string `json:"path"`
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// SemgrepParams are parameters for run_semgrep.
|
|
102
|
+
type SemgrepParams struct {
|
|
103
|
+
Path string `json:"path"`
|
|
104
|
+
Rules string `json:"rules"`
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ToolCallParams wraps an MCP tools/call request.
|
|
108
|
+
type ToolCallParams struct {
|
|
109
|
+
Name string `json:"name"`
|
|
110
|
+
Arguments json.RawMessage `json:"arguments"`
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ─── Linter output parsing types ─────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
// golangciOutput is the top-level JSON output from golangci-lint.
|
|
116
|
+
type golangciOutput struct {
|
|
117
|
+
Issues []golangciIssue `json:"Issues"`
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
type golangciIssue struct {
|
|
121
|
+
FromLinter string `json:"FromLinter"`
|
|
122
|
+
Text string `json:"Text"`
|
|
123
|
+
Severity string `json:"Severity"`
|
|
124
|
+
SourceLines []string `json:"SourceLines"`
|
|
125
|
+
Pos golangciPos `json:"Pos"`
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
type golangciPos struct {
|
|
129
|
+
Filename string `json:"Filename"`
|
|
130
|
+
Offset int `json:"Offset"`
|
|
131
|
+
Line int `json:"Line"`
|
|
132
|
+
Column int `json:"Column"`
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// semgrepOutput is the top-level JSON output from semgrep.
|
|
136
|
+
type semgrepOutput struct {
|
|
137
|
+
Results []semgrepResult `json:"results"`
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
type semgrepResult struct {
|
|
141
|
+
CheckID string `json:"check_id"`
|
|
142
|
+
Path string `json:"path"`
|
|
143
|
+
Start semgrepPos `json:"start"`
|
|
144
|
+
Extra semgrepExtra `json:"extra"`
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
type semgrepPos struct {
|
|
148
|
+
Line int `json:"line"`
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
type semgrepExtra struct {
|
|
152
|
+
Message string `json:"message"`
|
|
153
|
+
Severity string `json:"severity"`
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ─── Server ───────────────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
// server holds the JSON-RPC server state.
|
|
159
|
+
type server struct {
|
|
160
|
+
reader *bufio.Reader
|
|
161
|
+
writer io.Writer
|
|
162
|
+
logger *log.Logger
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
func newServer() *server {
|
|
166
|
+
return &server{
|
|
167
|
+
reader: bufio.NewReader(os.Stdin),
|
|
168
|
+
writer: os.Stdout,
|
|
169
|
+
logger: log.New(os.Stderr, "[go-reviewer-mcp] ", log.LstdFlags),
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
func (s *server) run() {
|
|
174
|
+
s.logger.Println("go-reviewer-mcp started, listening on stdio")
|
|
175
|
+
|
|
176
|
+
for {
|
|
177
|
+
line, err := s.reader.ReadBytes('\n')
|
|
178
|
+
if err != nil {
|
|
179
|
+
if err == io.EOF {
|
|
180
|
+
s.logger.Println("stdin closed, shutting down")
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
s.logger.Printf("read error: %v", err)
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
line = bytes.TrimSpace(line)
|
|
188
|
+
if len(line) == 0 {
|
|
189
|
+
continue
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
var req Request
|
|
193
|
+
if err := json.Unmarshal(line, &req); err != nil {
|
|
194
|
+
s.logger.Printf("JSON parse error: %v (raw: %s)", err, line)
|
|
195
|
+
s.writeError(json.RawMessage("null"), -32700, "Parse error")
|
|
196
|
+
continue
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
s.handleRequest(&req)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
func (s *server) handleRequest(req *Request) {
|
|
204
|
+
switch req.Method {
|
|
205
|
+
case "initialize":
|
|
206
|
+
s.handleInitialize(req)
|
|
207
|
+
case "initialized":
|
|
208
|
+
// notification — no response required
|
|
209
|
+
case "tools/list":
|
|
210
|
+
s.handleToolsList(req)
|
|
211
|
+
case "tools/call":
|
|
212
|
+
s.handleToolsCall(req)
|
|
213
|
+
default:
|
|
214
|
+
s.writeError(req.ID, -32601, fmt.Sprintf("Method not found: %s", req.Method))
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
func (s *server) handleInitialize(req *Request) {
|
|
219
|
+
result := CapabilitiesResult{
|
|
220
|
+
ProtocolVersion: "2024-11-05",
|
|
221
|
+
ServerInfo: ServerInfo{
|
|
222
|
+
Name: "go-reviewer-mcp",
|
|
223
|
+
Version: "0.1.0",
|
|
224
|
+
},
|
|
225
|
+
}
|
|
226
|
+
s.writeResult(req.ID, result)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
func (s *server) handleToolsList(req *Request) {
|
|
230
|
+
tools := []ToolDefinition{
|
|
231
|
+
{
|
|
232
|
+
Name: "run_golangci_lint",
|
|
233
|
+
Description: "Run golangci-lint on a Go package or file path. Returns structured findings. If golangci-lint is not on PATH, returns {skipped: true}.",
|
|
234
|
+
InputSchema: json.RawMessage(`{
|
|
235
|
+
"type": "object",
|
|
236
|
+
"properties": {
|
|
237
|
+
"path": {
|
|
238
|
+
"type": "string",
|
|
239
|
+
"description": "Absolute path to a Go file or package directory"
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
"required": ["path"]
|
|
243
|
+
}`),
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
Name: "run_gopls_check",
|
|
247
|
+
Description: "Run gopls check on a Go file or directory for type-aware diagnostics. Returns structured findings. If gopls is not on PATH, returns {skipped: true}.",
|
|
248
|
+
InputSchema: json.RawMessage(`{
|
|
249
|
+
"type": "object",
|
|
250
|
+
"properties": {
|
|
251
|
+
"path": {
|
|
252
|
+
"type": "string",
|
|
253
|
+
"description": "Absolute path to a Go file or package directory"
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
"required": ["path"]
|
|
257
|
+
}`),
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
Name: "run_semgrep",
|
|
261
|
+
Description: "Run semgrep on a path using specified rules. Returns structured findings. If semgrep is not on PATH, returns {skipped: true}.",
|
|
262
|
+
InputSchema: json.RawMessage(`{
|
|
263
|
+
"type": "object",
|
|
264
|
+
"properties": {
|
|
265
|
+
"path": {
|
|
266
|
+
"type": "string",
|
|
267
|
+
"description": "Absolute path to scan"
|
|
268
|
+
},
|
|
269
|
+
"rules": {
|
|
270
|
+
"type": "string",
|
|
271
|
+
"description": "Semgrep rule set (e.g. 'p/golang', 'p/security-audit', or path to local rules file). Defaults to 'p/golang'.",
|
|
272
|
+
"default": "p/golang"
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
"required": ["path"]
|
|
276
|
+
}`),
|
|
277
|
+
},
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
s.writeResult(req.ID, ToolsListResult{Tools: tools})
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
func (s *server) handleToolsCall(req *Request) {
|
|
284
|
+
var params ToolCallParams
|
|
285
|
+
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
|
286
|
+
s.writeError(req.ID, -32602, fmt.Sprintf("Invalid params: %v", err))
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
var result interface{}
|
|
291
|
+
var toolErr error
|
|
292
|
+
|
|
293
|
+
switch params.Name {
|
|
294
|
+
case "run_golangci_lint":
|
|
295
|
+
result, toolErr = s.runGolangciLint(params.Arguments)
|
|
296
|
+
case "run_gopls_check":
|
|
297
|
+
result, toolErr = s.runGoplsCheck(params.Arguments)
|
|
298
|
+
case "run_semgrep":
|
|
299
|
+
result, toolErr = s.runSemgrep(params.Arguments)
|
|
300
|
+
default:
|
|
301
|
+
s.writeError(req.ID, -32602, fmt.Sprintf("Unknown tool: %s", params.Name))
|
|
302
|
+
return
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if toolErr != nil {
|
|
306
|
+
// Tool execution errors are returned as tool-level errors, not RPC errors
|
|
307
|
+
s.logger.Printf("tool %s error: %v", params.Name, toolErr)
|
|
308
|
+
s.writeResult(req.ID, ToolResult{
|
|
309
|
+
Skipped: true,
|
|
310
|
+
Reason: fmt.Sprintf("tool execution error: %v", toolErr),
|
|
311
|
+
})
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Wrap in MCP tool call content format
|
|
316
|
+
resultBytes, err := json.Marshal(result)
|
|
317
|
+
if err != nil {
|
|
318
|
+
s.writeError(req.ID, -32603, "Internal error marshaling result")
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
s.writeResult(req.ID, map[string]interface{}{
|
|
323
|
+
"content": []map[string]interface{}{
|
|
324
|
+
{
|
|
325
|
+
"type": "text",
|
|
326
|
+
"text": string(resultBytes),
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
})
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ─── Tool implementations ─────────────────────────────────────────────────────
|
|
333
|
+
|
|
334
|
+
// runGolangciLint executes golangci-lint and parses its JSON output.
|
|
335
|
+
func (s *server) runGolangciLint(rawArgs json.RawMessage) (ToolResult, error) {
|
|
336
|
+
var params LintParams
|
|
337
|
+
if err := json.Unmarshal(rawArgs, ¶ms); err != nil {
|
|
338
|
+
return ToolResult{}, fmt.Errorf("parse params: %w", err)
|
|
339
|
+
}
|
|
340
|
+
if params.Path == "" {
|
|
341
|
+
return ToolResult{}, fmt.Errorf("path is required")
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
binPath, err := exec.LookPath("golangci-lint")
|
|
345
|
+
if err != nil {
|
|
346
|
+
s.logger.Printf("golangci-lint not found on PATH")
|
|
347
|
+
return ToolResult{Skipped: true, Reason: "binary not found: golangci-lint"}, nil
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
args := []string{"run", "--out-format", "json", "--timeout", "120s"}
|
|
351
|
+
|
|
352
|
+
// If path is a file, pass it directly; if directory, append ./...
|
|
353
|
+
info, err := os.Stat(params.Path)
|
|
354
|
+
if err != nil {
|
|
355
|
+
return ToolResult{}, fmt.Errorf("stat path: %w", err)
|
|
356
|
+
}
|
|
357
|
+
if info.IsDir() {
|
|
358
|
+
args = append(args, "./...")
|
|
359
|
+
} else {
|
|
360
|
+
args = append(args, params.Path)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
cmd := exec.Command(binPath, args...)
|
|
364
|
+
if info.IsDir() {
|
|
365
|
+
cmd.Dir = params.Path
|
|
366
|
+
} else {
|
|
367
|
+
cmd.Dir = filepath.Dir(params.Path)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
var stdout, stderr bytes.Buffer
|
|
371
|
+
cmd.Stdout = &stdout
|
|
372
|
+
cmd.Stderr = &stderr
|
|
373
|
+
|
|
374
|
+
// golangci-lint exits non-zero when there are findings — that is expected
|
|
375
|
+
runErr := cmd.Run()
|
|
376
|
+
if runErr != nil {
|
|
377
|
+
// Check if it's a real failure vs just "there are lint issues"
|
|
378
|
+
// golangci-lint exit code 1 = issues found; exit code > 1 = real error
|
|
379
|
+
if exitErr, ok := runErr.(*exec.ExitError); ok {
|
|
380
|
+
if exitErr.ExitCode() > 1 {
|
|
381
|
+
s.logger.Printf("golangci-lint stderr: %s", stderr.String())
|
|
382
|
+
return ToolResult{}, fmt.Errorf("golangci-lint failed (exit %d): %s", exitErr.ExitCode(), stderr.String())
|
|
383
|
+
}
|
|
384
|
+
// exit 1 = findings present, continue parsing
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if stdout.Len() == 0 {
|
|
389
|
+
return ToolResult{Skipped: false, Findings: []Finding{}}, nil
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
var output golangciOutput
|
|
393
|
+
if err := json.Unmarshal(stdout.Bytes(), &output); err != nil {
|
|
394
|
+
s.logger.Printf("golangci-lint parse error: %v, raw: %s", err, stdout.String()[:min(500, stdout.Len())])
|
|
395
|
+
return ToolResult{}, fmt.Errorf("parse golangci-lint output: %w", err)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
findings := make([]Finding, 0, len(output.Issues))
|
|
399
|
+
for _, issue := range output.Issues {
|
|
400
|
+
findings = append(findings, Finding{
|
|
401
|
+
File: issue.Pos.Filename,
|
|
402
|
+
Line: issue.Pos.Line,
|
|
403
|
+
Rule: issue.FromLinter,
|
|
404
|
+
Severity: mapGolangciSeverity(issue.Severity, issue.FromLinter),
|
|
405
|
+
Message: issue.Text,
|
|
406
|
+
})
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return ToolResult{Skipped: false, Findings: findings}, nil
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// mapGolangciSeverity converts golangci-lint severity strings to our levels.
|
|
413
|
+
func mapGolangciSeverity(severity, linter string) string {
|
|
414
|
+
switch strings.ToLower(severity) {
|
|
415
|
+
case "error":
|
|
416
|
+
return "CRITICAL"
|
|
417
|
+
case "warning":
|
|
418
|
+
return "WARNING"
|
|
419
|
+
}
|
|
420
|
+
// Certain linters are always critical regardless of reported severity
|
|
421
|
+
criticalLinters := map[string]bool{
|
|
422
|
+
"errcheck": true, "gosec": true, "govet": true, "bodyclose": true,
|
|
423
|
+
}
|
|
424
|
+
if criticalLinters[linter] {
|
|
425
|
+
return "CRITICAL"
|
|
426
|
+
}
|
|
427
|
+
return "INFO"
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// runGoplsCheck executes gopls check and parses its line-based output.
|
|
431
|
+
func (s *server) runGoplsCheck(rawArgs json.RawMessage) (ToolResult, error) {
|
|
432
|
+
var params LintParams
|
|
433
|
+
if err := json.Unmarshal(rawArgs, ¶ms); err != nil {
|
|
434
|
+
return ToolResult{}, fmt.Errorf("parse params: %w", err)
|
|
435
|
+
}
|
|
436
|
+
if params.Path == "" {
|
|
437
|
+
return ToolResult{}, fmt.Errorf("path is required")
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
binPath, err := exec.LookPath("gopls")
|
|
441
|
+
if err != nil {
|
|
442
|
+
s.logger.Printf("gopls not found on PATH")
|
|
443
|
+
return ToolResult{Skipped: true, Reason: "binary not found: gopls"}, nil
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// gopls check accepts file paths; for directories we enumerate .go files
|
|
447
|
+
var targets []string
|
|
448
|
+
info, err := os.Stat(params.Path)
|
|
449
|
+
if err != nil {
|
|
450
|
+
return ToolResult{}, fmt.Errorf("stat path: %w", err)
|
|
451
|
+
}
|
|
452
|
+
if info.IsDir() {
|
|
453
|
+
goFiles, err := findGoFiles(params.Path)
|
|
454
|
+
if err != nil {
|
|
455
|
+
return ToolResult{}, fmt.Errorf("find go files: %w", err)
|
|
456
|
+
}
|
|
457
|
+
targets = goFiles
|
|
458
|
+
} else {
|
|
459
|
+
targets = []string{params.Path}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if len(targets) == 0 {
|
|
463
|
+
return ToolResult{Skipped: false, Findings: []Finding{}}, nil
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
args := append([]string{"check"}, targets...)
|
|
467
|
+
cmd := exec.Command(binPath, args...)
|
|
468
|
+
|
|
469
|
+
var stdout, stderr bytes.Buffer
|
|
470
|
+
cmd.Stdout = &stdout
|
|
471
|
+
cmd.Stderr = &stderr
|
|
472
|
+
|
|
473
|
+
// gopls check exits non-zero when diagnostics are found
|
|
474
|
+
_ = cmd.Run()
|
|
475
|
+
|
|
476
|
+
// gopls writes diagnostics to stderr
|
|
477
|
+
output := stderr.String()
|
|
478
|
+
if output == "" {
|
|
479
|
+
output = stdout.String()
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
findings := parseGoplsOutput(output)
|
|
483
|
+
return ToolResult{Skipped: false, Findings: findings}, nil
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// parseGoplsOutput parses gopls check line-by-line output.
|
|
487
|
+
// Format: <file>:<line>:<col>: <message>
|
|
488
|
+
func parseGoplsOutput(output string) []Finding {
|
|
489
|
+
var findings []Finding
|
|
490
|
+
for _, line := range strings.Split(output, "\n") {
|
|
491
|
+
line = strings.TrimSpace(line)
|
|
492
|
+
if line == "" {
|
|
493
|
+
continue
|
|
494
|
+
}
|
|
495
|
+
// Split off the message: last colon-delimited segment after "file:line:col:"
|
|
496
|
+
// Pattern: /path/to/file.go:12:34: some message here
|
|
497
|
+
parts := strings.SplitN(line, ":", 5)
|
|
498
|
+
if len(parts) < 4 {
|
|
499
|
+
continue
|
|
500
|
+
}
|
|
501
|
+
filePath := parts[0]
|
|
502
|
+
if !strings.HasSuffix(filePath, ".go") {
|
|
503
|
+
continue
|
|
504
|
+
}
|
|
505
|
+
lineNum, err := strconv.Atoi(strings.TrimSpace(parts[1]))
|
|
506
|
+
if err != nil {
|
|
507
|
+
continue
|
|
508
|
+
}
|
|
509
|
+
// parts[2] is column, parts[3]+ is message
|
|
510
|
+
message := strings.TrimSpace(strings.Join(parts[3:], ":"))
|
|
511
|
+
if message == "" {
|
|
512
|
+
continue
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
findings = append(findings, Finding{
|
|
516
|
+
File: filePath,
|
|
517
|
+
Line: lineNum,
|
|
518
|
+
Rule: "gopls/diagnostic",
|
|
519
|
+
Severity: "WARNING",
|
|
520
|
+
Message: message,
|
|
521
|
+
})
|
|
522
|
+
}
|
|
523
|
+
return findings
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// runSemgrep executes semgrep and parses its JSON output.
|
|
527
|
+
func (s *server) runSemgrep(rawArgs json.RawMessage) (ToolResult, error) {
|
|
528
|
+
var params SemgrepParams
|
|
529
|
+
if err := json.Unmarshal(rawArgs, ¶ms); err != nil {
|
|
530
|
+
return ToolResult{}, fmt.Errorf("parse params: %w", err)
|
|
531
|
+
}
|
|
532
|
+
if params.Path == "" {
|
|
533
|
+
return ToolResult{}, fmt.Errorf("path is required")
|
|
534
|
+
}
|
|
535
|
+
if params.Rules == "" {
|
|
536
|
+
params.Rules = "p/golang"
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
binPath, err := exec.LookPath("semgrep")
|
|
540
|
+
if err != nil {
|
|
541
|
+
s.logger.Printf("semgrep not found on PATH")
|
|
542
|
+
return ToolResult{Skipped: true, Reason: "binary not found: semgrep"}, nil
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
args := []string{
|
|
546
|
+
"--config", params.Rules,
|
|
547
|
+
"--json",
|
|
548
|
+
"--no-git-ignore",
|
|
549
|
+
"--include", "*.go",
|
|
550
|
+
params.Path,
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
cmd := exec.Command(binPath, args...)
|
|
554
|
+
|
|
555
|
+
var stdout, stderr bytes.Buffer
|
|
556
|
+
cmd.Stdout = &stdout
|
|
557
|
+
cmd.Stderr = &stderr
|
|
558
|
+
|
|
559
|
+
// semgrep exits non-zero on findings (exit 1) or errors (exit >1)
|
|
560
|
+
runErr := cmd.Run()
|
|
561
|
+
if runErr != nil {
|
|
562
|
+
if exitErr, ok := runErr.(*exec.ExitError); ok {
|
|
563
|
+
if exitErr.ExitCode() > 1 {
|
|
564
|
+
s.logger.Printf("semgrep stderr: %s", stderr.String())
|
|
565
|
+
return ToolResult{}, fmt.Errorf("semgrep failed (exit %d): %s", exitErr.ExitCode(), stderr.String())
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if stdout.Len() == 0 {
|
|
571
|
+
return ToolResult{Skipped: false, Findings: []Finding{}}, nil
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
var output semgrepOutput
|
|
575
|
+
if err := json.Unmarshal(stdout.Bytes(), &output); err != nil {
|
|
576
|
+
s.logger.Printf("semgrep parse error: %v", err)
|
|
577
|
+
return ToolResult{}, fmt.Errorf("parse semgrep output: %w", err)
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
findings := make([]Finding, 0, len(output.Results))
|
|
581
|
+
for _, result := range output.Results {
|
|
582
|
+
findings = append(findings, Finding{
|
|
583
|
+
File: result.Path,
|
|
584
|
+
Line: result.Start.Line,
|
|
585
|
+
Rule: result.CheckID,
|
|
586
|
+
Severity: mapSemgrepSeverity(result.Extra.Severity),
|
|
587
|
+
Message: result.Extra.Message,
|
|
588
|
+
})
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return ToolResult{Skipped: false, Findings: findings}, nil
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// mapSemgrepSeverity converts semgrep severity strings to our levels.
|
|
595
|
+
func mapSemgrepSeverity(s string) string {
|
|
596
|
+
switch strings.ToUpper(s) {
|
|
597
|
+
case "ERROR":
|
|
598
|
+
return "CRITICAL"
|
|
599
|
+
case "WARNING":
|
|
600
|
+
return "WARNING"
|
|
601
|
+
default:
|
|
602
|
+
return "INFO"
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// ─── Utility functions ────────────────────────────────────────────────────────
|
|
607
|
+
|
|
608
|
+
// findGoFiles returns all .go files (non-test, non-generated) under root.
|
|
609
|
+
func findGoFiles(root string) ([]string, error) {
|
|
610
|
+
var files []string
|
|
611
|
+
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
|
612
|
+
if err != nil {
|
|
613
|
+
return nil // skip unreadable paths
|
|
614
|
+
}
|
|
615
|
+
if info.IsDir() {
|
|
616
|
+
base := info.Name()
|
|
617
|
+
if base == "vendor" || base == "generated" || base == "gen" || strings.HasPrefix(base, ".") {
|
|
618
|
+
return filepath.SkipDir
|
|
619
|
+
}
|
|
620
|
+
return nil
|
|
621
|
+
}
|
|
622
|
+
name := info.Name()
|
|
623
|
+
if strings.HasSuffix(name, ".go") &&
|
|
624
|
+
!strings.HasSuffix(name, "_test.go") &&
|
|
625
|
+
!strings.HasSuffix(name, ".pb.go") {
|
|
626
|
+
files = append(files, path)
|
|
627
|
+
}
|
|
628
|
+
return nil
|
|
629
|
+
})
|
|
630
|
+
return files, err
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// min returns the smaller of two ints (stdlib min was added in Go 1.21).
|
|
634
|
+
func min(a, b int) int {
|
|
635
|
+
if a < b {
|
|
636
|
+
return a
|
|
637
|
+
}
|
|
638
|
+
return b
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// ─── JSON-RPC I/O ─────────────────────────────────────────────────────────────
|
|
642
|
+
|
|
643
|
+
func (s *server) writeResult(id json.RawMessage, result interface{}) {
|
|
644
|
+
resp := Response{
|
|
645
|
+
JSONRPC: "2.0",
|
|
646
|
+
ID: id,
|
|
647
|
+
Result: result,
|
|
648
|
+
}
|
|
649
|
+
s.writeResponse(resp)
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
func (s *server) writeError(id json.RawMessage, code int, message string) {
|
|
653
|
+
resp := Response{
|
|
654
|
+
JSONRPC: "2.0",
|
|
655
|
+
ID: id,
|
|
656
|
+
Error: &RPCError{Code: code, Message: message},
|
|
657
|
+
}
|
|
658
|
+
s.writeResponse(resp)
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
func (s *server) writeResponse(resp Response) {
|
|
662
|
+
data, err := json.Marshal(resp)
|
|
663
|
+
if err != nil {
|
|
664
|
+
s.logger.Printf("marshal response error: %v", err)
|
|
665
|
+
return
|
|
666
|
+
}
|
|
667
|
+
// JSON-RPC over stdio: one message per line
|
|
668
|
+
if _, err := fmt.Fprintf(s.writer, "%s\n", data); err != nil {
|
|
669
|
+
s.logger.Printf("write response error: %v", err)
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// ─── Entry point ──────────────────────────────────────────────────────────────
|
|
674
|
+
|
|
675
|
+
func main() {
|
|
676
|
+
srv := newServer()
|
|
677
|
+
srv.run()
|
|
678
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "go-scaffolder",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Interactive wizard that generates thin Go services backed by go-ga-lib. Asks service type, SQL, NoSQL, broker — imports ONLY chosen packages.",
|
|
5
|
+
"commands": [
|
|
6
|
+
{
|
|
7
|
+
"name": "scaffold-service",
|
|
8
|
+
"description": "Generate a new thin Go microservice wired to go-ga-lib",
|
|
9
|
+
"path": "commands/scaffold-service.md"
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|