ryeos-code 0.1.0__py3-none-any.whl
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.
- ryeos_code/.ai/directives/rye/code/diagnostics.md +48 -0
- ryeos_code/.ai/directives/rye/code/lsp.md +50 -0
- ryeos_code/.ai/directives/rye/code/npm.md +50 -0
- ryeos_code/.ai/directives/rye/code/typescript.md +50 -0
- ryeos_code/.ai/knowledge/rye/code/code-tools.md +349 -0
- ryeos_code/.ai/tools/rye/code/diagnostics/diagnostics.ts +347 -0
- ryeos_code/.ai/tools/rye/code/diagnostics/package-lock.json +555 -0
- ryeos_code/.ai/tools/rye/code/diagnostics/package.json +8 -0
- ryeos_code/.ai/tools/rye/code/git/git.py +246 -0
- ryeos_code/.ai/tools/rye/code/lsp/lsp.ts +438 -0
- ryeos_code/.ai/tools/rye/code/lsp/package-lock.json +593 -0
- ryeos_code/.ai/tools/rye/code/lsp/package.json +12 -0
- ryeos_code/.ai/tools/rye/code/npm/npm.ts +212 -0
- ryeos_code/.ai/tools/rye/code/npm/package-lock.json +555 -0
- ryeos_code/.ai/tools/rye/code/npm/package.json +8 -0
- ryeos_code/.ai/tools/rye/code/typescript/package-lock.json +555 -0
- ryeos_code/.ai/tools/rye/code/typescript/package.json +8 -0
- ryeos_code/.ai/tools/rye/code/typescript/typescript.ts +215 -0
- ryeos_code/__init__.py +1 -0
- ryeos_code/bundle.py +13 -0
- ryeos_code-0.1.0.dist-info/METADATA +13 -0
- ryeos_code-0.1.0.dist-info/RECORD +24 -0
- ryeos_code-0.1.0.dist-info/WHEEL +4 -0
- ryeos_code-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# rye:signed:2026-02-25T00:02:15Z:a23a1a0375614da586cad7d7dcaa6e27ebe4ddb16efad18e250643bd760bbf66:9SsOH5ZdKvZbv8v4xiFxMBRk4Q3ELA3x1TrMEtzITvI9e4ldiSZVsQbkk5ATYN5EegNOV3G4pvA_zBRKhANFBA==:9fbfabe975fa5a7f
|
|
2
|
+
|
|
3
|
+
"""Git operations - status, add, commit, diff, log, branch, checkout, stash, reset, tag."""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
__version__ = "1.0.0"
|
|
11
|
+
__tool_type__ = "python"
|
|
12
|
+
__executor_id__ = "rye/core/runtimes/python/script"
|
|
13
|
+
__category__ = "rye/code/git"
|
|
14
|
+
__tool_description__ = "Git operations - status, add, commit, diff, log, branch, checkout, stash, reset, tag"
|
|
15
|
+
|
|
16
|
+
CONFIG_SCHEMA = {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"properties": {
|
|
19
|
+
"action": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"enum": ["status", "add", "commit", "diff", "log", "branch", "checkout", "stash", "reset", "tag"],
|
|
22
|
+
"description": "Git action to perform",
|
|
23
|
+
},
|
|
24
|
+
"args": {
|
|
25
|
+
"type": "array",
|
|
26
|
+
"items": {"type": "string"},
|
|
27
|
+
"default": [],
|
|
28
|
+
"description": "Arguments for the action (file paths, branch names, etc.)",
|
|
29
|
+
},
|
|
30
|
+
"message": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"description": "Commit/tag message (required for commit action)",
|
|
33
|
+
},
|
|
34
|
+
"flags": {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"default": {},
|
|
37
|
+
"description": "Flags to pass (e.g. { staged: true, amend: true, create: true })",
|
|
38
|
+
},
|
|
39
|
+
"working_dir": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"description": "Working directory (relative to project root or absolute)",
|
|
42
|
+
},
|
|
43
|
+
"timeout": {
|
|
44
|
+
"type": "integer",
|
|
45
|
+
"default": 30,
|
|
46
|
+
"description": "Timeout in seconds",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
"required": ["action"],
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
MAX_OUTPUT_BYTES = 51200
|
|
53
|
+
DEFAULT_TIMEOUT = 30
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def truncate_output(output: str, max_bytes: int) -> tuple[str, bool]:
|
|
57
|
+
encoded = output.encode("utf-8", errors="replace")
|
|
58
|
+
if len(encoded) <= max_bytes:
|
|
59
|
+
return output, False
|
|
60
|
+
|
|
61
|
+
truncated_bytes = encoded[:max_bytes]
|
|
62
|
+
truncated_str = truncated_bytes.decode("utf-8", errors="replace")
|
|
63
|
+
|
|
64
|
+
truncation_msg = f"\n... [output truncated, {len(encoded)} bytes total]"
|
|
65
|
+
return truncated_str + truncation_msg, True
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def build_command(params: dict) -> list[str]:
|
|
69
|
+
action = params["action"]
|
|
70
|
+
args = params.get("args", [])
|
|
71
|
+
flags = params.get("flags", {})
|
|
72
|
+
message = params.get("message")
|
|
73
|
+
|
|
74
|
+
match action:
|
|
75
|
+
case "status":
|
|
76
|
+
cmd = ["git", "status", "--porcelain"]
|
|
77
|
+
if flags.get("long"):
|
|
78
|
+
cmd = ["git", "status"]
|
|
79
|
+
return cmd + args
|
|
80
|
+
|
|
81
|
+
case "add":
|
|
82
|
+
if not args:
|
|
83
|
+
raise ValueError("add requires explicit file paths")
|
|
84
|
+
blocked = {"-A", "--all", "."}
|
|
85
|
+
for a in args:
|
|
86
|
+
if a in blocked:
|
|
87
|
+
raise ValueError("Use explicit file paths instead of '-A', '--all', or '.'")
|
|
88
|
+
return ["git", "add"] + args
|
|
89
|
+
|
|
90
|
+
case "commit":
|
|
91
|
+
if not message:
|
|
92
|
+
raise ValueError("commit requires a message")
|
|
93
|
+
cmd = ["git", "commit", "-m", message]
|
|
94
|
+
if flags.get("no_verify"):
|
|
95
|
+
cmd.append("--no-verify")
|
|
96
|
+
if flags.get("amend"):
|
|
97
|
+
cmd.append("--amend")
|
|
98
|
+
return cmd
|
|
99
|
+
|
|
100
|
+
case "diff":
|
|
101
|
+
cmd = ["git", "diff"]
|
|
102
|
+
if flags.get("staged") or flags.get("cached"):
|
|
103
|
+
cmd.append("--staged")
|
|
104
|
+
return cmd + args
|
|
105
|
+
|
|
106
|
+
case "log":
|
|
107
|
+
max_count = flags.get("max_count", 20)
|
|
108
|
+
cmd = ["git", "log", "--oneline", f"-n{max_count}"]
|
|
109
|
+
if flags.get("format"):
|
|
110
|
+
cmd = ["git", "log", f"--format={flags['format']}", f"-n{max_count}"]
|
|
111
|
+
return cmd + args
|
|
112
|
+
|
|
113
|
+
case "branch":
|
|
114
|
+
if flags.get("delete") and args:
|
|
115
|
+
return ["git", "branch", "-d", args[0]]
|
|
116
|
+
if flags.get("list") or not args:
|
|
117
|
+
cmd = ["git", "branch"]
|
|
118
|
+
if flags.get("all"):
|
|
119
|
+
cmd.append("-a")
|
|
120
|
+
return cmd
|
|
121
|
+
return ["git", "branch", args[0]]
|
|
122
|
+
|
|
123
|
+
case "checkout":
|
|
124
|
+
if not args:
|
|
125
|
+
raise ValueError("checkout requires a branch or file path")
|
|
126
|
+
cmd = ["git", "checkout"]
|
|
127
|
+
if flags.get("create"):
|
|
128
|
+
cmd.append("-b")
|
|
129
|
+
return cmd + args
|
|
130
|
+
|
|
131
|
+
case "stash":
|
|
132
|
+
sub = args[0] if args else "push"
|
|
133
|
+
valid = {"push", "pop", "list", "drop", "apply"}
|
|
134
|
+
if sub not in valid:
|
|
135
|
+
raise ValueError(f"Invalid stash subcommand: {sub}. Valid: {', '.join(sorted(valid))}")
|
|
136
|
+
cmd = ["git", "stash", sub]
|
|
137
|
+
return cmd + args[1:]
|
|
138
|
+
|
|
139
|
+
case "reset":
|
|
140
|
+
cmd = ["git", "reset"]
|
|
141
|
+
if flags.get("hard"):
|
|
142
|
+
cmd.append("--hard")
|
|
143
|
+
elif flags.get("soft"):
|
|
144
|
+
cmd.append("--soft")
|
|
145
|
+
elif flags.get("mixed"):
|
|
146
|
+
cmd.append("--mixed")
|
|
147
|
+
return cmd + args
|
|
148
|
+
|
|
149
|
+
case "tag":
|
|
150
|
+
if flags.get("list") or (flags.get("delete") is None and not args):
|
|
151
|
+
return ["git", "tag", "--list"]
|
|
152
|
+
if flags.get("delete") and args:
|
|
153
|
+
return ["git", "tag", "-d", args[0]]
|
|
154
|
+
if not args:
|
|
155
|
+
raise ValueError("tag requires a tag name")
|
|
156
|
+
cmd = ["git", "tag"]
|
|
157
|
+
if flags.get("message"):
|
|
158
|
+
cmd.extend(["-a", args[0], "-m", flags["message"]])
|
|
159
|
+
else:
|
|
160
|
+
cmd.append(args[0])
|
|
161
|
+
return cmd
|
|
162
|
+
|
|
163
|
+
case _:
|
|
164
|
+
raise ValueError(f"Unknown action: {action}")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def execute(params: dict, project_path: str) -> dict:
|
|
168
|
+
project = Path(project_path).resolve()
|
|
169
|
+
timeout = params.get("timeout", DEFAULT_TIMEOUT)
|
|
170
|
+
working_dir = params.get("working_dir")
|
|
171
|
+
|
|
172
|
+
if working_dir:
|
|
173
|
+
work_path = Path(working_dir)
|
|
174
|
+
if not work_path.is_absolute():
|
|
175
|
+
work_path = project / work_path
|
|
176
|
+
work_path = work_path.resolve()
|
|
177
|
+
|
|
178
|
+
if not work_path.is_relative_to(project):
|
|
179
|
+
return {
|
|
180
|
+
"success": False,
|
|
181
|
+
"error": "Working directory is outside the project workspace",
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if not work_path.exists():
|
|
185
|
+
return {
|
|
186
|
+
"success": False,
|
|
187
|
+
"error": f"Working directory not found: {work_path}",
|
|
188
|
+
}
|
|
189
|
+
else:
|
|
190
|
+
work_path = project
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
cmd = build_command(params)
|
|
194
|
+
except ValueError as e:
|
|
195
|
+
return {"success": False, "error": str(e)}
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
result = subprocess.run(
|
|
199
|
+
cmd,
|
|
200
|
+
capture_output=True,
|
|
201
|
+
text=True,
|
|
202
|
+
cwd=str(work_path),
|
|
203
|
+
timeout=timeout,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
stdout = result.stdout or ""
|
|
207
|
+
stderr = result.stderr or ""
|
|
208
|
+
|
|
209
|
+
stdout, stdout_truncated = truncate_output(stdout, MAX_OUTPUT_BYTES)
|
|
210
|
+
stderr, stderr_truncated = truncate_output(stderr, MAX_OUTPUT_BYTES)
|
|
211
|
+
|
|
212
|
+
success = result.returncode == 0
|
|
213
|
+
|
|
214
|
+
output_parts = []
|
|
215
|
+
if stdout:
|
|
216
|
+
output_parts.append(stdout)
|
|
217
|
+
if stderr:
|
|
218
|
+
output_parts.append(f"[stderr]\n{stderr}")
|
|
219
|
+
|
|
220
|
+
combined_output = "\n".join(output_parts)
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
"success": success,
|
|
224
|
+
"output": combined_output,
|
|
225
|
+
"stdout": stdout,
|
|
226
|
+
"stderr": stderr,
|
|
227
|
+
"exit_code": result.returncode,
|
|
228
|
+
"truncated": stdout_truncated or stderr_truncated,
|
|
229
|
+
}
|
|
230
|
+
except subprocess.TimeoutExpired:
|
|
231
|
+
return {
|
|
232
|
+
"success": False,
|
|
233
|
+
"error": f"Command timed out after {timeout} seconds",
|
|
234
|
+
"timeout": timeout,
|
|
235
|
+
}
|
|
236
|
+
except Exception as e:
|
|
237
|
+
return {"success": False, "error": str(e)}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
if __name__ == "__main__":
|
|
241
|
+
parser = argparse.ArgumentParser()
|
|
242
|
+
parser.add_argument("--params", required=True)
|
|
243
|
+
parser.add_argument("--project-path", required=True)
|
|
244
|
+
args = parser.parse_args()
|
|
245
|
+
result = execute(json.loads(args.params), args.project_path)
|
|
246
|
+
print(json.dumps(result))
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
// rye:signed:2026-02-25T00:02:14Z:86f089cf310d3ff22be3f65d2768cea1e3c568955b4276605c3911d67bdd8b9e:U6Mjpa8FTWO519-6nqdILwMJIlNu_D6gKS2Uu3AgJmS4mWgI_DWOH-jtNWW8A_WSpRJbXqRiH88cA9CVaeXrAQ==:9fbfabe975fa5a7f
|
|
2
|
+
// rye:unsigned
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { resolve, isAbsolute, extname, relative } from "node:path";
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
8
|
+
import { execSync } from "node:child_process";
|
|
9
|
+
import {
|
|
10
|
+
createMessageConnection,
|
|
11
|
+
StreamMessageReader,
|
|
12
|
+
StreamMessageWriter,
|
|
13
|
+
} from "vscode-jsonrpc/node.js";
|
|
14
|
+
|
|
15
|
+
export const __version__ = "1.0.0";
|
|
16
|
+
export const __tool_type__ = "javascript";
|
|
17
|
+
export const __executor_id__ = "rye/core/runtimes/node/node";
|
|
18
|
+
export const __category__ = "rye/code/lsp";
|
|
19
|
+
export const __tool_description__ =
|
|
20
|
+
"LSP client — go to definition, find references, hover, document symbols, and more via language servers";
|
|
21
|
+
|
|
22
|
+
const OPERATIONS = [
|
|
23
|
+
"goToDefinition",
|
|
24
|
+
"findReferences",
|
|
25
|
+
"hover",
|
|
26
|
+
"documentSymbol",
|
|
27
|
+
"workspaceSymbol",
|
|
28
|
+
"goToImplementation",
|
|
29
|
+
"prepareCallHierarchy",
|
|
30
|
+
"incomingCalls",
|
|
31
|
+
"outgoingCalls",
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
export const CONFIG_SCHEMA = {
|
|
35
|
+
type: "object",
|
|
36
|
+
properties: {
|
|
37
|
+
operation: {
|
|
38
|
+
type: "string",
|
|
39
|
+
enum: [...OPERATIONS],
|
|
40
|
+
description: "LSP operation to perform",
|
|
41
|
+
},
|
|
42
|
+
file_path: {
|
|
43
|
+
type: "string",
|
|
44
|
+
description: "Path to the file",
|
|
45
|
+
},
|
|
46
|
+
line: {
|
|
47
|
+
type: "integer",
|
|
48
|
+
minimum: 1,
|
|
49
|
+
description: "Line number (1-based)",
|
|
50
|
+
},
|
|
51
|
+
character: {
|
|
52
|
+
type: "integer",
|
|
53
|
+
minimum: 1,
|
|
54
|
+
description: "Character offset (1-based)",
|
|
55
|
+
},
|
|
56
|
+
timeout: {
|
|
57
|
+
type: "integer",
|
|
58
|
+
default: 15,
|
|
59
|
+
description: "Timeout in seconds",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
required: ["operation", "file_path", "line", "character"],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const DEFAULT_TIMEOUT = 15;
|
|
66
|
+
|
|
67
|
+
const LANGUAGE_EXTENSIONS: Record<string, string> = {
|
|
68
|
+
".py": "python",
|
|
69
|
+
".js": "javascript",
|
|
70
|
+
".jsx": "javascriptreact",
|
|
71
|
+
".ts": "typescript",
|
|
72
|
+
".tsx": "typescriptreact",
|
|
73
|
+
".mjs": "javascript",
|
|
74
|
+
".cjs": "javascript",
|
|
75
|
+
".mts": "typescript",
|
|
76
|
+
".cts": "typescript",
|
|
77
|
+
".go": "go",
|
|
78
|
+
".rs": "rust",
|
|
79
|
+
".rb": "ruby",
|
|
80
|
+
".java": "java",
|
|
81
|
+
".c": "c",
|
|
82
|
+
".cpp": "cpp",
|
|
83
|
+
".h": "c",
|
|
84
|
+
".hpp": "cpp",
|
|
85
|
+
".vue": "vue",
|
|
86
|
+
".svelte": "svelte",
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
interface ServerConfig {
|
|
90
|
+
id: string;
|
|
91
|
+
extensions: string[];
|
|
92
|
+
command: string[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const SERVERS: ServerConfig[] = [
|
|
96
|
+
{
|
|
97
|
+
id: "typescript",
|
|
98
|
+
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
|
|
99
|
+
command: ["typescript-language-server", "--stdio"],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: "pyright",
|
|
103
|
+
extensions: [".py"],
|
|
104
|
+
command: ["pyright-langserver", "--stdio"],
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: "gopls",
|
|
108
|
+
extensions: [".go"],
|
|
109
|
+
command: ["gopls", "serve"],
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "rust-analyzer",
|
|
113
|
+
extensions: [".rs"],
|
|
114
|
+
command: ["rust-analyzer"],
|
|
115
|
+
},
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
interface Params {
|
|
119
|
+
operation: (typeof OPERATIONS)[number];
|
|
120
|
+
file_path: string;
|
|
121
|
+
line: number;
|
|
122
|
+
character: number;
|
|
123
|
+
timeout?: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface Result {
|
|
127
|
+
success: boolean;
|
|
128
|
+
output?: string;
|
|
129
|
+
error?: string;
|
|
130
|
+
operation?: string;
|
|
131
|
+
server?: string;
|
|
132
|
+
results?: unknown[];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function findServer(filePath: string): ServerConfig | null {
|
|
136
|
+
const ext = extname(filePath).toLowerCase();
|
|
137
|
+
for (const server of SERVERS) {
|
|
138
|
+
if (!server.extensions.includes(ext)) continue;
|
|
139
|
+
try {
|
|
140
|
+
execSync(`which ${server.command[0]}`, {
|
|
141
|
+
encoding: "utf-8",
|
|
142
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
143
|
+
});
|
|
144
|
+
return server;
|
|
145
|
+
} catch {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
|
153
|
+
return new Promise((resolve, reject) => {
|
|
154
|
+
const timer = setTimeout(
|
|
155
|
+
() => reject(new Error(`Timed out after ${ms}ms`)),
|
|
156
|
+
ms,
|
|
157
|
+
);
|
|
158
|
+
promise.then(
|
|
159
|
+
(v) => {
|
|
160
|
+
clearTimeout(timer);
|
|
161
|
+
resolve(v);
|
|
162
|
+
},
|
|
163
|
+
(e) => {
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
reject(e);
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function execute(params: Params, projectPath: string): Promise<Result> {
|
|
172
|
+
const project = resolve(projectPath);
|
|
173
|
+
const operation = params.operation;
|
|
174
|
+
const timeout = (params.timeout ?? DEFAULT_TIMEOUT) * 1000;
|
|
175
|
+
|
|
176
|
+
let filePath = params.file_path;
|
|
177
|
+
if (!isAbsolute(filePath)) filePath = resolve(project, filePath);
|
|
178
|
+
|
|
179
|
+
if (!existsSync(filePath)) {
|
|
180
|
+
return { success: false, error: `File not found: ${filePath}` };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const server = findServer(filePath);
|
|
184
|
+
if (!server) {
|
|
185
|
+
return {
|
|
186
|
+
success: false,
|
|
187
|
+
error: `No LSP server available for ${extname(filePath)} files`,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const proc = spawn(server.command[0], server.command.slice(1), {
|
|
192
|
+
cwd: project,
|
|
193
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const connection = createMessageConnection(
|
|
197
|
+
new StreamMessageReader(proc.stdout),
|
|
198
|
+
new StreamMessageWriter(proc.stdin),
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// Suppress unhandled notifications
|
|
202
|
+
connection.onRequest("window/workDoneProgress/create", () => null);
|
|
203
|
+
connection.onRequest("workspace/configuration", () => [{}]);
|
|
204
|
+
connection.onRequest("client/registerCapability", () => {});
|
|
205
|
+
connection.onRequest("client/unregisterCapability", () => {});
|
|
206
|
+
connection.onRequest("workspace/workspaceFolders", () => [
|
|
207
|
+
{ name: "workspace", uri: pathToFileURL(project).href },
|
|
208
|
+
]);
|
|
209
|
+
|
|
210
|
+
connection.listen();
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
// Initialize
|
|
214
|
+
await withTimeout(
|
|
215
|
+
connection.sendRequest("initialize", {
|
|
216
|
+
rootUri: pathToFileURL(project).href,
|
|
217
|
+
processId: proc.pid,
|
|
218
|
+
workspaceFolders: [
|
|
219
|
+
{ name: "workspace", uri: pathToFileURL(project).href },
|
|
220
|
+
],
|
|
221
|
+
capabilities: {
|
|
222
|
+
workspace: {
|
|
223
|
+
configuration: true,
|
|
224
|
+
didChangeWatchedFiles: { dynamicRegistration: true },
|
|
225
|
+
},
|
|
226
|
+
textDocument: {
|
|
227
|
+
synchronization: { didOpen: true, didChange: true },
|
|
228
|
+
publishDiagnostics: { versionSupport: true },
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
}),
|
|
232
|
+
timeout,
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
await connection.sendNotification("initialized", {});
|
|
236
|
+
|
|
237
|
+
// Open file
|
|
238
|
+
const ext = extname(filePath);
|
|
239
|
+
const languageId = LANGUAGE_EXTENSIONS[ext] ?? "plaintext";
|
|
240
|
+
const text = readFileSync(filePath, "utf-8");
|
|
241
|
+
const uri = pathToFileURL(filePath).href;
|
|
242
|
+
|
|
243
|
+
await connection.sendNotification("textDocument/didOpen", {
|
|
244
|
+
textDocument: { uri, languageId, version: 0, text },
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Brief delay for server to process
|
|
248
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
249
|
+
|
|
250
|
+
const position = {
|
|
251
|
+
line: params.line - 1,
|
|
252
|
+
character: params.character - 1,
|
|
253
|
+
};
|
|
254
|
+
const textDocument = { uri };
|
|
255
|
+
|
|
256
|
+
// Execute operation
|
|
257
|
+
let results: unknown[];
|
|
258
|
+
switch (operation) {
|
|
259
|
+
case "goToDefinition":
|
|
260
|
+
results = await withTimeout(
|
|
261
|
+
connection
|
|
262
|
+
.sendRequest("textDocument/definition", { textDocument, position })
|
|
263
|
+
.then(normalize),
|
|
264
|
+
timeout,
|
|
265
|
+
);
|
|
266
|
+
break;
|
|
267
|
+
case "findReferences":
|
|
268
|
+
results = await withTimeout(
|
|
269
|
+
connection
|
|
270
|
+
.sendRequest("textDocument/references", {
|
|
271
|
+
textDocument,
|
|
272
|
+
position,
|
|
273
|
+
context: { includeDeclaration: true },
|
|
274
|
+
})
|
|
275
|
+
.then(normalize),
|
|
276
|
+
timeout,
|
|
277
|
+
);
|
|
278
|
+
break;
|
|
279
|
+
case "hover":
|
|
280
|
+
results = await withTimeout(
|
|
281
|
+
connection
|
|
282
|
+
.sendRequest("textDocument/hover", { textDocument, position })
|
|
283
|
+
.then((r) => (r ? [r] : [])),
|
|
284
|
+
timeout,
|
|
285
|
+
);
|
|
286
|
+
break;
|
|
287
|
+
case "documentSymbol":
|
|
288
|
+
results = await withTimeout(
|
|
289
|
+
connection
|
|
290
|
+
.sendRequest("textDocument/documentSymbol", { textDocument })
|
|
291
|
+
.then(normalize),
|
|
292
|
+
timeout,
|
|
293
|
+
);
|
|
294
|
+
break;
|
|
295
|
+
case "workspaceSymbol":
|
|
296
|
+
results = await withTimeout(
|
|
297
|
+
connection
|
|
298
|
+
.sendRequest("workspace/symbol", { query: "" })
|
|
299
|
+
.then(normalize)
|
|
300
|
+
.then((r: any[]) => r.slice(0, 20)),
|
|
301
|
+
timeout,
|
|
302
|
+
);
|
|
303
|
+
break;
|
|
304
|
+
case "goToImplementation":
|
|
305
|
+
results = await withTimeout(
|
|
306
|
+
connection
|
|
307
|
+
.sendRequest("textDocument/implementation", {
|
|
308
|
+
textDocument,
|
|
309
|
+
position,
|
|
310
|
+
})
|
|
311
|
+
.then(normalize),
|
|
312
|
+
timeout,
|
|
313
|
+
);
|
|
314
|
+
break;
|
|
315
|
+
case "prepareCallHierarchy":
|
|
316
|
+
results = await withTimeout(
|
|
317
|
+
connection
|
|
318
|
+
.sendRequest("textDocument/prepareCallHierarchy", {
|
|
319
|
+
textDocument,
|
|
320
|
+
position,
|
|
321
|
+
})
|
|
322
|
+
.then(normalize),
|
|
323
|
+
timeout,
|
|
324
|
+
);
|
|
325
|
+
break;
|
|
326
|
+
case "incomingCalls": {
|
|
327
|
+
const items: any[] = await withTimeout(
|
|
328
|
+
connection
|
|
329
|
+
.sendRequest("textDocument/prepareCallHierarchy", {
|
|
330
|
+
textDocument,
|
|
331
|
+
position,
|
|
332
|
+
})
|
|
333
|
+
.catch(() => []),
|
|
334
|
+
timeout,
|
|
335
|
+
);
|
|
336
|
+
if (!items?.length) {
|
|
337
|
+
results = [];
|
|
338
|
+
} else {
|
|
339
|
+
results = await withTimeout(
|
|
340
|
+
connection
|
|
341
|
+
.sendRequest("callHierarchy/incomingCalls", { item: items[0] })
|
|
342
|
+
.then(normalize),
|
|
343
|
+
timeout,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
case "outgoingCalls": {
|
|
349
|
+
const items2: any[] = await withTimeout(
|
|
350
|
+
connection
|
|
351
|
+
.sendRequest("textDocument/prepareCallHierarchy", {
|
|
352
|
+
textDocument,
|
|
353
|
+
position,
|
|
354
|
+
})
|
|
355
|
+
.catch(() => []),
|
|
356
|
+
timeout,
|
|
357
|
+
);
|
|
358
|
+
if (!items2?.length) {
|
|
359
|
+
results = [];
|
|
360
|
+
} else {
|
|
361
|
+
results = await withTimeout(
|
|
362
|
+
connection
|
|
363
|
+
.sendRequest("callHierarchy/outgoingCalls", { item: items2[0] })
|
|
364
|
+
.then(normalize),
|
|
365
|
+
timeout,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
default:
|
|
371
|
+
return { success: false, error: `Unknown operation: ${operation}` };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Convert file URIs to relative paths
|
|
375
|
+
const cleaned = JSON.parse(
|
|
376
|
+
JSON.stringify(results, (key, value) => {
|
|
377
|
+
if (
|
|
378
|
+
key === "uri" &&
|
|
379
|
+
typeof value === "string" &&
|
|
380
|
+
value.startsWith("file://")
|
|
381
|
+
) {
|
|
382
|
+
try {
|
|
383
|
+
return relative(project, fileURLToPath(value));
|
|
384
|
+
} catch {
|
|
385
|
+
return value;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return value;
|
|
389
|
+
}),
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
const output =
|
|
393
|
+
cleaned.length === 0
|
|
394
|
+
? `No results found for ${operation}`
|
|
395
|
+
: JSON.stringify(cleaned, null, 2);
|
|
396
|
+
|
|
397
|
+
const relPath = relative(project, filePath);
|
|
398
|
+
return {
|
|
399
|
+
success: true,
|
|
400
|
+
output,
|
|
401
|
+
operation,
|
|
402
|
+
server: server.id,
|
|
403
|
+
results: cleaned,
|
|
404
|
+
};
|
|
405
|
+
} catch (e: any) {
|
|
406
|
+
return {
|
|
407
|
+
success: false,
|
|
408
|
+
error: e.message ?? String(e),
|
|
409
|
+
operation,
|
|
410
|
+
server: server.id,
|
|
411
|
+
};
|
|
412
|
+
} finally {
|
|
413
|
+
connection.end();
|
|
414
|
+
connection.dispose();
|
|
415
|
+
proc.kill();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function normalize(result: unknown): unknown[] {
|
|
420
|
+
if (!result) return [];
|
|
421
|
+
return Array.isArray(result) ? result.filter(Boolean) : [result];
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// CLI entry point
|
|
425
|
+
const { values } = parseArgs({
|
|
426
|
+
options: {
|
|
427
|
+
params: { type: "string" },
|
|
428
|
+
"project-path": { type: "string" },
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
if (values.params && values["project-path"]) {
|
|
433
|
+
const result = await execute(
|
|
434
|
+
JSON.parse(values.params) as Params,
|
|
435
|
+
values["project-path"],
|
|
436
|
+
);
|
|
437
|
+
console.log(JSON.stringify(result));
|
|
438
|
+
}
|