amxx-builder 1.5.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/AGENTS.md +111 -0
- package/README.md +485 -0
- package/action-entry.js +42 -0
- package/action.yml +55 -0
- package/defaults/amxbuild.defaults.yml +40 -0
- package/index.js +4 -0
- package/mcp/dep-resolver.js +75 -0
- package/mcp/handlers.js +862 -0
- package/mcp/mcp-server.js +112 -0
- package/mcp/registry.js +863 -0
- package/mcp/symbol-index.js +171 -0
- package/package.json +68 -0
- package/src/archiver.js +140 -0
- package/src/asset-fetcher.js +277 -0
- package/src/build-plan.js +101 -0
- package/src/build-service.js +188 -0
- package/src/cache-dir.js +21 -0
- package/src/cache-info.js +104 -0
- package/src/cli.js +302 -0
- package/src/collector.js +89 -0
- package/src/commands/build.js +49 -0
- package/src/commands/cache.js +77 -0
- package/src/commands/clean.js +33 -0
- package/src/commands/compile-renderer.js +38 -0
- package/src/commands/deploy.js +40 -0
- package/src/commands/deps-tree.js +92 -0
- package/src/commands/doctor.js +77 -0
- package/src/commands/dry-run.js +64 -0
- package/src/commands/init.js +228 -0
- package/src/commands/mcp.js +13 -0
- package/src/commands/releases.js +45 -0
- package/src/commands/resolve-manifest.js +27 -0
- package/src/commands/serve.js +489 -0
- package/src/commands/shared.js +24 -0
- package/src/commands/validate.js +34 -0
- package/src/commands/watch.js +209 -0
- package/src/compile-utils.js +65 -0
- package/src/compiler-fetcher.js +327 -0
- package/src/compiler.js +228 -0
- package/src/dep-graph.js +92 -0
- package/src/deployer.js +197 -0
- package/src/deps-resolver.js +127 -0
- package/src/deps-tree.js +202 -0
- package/src/env.js +18 -0
- package/src/events.js +29 -0
- package/src/format.js +23 -0
- package/src/fs-utils.js +87 -0
- package/src/include-tree.js +845 -0
- package/src/ini-builder.js +44 -0
- package/src/jsonrpc-transport.js +195 -0
- package/src/logger.js +50 -0
- package/src/manifest-path.js +34 -0
- package/src/manifest.js +373 -0
- package/src/progress.js +66 -0
- package/src/rcon.js +103 -0
- package/src/release-fetcher.js +206 -0
- package/src/release-lister.js +79 -0
- package/src/repo-fetcher.js +273 -0
- package/src/retry.js +50 -0
- package/src/schema.js +54 -0
- package/src/update-check.js +114 -0
- package/src/validate.js +69 -0
- package/src/watcher.js +135 -0
- package/templates/init-build.bat +11 -0
- package/templates/init-build.sh +7 -0
- package/templates/init-deploy.env +14 -0
- package/templates/init-manifest.yml +6 -0
- package/templates/init-workflow.yml +59 -0
package/mcp/handlers.js
ADDED
|
@@ -0,0 +1,862 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const glob = require('fast-glob');
|
|
7
|
+
|
|
8
|
+
const { fetchRepo, resolveRefIfLatest } = require('../src/repo-fetcher');
|
|
9
|
+
const { fetchReleaseDep } = require('../src/release-fetcher');
|
|
10
|
+
const { fetchCompiler, resolveAmxmodxVersion: resolveAmxmodxVersionCore } = require('../src/compiler-fetcher');
|
|
11
|
+
const { resolveManifest, resolveGithubToken, parseDepString, parseDepObject } = require('../src/manifest');
|
|
12
|
+
const { parseManifest } = require('../src/manifest');
|
|
13
|
+
const { validateManifestFile } = require('../src/validate');
|
|
14
|
+
const { getManifestSchema } = require('../src/schema');
|
|
15
|
+
const { getCacheInfo } = require('../src/cache-info');
|
|
16
|
+
const { buildDepTree, assembleRootDeps } = require('../src/deps-tree');
|
|
17
|
+
const { buildIncludeTree, fetchDepIncludeDir, parseIncludeDirective, searchIncludeFile, collectIncFiles } = require('../src/include-tree');
|
|
18
|
+
const { listReleases, listTags } = require('../src/release-lister');
|
|
19
|
+
const { buildPlanData } = require('../src/build-plan');
|
|
20
|
+
const { spawnCompiler, buildIncludeArgs, buildDefineArgs } = require('../src/compile-utils');
|
|
21
|
+
const { buildIndex, searchIndex } = require('./symbol-index');
|
|
22
|
+
const { loadEnv } = require('../src/env');
|
|
23
|
+
const { resolveManifestPath } = require('../src/manifest-path');
|
|
24
|
+
const { formatBytes } = require('../src/format');
|
|
25
|
+
const logger = require('../src/logger');
|
|
26
|
+
|
|
27
|
+
// ─── Response formatters ───────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
function textResult(text) {
|
|
30
|
+
return {
|
|
31
|
+
content: [{ type: 'text', text }],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function errorResult(message, code = -32603) {
|
|
36
|
+
return {
|
|
37
|
+
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
38
|
+
isError: true,
|
|
39
|
+
_meta: code ? { code } : undefined,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Fallback token when no manifest is in scope — plain `token || GITHUB_TOKEN`.
|
|
44
|
+
function fallbackToken(token) {
|
|
45
|
+
return token || process.env.GITHUB_TOKEN || null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── Output limits ─────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
const DEFAULT_MAX_OUTPUT_BYTES = 200 * 1024; // 200 KB
|
|
51
|
+
const DEFAULT_MAX_FILES = 50;
|
|
52
|
+
|
|
53
|
+
function applyOutputLimit(text, args, maxBytes = DEFAULT_MAX_OUTPUT_BYTES) {
|
|
54
|
+
if (args?.full_output) return text;
|
|
55
|
+
const size = Buffer.byteLength(text, 'utf8');
|
|
56
|
+
if (size <= maxBytes) return text;
|
|
57
|
+
const buf = Buffer.from(text, 'utf8').subarray(0, maxBytes);
|
|
58
|
+
// Walk back past any UTF-8 continuation bytes so we never split a character.
|
|
59
|
+
let cutLen = buf.length;
|
|
60
|
+
while (cutLen > 0 && (buf[cutLen - 1] & 0xc0) === 0x80) cutLen--;
|
|
61
|
+
const cut = buf.subarray(0, cutLen).toString('utf8');
|
|
62
|
+
return (
|
|
63
|
+
cut +
|
|
64
|
+
`\n… [truncated ${formatBytes(size)} → ${formatBytes(maxBytes)}; ` +
|
|
65
|
+
`pass full_output=true for the complete output]`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function limitFiles(files, args) {
|
|
70
|
+
if (args?.full_output || files.length <= DEFAULT_MAX_FILES) return files;
|
|
71
|
+
return files.slice(0, DEFAULT_MAX_FILES);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── Dep parsing helpers ───────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
function parseDep(raw) {
|
|
77
|
+
if (typeof raw === 'string') return parseDepString(raw);
|
|
78
|
+
if (raw && typeof raw === 'object') return parseDepObject(raw);
|
|
79
|
+
throw new Error('Dep must be a string or an object');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function resolveDepRef(dep, token) {
|
|
83
|
+
return resolveRefIfLatest(dep.ref, dep.repo, token);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function readFileSafe(absPath) {
|
|
87
|
+
try {
|
|
88
|
+
const buf = fs.readFileSync(absPath);
|
|
89
|
+
try {
|
|
90
|
+
const text = buf.toString('utf8');
|
|
91
|
+
if (text.includes('\u0000')) {
|
|
92
|
+
return `[binary file, ${buf.length} bytes]`;
|
|
93
|
+
}
|
|
94
|
+
return text;
|
|
95
|
+
} catch (_) {
|
|
96
|
+
return `[binary file, ${buf.length} bytes]`;
|
|
97
|
+
}
|
|
98
|
+
} catch (err) {
|
|
99
|
+
return `[error reading file: ${err.message}]`;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Grep content with configurable before/after context lines.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} content - File content to search in.
|
|
107
|
+
* @param {string} pattern - Substring to match (case-insensitive).
|
|
108
|
+
* @param {number} [before=0] - Lines of context before each match.
|
|
109
|
+
* @param {number} [after=0] - Lines of context after each match.
|
|
110
|
+
* @returns {string} Formatted grep result or "No matches found." message.
|
|
111
|
+
*/
|
|
112
|
+
function grepContent(content, pattern, before = 0, after = 0) {
|
|
113
|
+
if (!pattern) return content;
|
|
114
|
+
const lines = content.split('\n');
|
|
115
|
+
const matches = [];
|
|
116
|
+
|
|
117
|
+
for (let i = 0; i < lines.length; i++) {
|
|
118
|
+
if (lines[i].toLowerCase().includes(pattern.toLowerCase())) {
|
|
119
|
+
const start = Math.max(0, i - before);
|
|
120
|
+
const end = Math.min(lines.length - 1, i + after);
|
|
121
|
+
matches.push({ matchLine: i, start, end });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (matches.length === 0) return `[grep: no matches for "${pattern}"]`;
|
|
126
|
+
|
|
127
|
+
// Merge overlapping ranges
|
|
128
|
+
const merged = [];
|
|
129
|
+
for (const m of matches) {
|
|
130
|
+
if (merged.length > 0 && m.start <= merged[merged.length - 1].end + 1) {
|
|
131
|
+
merged[merged.length - 1].end = Math.max(merged[merged.length - 1].end, m.end);
|
|
132
|
+
} else {
|
|
133
|
+
merged.push({ ...m });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const parts = merged.map((range, ri) => {
|
|
138
|
+
const chunk = [];
|
|
139
|
+
if (ri > 0) chunk.push('┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄');
|
|
140
|
+
for (let ln = range.start; ln <= range.end; ln++) {
|
|
141
|
+
const marker = ln === range.matchLine ? '>' : ' ';
|
|
142
|
+
chunk.push(`${marker} ${String(ln + 1).padStart(4, ' ')} │ ${lines[ln]}`);
|
|
143
|
+
}
|
|
144
|
+
return chunk.join('\n');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return parts.join('\n');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ─── Tool handlers ─────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
async function handleGetDepInterface(args, token, noFetch) {
|
|
153
|
+
token = fallbackToken(token);
|
|
154
|
+
let dep;
|
|
155
|
+
try {
|
|
156
|
+
dep = parseDep(args?.dep || args);
|
|
157
|
+
} catch (parseErr) {
|
|
158
|
+
return errorResult(parseErr.message);
|
|
159
|
+
}
|
|
160
|
+
if (args?.source) dep.source = args.source;
|
|
161
|
+
if (args?.include_path) dep.include_path = args.include_path;
|
|
162
|
+
if (args?.asset != null) dep.asset = args.asset;
|
|
163
|
+
|
|
164
|
+
const resolvedRef = await resolveDepRef(dep, token);
|
|
165
|
+
const srcDir = await fetchDepIncludeDir(dep, token, noFetch);
|
|
166
|
+
const incFiles = await collectIncFiles(srcDir);
|
|
167
|
+
|
|
168
|
+
if (incFiles.length === 0) {
|
|
169
|
+
return textResult(
|
|
170
|
+
`Dependency ${dep.repo}@${resolvedRef} has no .inc files in its include path.`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const grep = args?.grep;
|
|
175
|
+
const before = args?.before || 0;
|
|
176
|
+
const after = args?.after || 0;
|
|
177
|
+
|
|
178
|
+
const files = incFiles.map((f) => ({
|
|
179
|
+
path: f.rel,
|
|
180
|
+
content: grep ? grepContent(readFileSafe(f.abs), grep, before, after) : readFileSafe(f.abs),
|
|
181
|
+
}));
|
|
182
|
+
|
|
183
|
+
const shown = limitFiles(files, args);
|
|
184
|
+
const skipped = files.length - shown.length;
|
|
185
|
+
let out =
|
|
186
|
+
`Found ${files.length} .inc file(s) in ${dep.repo}@${resolvedRef}:\n\n` +
|
|
187
|
+
shown
|
|
188
|
+
.map(
|
|
189
|
+
(f) =>
|
|
190
|
+
`──── ${f.path} ────\n${f.content}${f.content.endsWith('\n') ? '' : '\n'}`
|
|
191
|
+
)
|
|
192
|
+
.join('\n');
|
|
193
|
+
if (skipped > 0) out += `\n… [${skipped} more file(s); pass full_output=true to list them]`;
|
|
194
|
+
return textResult(applyOutputLimit(out, args));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function handleListDepIncs(args, token, noFetch) {
|
|
198
|
+
token = fallbackToken(token);
|
|
199
|
+
let dep;
|
|
200
|
+
try {
|
|
201
|
+
dep = parseDep(args?.dep || args);
|
|
202
|
+
} catch (parseErr) {
|
|
203
|
+
return errorResult(parseErr.message);
|
|
204
|
+
}
|
|
205
|
+
if (args?.source) dep.source = args.source;
|
|
206
|
+
if (args?.include_path) dep.include_path = args.include_path;
|
|
207
|
+
if (args?.asset != null) dep.asset = args.asset;
|
|
208
|
+
|
|
209
|
+
const resolvedRef = await resolveDepRef(dep, token);
|
|
210
|
+
const srcDir = await fetchDepIncludeDir(dep, token, noFetch);
|
|
211
|
+
const incFiles = await collectIncFiles(srcDir);
|
|
212
|
+
|
|
213
|
+
if (incFiles.length === 0) {
|
|
214
|
+
return textResult(
|
|
215
|
+
`Dependency ${dep.repo}@${resolvedRef} has no .inc files in its include path.`
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const listing = incFiles.map((f) => ` ${f.rel}`).join('\n');
|
|
220
|
+
|
|
221
|
+
return textResult(
|
|
222
|
+
applyOutputLimit(`Dependency ${dep.repo}@${resolvedRef} — ${incFiles.length} .inc file(s):\n\n${listing}`, args)
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function handleGetDepTree(args, token, noFetch) {
|
|
227
|
+
const depth = args?.depth || 0;
|
|
228
|
+
let rootDeps;
|
|
229
|
+
let getDepsOverride = null;
|
|
230
|
+
let tokenFor = null;
|
|
231
|
+
|
|
232
|
+
if (args?.manifest) {
|
|
233
|
+
const manifest = parseManifest(path.resolve(args.manifest));
|
|
234
|
+
tokenFor = (repo) => resolveGithubToken(manifest, repo);
|
|
235
|
+
const assembled = assembleRootDeps(manifest);
|
|
236
|
+
rootDeps = assembled.rootDeps;
|
|
237
|
+
getDepsOverride = assembled.getDepsOverride;
|
|
238
|
+
} else if (args?.deps) {
|
|
239
|
+
rootDeps = args.deps.map((entry) => {
|
|
240
|
+
if (typeof entry === 'string') {
|
|
241
|
+
const parsed = parseDep(entry);
|
|
242
|
+
return { repo: parsed.repo, ref: parsed.ref, source: parsed.source, include_path: parsed.include_path, asset: parsed.asset };
|
|
243
|
+
}
|
|
244
|
+
return { repo: entry.repo, ref: entry.ref, source: entry.source || 'git', include_path: entry.include_path || null, asset: entry.asset != null ? entry.asset : null };
|
|
245
|
+
});
|
|
246
|
+
} else {
|
|
247
|
+
return errorResult('Provide either "manifest" or "deps"', -32602);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const tree = await buildDepTree(rootDeps, {
|
|
251
|
+
token,
|
|
252
|
+
tokenFor,
|
|
253
|
+
noFetch,
|
|
254
|
+
depth,
|
|
255
|
+
from: args?.manifest ? 'manifest' : 'user',
|
|
256
|
+
getDepsOverride,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
return textResult(applyOutputLimit(JSON.stringify(tree, null, 2), args));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function handleResolveManifestTool(args) {
|
|
263
|
+
const manifestPath = resolveManifestPath(args?.manifest).path;
|
|
264
|
+
const fullPath = path.resolve(manifestPath);
|
|
265
|
+
loadEnv(fullPath);
|
|
266
|
+
|
|
267
|
+
const manifest = resolveManifest(fullPath, {
|
|
268
|
+
set: args?.set,
|
|
269
|
+
define: args?.define,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
return textResult(applyOutputLimit(JSON.stringify(manifest, null, 2), args));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function handleValidateManifestTool(args) {
|
|
276
|
+
const manifestPath = resolveManifestPath(args?.manifest).path;
|
|
277
|
+
const result = validateManifestFile(manifestPath);
|
|
278
|
+
return textResult(applyOutputLimit(JSON.stringify(result, null, 2), args));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function handleGetCacheInfo(args) {
|
|
282
|
+
const manifestPath = args?.manifest ? path.resolve(args.manifest) : undefined;
|
|
283
|
+
const info = getCacheInfo(manifestPath);
|
|
284
|
+
return textResult(applyOutputLimit(JSON.stringify(info, null, 2), args));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function handleListReleasesTool(args, token) {
|
|
288
|
+
if (!args?.repo) return errorResult('Missing required "repo" field', -32602);
|
|
289
|
+
const limit = args?.limit || 10;
|
|
290
|
+
token = fallbackToken(token);
|
|
291
|
+
|
|
292
|
+
let entries;
|
|
293
|
+
if (args?.tags) {
|
|
294
|
+
entries = await listTags(args.repo, { token, limit });
|
|
295
|
+
} else {
|
|
296
|
+
entries = await listReleases(args.repo, { token, limit, includeAssets: args?.includeAssets });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return textResult(applyOutputLimit(JSON.stringify(entries, null, 2), args));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function handleBuildIncludeTree(args, token, noFetch) {
|
|
303
|
+
if (!args?.file) return errorResult('Missing required "file" parameter', -32602);
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
const result = await buildIncludeTree(
|
|
307
|
+
args.manifest || undefined,
|
|
308
|
+
args.file,
|
|
309
|
+
{
|
|
310
|
+
direction: args.direction || 'auto',
|
|
311
|
+
depth: args.depth || 0,
|
|
312
|
+
format: args.format || 'text',
|
|
313
|
+
token,
|
|
314
|
+
noFetch: noFetch || args?.no_fetch === true,
|
|
315
|
+
}
|
|
316
|
+
);
|
|
317
|
+
return textResult(applyOutputLimit(result.text, args));
|
|
318
|
+
} catch (err) {
|
|
319
|
+
return errorResult(err.message);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ─── AMXX standard include helpers ────────────────────────────────────────────
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Resolve the AMX Mod X version to use.
|
|
327
|
+
* Priority: explicit `version` arg → manifest `amxmodx.version` → latest.
|
|
328
|
+
* The priority logic lives in core (compiler-fetcher.resolveAmxmodxVersion);
|
|
329
|
+
* this wrapper only does arg extraction + manifest discovery/parse, keeping
|
|
330
|
+
* the current error-fallback behavior (unparseable manifest → latest).
|
|
331
|
+
*/
|
|
332
|
+
async function resolveAmxmodxVersion(args, noFetch) {
|
|
333
|
+
if (args?.version) return resolveAmxmodxVersionCore(null, { version: args.version });
|
|
334
|
+
|
|
335
|
+
const manifestPathStr = args?.manifest;
|
|
336
|
+
const manifestPath = resolveManifestPath(manifestPathStr || undefined).path;
|
|
337
|
+
let manifest = null;
|
|
338
|
+
if (fs.existsSync(manifestPath)) {
|
|
339
|
+
try {
|
|
340
|
+
manifest = parseManifest(manifestPath);
|
|
341
|
+
} catch (err) {
|
|
342
|
+
logger.warn(`Manifest parse failed (${manifestPath}), falling back to latest: ${err.message}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return resolveAmxmodxVersionCore(manifest, { noFetch });
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function handleListAmxmodxIncs(args, token, noFetch) {
|
|
350
|
+
const version = await resolveAmxmodxVersion(args, noFetch);
|
|
351
|
+
const pattern = args?.pattern || '*.inc';
|
|
352
|
+
|
|
353
|
+
const { includeDir } = await fetchCompiler(version);
|
|
354
|
+
if (!includeDir) {
|
|
355
|
+
return textResult(
|
|
356
|
+
`No standard include directory found for AMX Mod X ${version}.`
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const files = await glob(pattern, { cwd: includeDir, dot: false });
|
|
361
|
+
files.sort();
|
|
362
|
+
|
|
363
|
+
if (files.length === 0) {
|
|
364
|
+
return textResult(
|
|
365
|
+
`No .inc files matching "${pattern}" in AMX Mod X ${version} includes.`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const listing = files.map((f) => ` ${f}`).join('\n');
|
|
370
|
+
return textResult(
|
|
371
|
+
applyOutputLimit(`AMX Mod X ${version} — ${files.length} standard include file(s):\n\n${listing}`, args)
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function handleGetAmxmodxInclude(args, token, noFetch) {
|
|
376
|
+
const version = await resolveAmxmodxVersion(args, noFetch);
|
|
377
|
+
const pattern = args?.file || args?.pattern || '*.inc';
|
|
378
|
+
const grep = args?.grep;
|
|
379
|
+
const before = args?.before || 0;
|
|
380
|
+
const after = args?.after || 0;
|
|
381
|
+
|
|
382
|
+
const { includeDir } = await fetchCompiler(version);
|
|
383
|
+
if (!includeDir) {
|
|
384
|
+
return textResult(
|
|
385
|
+
`No standard include directory found for AMX Mod X ${version}.`
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const files = await glob(pattern, { cwd: includeDir, dot: false });
|
|
390
|
+
files.sort();
|
|
391
|
+
|
|
392
|
+
if (files.length === 0) {
|
|
393
|
+
return textResult(
|
|
394
|
+
`No .inc files matching "${pattern}" in AMX Mod X ${version} includes.`
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const shown = limitFiles(files, args);
|
|
399
|
+
const skipped = files.length - shown.length;
|
|
400
|
+
const contents = shown
|
|
401
|
+
.map((rel) => {
|
|
402
|
+
const raw = readFileSafe(path.join(includeDir, rel));
|
|
403
|
+
const processed = grep ? grepContent(raw, grep, before, after) : raw;
|
|
404
|
+
return `──── ${rel} ────\n${processed}${processed.endsWith('\n') ? '' : '\n'}`;
|
|
405
|
+
})
|
|
406
|
+
.join('\n')
|
|
407
|
+
+ (skipped > 0 ? `\n… [${skipped} more file(s); pass full_output=true to list them]` : '');
|
|
408
|
+
|
|
409
|
+
return textResult(
|
|
410
|
+
applyOutputLimit(`AMX Mod X ${version} — ${files.length} standard include file(s):\n\n${contents}`, args)
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ─── Include resolution ─────────────────────────────────────────────────────────
|
|
415
|
+
|
|
416
|
+
async function handleResolveInclude(args, token, noFetch) {
|
|
417
|
+
let parsed;
|
|
418
|
+
try {
|
|
419
|
+
parsed = parseIncludeDirective(args?.directive || args?.include);
|
|
420
|
+
} catch (err) {
|
|
421
|
+
return errorResult(err.message);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const { filename, localFirst } = parsed;
|
|
425
|
+
const searchPaths = [];
|
|
426
|
+
|
|
427
|
+
if (localFirst) {
|
|
428
|
+
const smaDir = args?.sma_file
|
|
429
|
+
? path.dirname(path.resolve(args.sma_file))
|
|
430
|
+
: process.cwd();
|
|
431
|
+
const label = args?.sma_file
|
|
432
|
+
? `local (${path.basename(args.sma_file)})`
|
|
433
|
+
: 'local (current directory)';
|
|
434
|
+
searchPaths.push({ path: smaDir, label });
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Dep includes come BEFORE the stdlib — matching the real build's search
|
|
438
|
+
// order (deps first, then the compiler bundle).
|
|
439
|
+
const manifestPath = resolveManifestPath(args?.manifest || undefined).path;
|
|
440
|
+
const depErrors = [];
|
|
441
|
+
if (fs.existsSync(manifestPath)) {
|
|
442
|
+
try {
|
|
443
|
+
const manifest = parseManifest(manifestPath);
|
|
444
|
+
for (const dep of manifest.globalDeps) {
|
|
445
|
+
try {
|
|
446
|
+
const depDir = await fetchDepIncludeDir(dep, resolveGithubToken(manifest, dep.repo), noFetch);
|
|
447
|
+
searchPaths.push({ path: depDir, label: `${dep.repo}@${dep.ref}` });
|
|
448
|
+
} catch (err) {
|
|
449
|
+
depErrors.push(`${dep.repo}@${dep.ref}: ${err.message}`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
} catch (err) {
|
|
453
|
+
depErrors.push(`manifest ${manifestPath}: ${err.message}`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const version = await resolveAmxmodxVersion(args, noFetch);
|
|
458
|
+
const { includeDir } = await fetchCompiler(version);
|
|
459
|
+
if (includeDir) {
|
|
460
|
+
searchPaths.push({ path: includeDir, label: `AMXX stdlib ${version}` });
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const result = searchIncludeFile(searchPaths, filename);
|
|
464
|
+
|
|
465
|
+
if (!result) {
|
|
466
|
+
let msg =
|
|
467
|
+
`Include "${filename}" not found.\n\n` +
|
|
468
|
+
`Searched:\n` +
|
|
469
|
+
searchPaths.map((s) => ` ${s.label}`).join('\n');
|
|
470
|
+
if (depErrors.length) {
|
|
471
|
+
msg +=
|
|
472
|
+
`\n\nFailed to resolve:\n` +
|
|
473
|
+
depErrors.map((e) => ` ${e}`).join('\n');
|
|
474
|
+
}
|
|
475
|
+
msg += '\n\nTip: provide a manifest with deps, or ensure the compiler is cached.';
|
|
476
|
+
return textResult(msg);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const content = readFileSafe(result.foundPath);
|
|
480
|
+
const grep = args?.grep;
|
|
481
|
+
const before = args?.before || 0;
|
|
482
|
+
const after = args?.after || 0;
|
|
483
|
+
const displayed = grep ? grepContent(content, grep, before, after) : content;
|
|
484
|
+
|
|
485
|
+
let out =
|
|
486
|
+
`Include "${parsed.filename}" resolved to:\n` +
|
|
487
|
+
` Source: ${result.label}\n` +
|
|
488
|
+
` Path: ${result.foundPath}\n\n` +
|
|
489
|
+
`──── ${parsed.filename} ────\n${displayed}${displayed.endsWith('\n') ? '' : '\n'}`;
|
|
490
|
+
if (depErrors.length) {
|
|
491
|
+
out += `\nNote — some deps failed to resolve:\n` + depErrors.map((e) => ` ${e}`).join('\n');
|
|
492
|
+
}
|
|
493
|
+
return textResult(applyOutputLimit(out, args));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ─── Build plan ────────────────────────────────────────────────────────────────
|
|
497
|
+
|
|
498
|
+
async function handleBuildPlan(args) {
|
|
499
|
+
const manifestPath = resolveManifestPath(args?.manifest).path;
|
|
500
|
+
const fullPath = path.resolve(manifestPath);
|
|
501
|
+
loadEnv(fullPath, { quiet: true, override: false });
|
|
502
|
+
|
|
503
|
+
try {
|
|
504
|
+
const manifest = resolveManifest(fullPath, { set: args?.set, define: args?.define });
|
|
505
|
+
return textResult(applyOutputLimit(JSON.stringify(buildPlanData(manifest), null, 2), args));
|
|
506
|
+
} catch (err) {
|
|
507
|
+
return errorResult(err.message);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ─── Repo file access ──────────────────────────────────────────────────────────
|
|
512
|
+
|
|
513
|
+
async function fetchDepRoot(args, token, noFetch) {
|
|
514
|
+
token = fallbackToken(token);
|
|
515
|
+
let dep;
|
|
516
|
+
if (args?.dep) {
|
|
517
|
+
dep = parseDep(args.dep);
|
|
518
|
+
} else {
|
|
519
|
+
if (!args?.repo) throw new Error('Provide either "dep" or "repo"');
|
|
520
|
+
const source = args.source || 'git';
|
|
521
|
+
// Release deps need a ref — default to 'latest' when omitted.
|
|
522
|
+
const ref = args.ref || (source === 'release' ? 'latest' : null);
|
|
523
|
+
dep = { repo: args.repo, ref, source, include_path: args.include_path || null, asset: args.asset ?? null };
|
|
524
|
+
}
|
|
525
|
+
if (args?.source) dep.source = args.source;
|
|
526
|
+
if (args?.include_path) dep.include_path = args.include_path;
|
|
527
|
+
if (args?.asset != null) dep.asset = args.asset;
|
|
528
|
+
|
|
529
|
+
if (dep.source === 'release') {
|
|
530
|
+
const dir = await fetchReleaseDep(dep, token, noFetch);
|
|
531
|
+
return { rootDir: dir, label: `${dep.repo}@${dep.ref} (release)` };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const resolvedRef = await resolveRefIfLatest(dep.ref, dep.repo, token);
|
|
535
|
+
const repoDir = await fetchRepo(dep.repo, resolvedRef, token, noFetch, false);
|
|
536
|
+
if (dep.include_path) {
|
|
537
|
+
const sub = path.join(repoDir, dep.include_path);
|
|
538
|
+
if (!fs.existsSync(sub)) {
|
|
539
|
+
throw new Error(`include_path "${dep.include_path}" not found in ${dep.repo}`);
|
|
540
|
+
}
|
|
541
|
+
return { rootDir: sub, label: `${dep.repo}@${dep.ref || 'default branch'}` };
|
|
542
|
+
}
|
|
543
|
+
return { rootDir: repoDir, label: `${dep.repo}@${dep.ref || 'default branch'}` };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async function handleListRepoFiles(args, token, noFetch) {
|
|
547
|
+
let root;
|
|
548
|
+
try {
|
|
549
|
+
root = await fetchDepRoot(args, token, noFetch);
|
|
550
|
+
} catch (err) {
|
|
551
|
+
return errorResult(err.message);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const pattern = args?.pattern || '**/*';
|
|
555
|
+
const limit = args?.limit || 500;
|
|
556
|
+
|
|
557
|
+
let files;
|
|
558
|
+
try {
|
|
559
|
+
files = await glob(pattern, { cwd: root.rootDir, dot: false });
|
|
560
|
+
} catch (err) {
|
|
561
|
+
return errorResult(`Invalid pattern "${pattern}": ${err.message}`);
|
|
562
|
+
}
|
|
563
|
+
files.sort();
|
|
564
|
+
|
|
565
|
+
const shown = files.slice(0, limit);
|
|
566
|
+
const skipped = files.length - shown.length;
|
|
567
|
+
const listing = shown.map((f) => ` ${f}`).join('\n')
|
|
568
|
+
+ (skipped > 0 ? `\n … [${skipped} more; pass a higher limit]` : '');
|
|
569
|
+
|
|
570
|
+
return textResult(
|
|
571
|
+
applyOutputLimit(
|
|
572
|
+
`${root.label} — ${files.length} file(s) matching "${pattern}":\n\n${listing}`,
|
|
573
|
+
args
|
|
574
|
+
)
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
async function handleReadRepoFile(args, token, noFetch) {
|
|
579
|
+
if (!args?.file) return errorResult('Missing required "file" parameter', -32602);
|
|
580
|
+
|
|
581
|
+
let root;
|
|
582
|
+
try {
|
|
583
|
+
root = await fetchDepRoot(args, token, noFetch);
|
|
584
|
+
} catch (err) {
|
|
585
|
+
return errorResult(err.message);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const target = path.resolve(root.rootDir, args.file);
|
|
589
|
+
if (target !== root.rootDir && !target.startsWith(root.rootDir + path.sep)) {
|
|
590
|
+
return errorResult(`Path escapes the repo root: "${args.file}"`);
|
|
591
|
+
}
|
|
592
|
+
if (!fs.existsSync(target) || fs.statSync(target).isDirectory()) {
|
|
593
|
+
return errorResult(`File not found in ${root.label}: ${args.file}`);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const content = readFileSafe(target);
|
|
597
|
+
const grep = args?.grep;
|
|
598
|
+
const before = args?.before || 0;
|
|
599
|
+
const after = args?.after || 0;
|
|
600
|
+
const displayed = grep ? grepContent(content, grep, before, after) : content;
|
|
601
|
+
|
|
602
|
+
return textResult(
|
|
603
|
+
applyOutputLimit(
|
|
604
|
+
`──── ${args.file} (${root.label}) ────\n${displayed}${displayed.endsWith('\n') ? '' : '\n'}`,
|
|
605
|
+
args
|
|
606
|
+
)
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// ─── Single-file compilation ───────────────────────────────────────────────────
|
|
611
|
+
|
|
612
|
+
async function runCompiler(cmd, args) {
|
|
613
|
+
return spawnCompiler(cmd, args);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
async function handleCompileSma(args, token, noFetch) {
|
|
617
|
+
if (!args?.sma_file) return errorResult('Missing required "sma_file" parameter', -32602);
|
|
618
|
+
const smaPath = path.resolve(args.sma_file);
|
|
619
|
+
if (!fs.existsSync(smaPath)) return errorResult(`File not found: ${smaPath}`);
|
|
620
|
+
|
|
621
|
+
const version = await resolveAmxmodxVersion(args, noFetch);
|
|
622
|
+
const { compilerPath, includeDir } = await fetchCompiler(version);
|
|
623
|
+
|
|
624
|
+
const depDirs = [];
|
|
625
|
+
const depErrors = [];
|
|
626
|
+
const manifestPath = resolveManifestPath(args?.manifest || undefined).path;
|
|
627
|
+
if (fs.existsSync(manifestPath)) {
|
|
628
|
+
try {
|
|
629
|
+
const manifest = parseManifest(manifestPath);
|
|
630
|
+
for (const dep of manifest.globalDeps) {
|
|
631
|
+
try {
|
|
632
|
+
depDirs.push(await fetchDepIncludeDir(dep, resolveGithubToken(manifest, dep.repo), noFetch));
|
|
633
|
+
} catch (err) {
|
|
634
|
+
depErrors.push(`${dep.repo}@${dep.ref}: ${err.message}`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
} catch (err) {
|
|
638
|
+
depErrors.push(`manifest ${manifestPath}: ${err.message}`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Dep includes come BEFORE the stdlib — matching the real build's search
|
|
643
|
+
// order (deps first, then the compiler bundle).
|
|
644
|
+
const includeDirs = [...depDirs];
|
|
645
|
+
if (includeDir) includeDirs.push(includeDir);
|
|
646
|
+
for (const d of args?.include_dirs || []) includeDirs.push(path.resolve(d));
|
|
647
|
+
|
|
648
|
+
const includes = buildIncludeArgs({
|
|
649
|
+
scriptingDir: path.dirname(smaPath),
|
|
650
|
+
localIncDir: path.join(path.dirname(smaPath), 'include'),
|
|
651
|
+
collectedIncDir: undefined,
|
|
652
|
+
includeDirs,
|
|
653
|
+
});
|
|
654
|
+
const defines = buildDefineArgs(args?.define);
|
|
655
|
+
|
|
656
|
+
const outDir = path.join(os.tmpdir(), 'amxb-mcp-compile');
|
|
657
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
658
|
+
// Unique suffix per call: the server now dispatches requests concurrently,
|
|
659
|
+
// so two compile_sma calls for the same file must not share an output path.
|
|
660
|
+
const outPath = path.join(outDir, `${path.basename(smaPath, '.sma')}_${process.pid}_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}.amxx`);
|
|
661
|
+
|
|
662
|
+
const { status, output } = await runCompiler(compilerPath, [smaPath, `-o${outPath}`, ...includes, ...defines]);
|
|
663
|
+
|
|
664
|
+
let msg = status === 0
|
|
665
|
+
? `Compiled OK (amxxpc ${version}): ${path.basename(smaPath)}`
|
|
666
|
+
: `Compilation FAILED (amxxpc ${version}, exit ${status}): ${path.basename(smaPath)}`;
|
|
667
|
+
|
|
668
|
+
if (status === 0 && args?.keep_output) {
|
|
669
|
+
msg += `\n Output: ${outPath}`;
|
|
670
|
+
} else {
|
|
671
|
+
try { fs.rmSync(outPath, { force: true }); } catch (_) {}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (depErrors.length) {
|
|
675
|
+
msg += `\n\nNote — deps failed to resolve:\n` + depErrors.map((e) => ` ${e}`).join('\n');
|
|
676
|
+
}
|
|
677
|
+
msg += `\n\n──── compiler output ────\n${output || '(no output)'}`;
|
|
678
|
+
|
|
679
|
+
return textResult(applyOutputLimit(msg, args));
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// ─── Asset plan ────────────────────────────────────────────────────────────────
|
|
683
|
+
|
|
684
|
+
async function handleResolveAssets(args) {
|
|
685
|
+
const manifestPath = resolveManifestPath(args?.manifest).path;
|
|
686
|
+
const fullPath = path.resolve(manifestPath);
|
|
687
|
+
|
|
688
|
+
let manifest;
|
|
689
|
+
try {
|
|
690
|
+
manifest = parseManifest(fullPath);
|
|
691
|
+
} catch (err) {
|
|
692
|
+
return errorResult(err.message);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const plan = buildPlanData(manifest, {
|
|
696
|
+
detailedAssets: true,
|
|
697
|
+
listLocal: args?.list_local !== false,
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
return textResult(
|
|
701
|
+
applyOutputLimit(
|
|
702
|
+
JSON.stringify({ on_conflict: manifest.assets.on_conflict, sources: plan.assets }, null, 2),
|
|
703
|
+
args
|
|
704
|
+
)
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// ─── Manifest schema ───────────────────────────────────────────────────────────
|
|
709
|
+
|
|
710
|
+
async function handleManifestSchema(args) {
|
|
711
|
+
const schema = getManifestSchema();
|
|
712
|
+
if (!schema) {
|
|
713
|
+
return textResult('No schema file found (schema/amxbuild.schema.json missing).');
|
|
714
|
+
}
|
|
715
|
+
return textResult(applyOutputLimit(JSON.stringify(schema, null, 2), args));
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// ─── Symbol search ─────────────────────────────────────────────────────────────
|
|
719
|
+
|
|
720
|
+
const MAX_SYMBOLS_PER_SOURCE = 100;
|
|
721
|
+
|
|
722
|
+
async function handleSearchSymbol(args, token, noFetch) {
|
|
723
|
+
if (!args?.symbol) return errorResult('Missing required "symbol" parameter', -32602);
|
|
724
|
+
const scope = args?.scope || 'all';
|
|
725
|
+
const partial = args?.partial === true;
|
|
726
|
+
|
|
727
|
+
const sources = [];
|
|
728
|
+
const errors = [];
|
|
729
|
+
|
|
730
|
+
const addSource = async (label, dirs, pattern) => {
|
|
731
|
+
if (!dirs.length) return;
|
|
732
|
+
try {
|
|
733
|
+
const index = await buildIndex(dirs, pattern);
|
|
734
|
+
sources.push({ label, index });
|
|
735
|
+
} catch (err) {
|
|
736
|
+
errors.push(`${label}: ${err.message}`);
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
const manifestPath = resolveManifestPath(args?.manifest || undefined).path;
|
|
741
|
+
let manifest = null;
|
|
742
|
+
if (fs.existsSync(manifestPath)) {
|
|
743
|
+
try {
|
|
744
|
+
manifest = parseManifest(manifestPath);
|
|
745
|
+
} catch (err) {
|
|
746
|
+
errors.push(`manifest ${manifestPath}: ${err.message}`);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Per-owner token when a manifest is in scope; plain arg/env fallback otherwise.
|
|
751
|
+
const tokenFor = (repo) => manifest
|
|
752
|
+
? resolveGithubToken(manifest, repo)
|
|
753
|
+
: fallbackToken(token);
|
|
754
|
+
|
|
755
|
+
const jobs = [];
|
|
756
|
+
|
|
757
|
+
if (scope === 'all' || scope === 'stdlib') {
|
|
758
|
+
jobs.push((async () => {
|
|
759
|
+
try {
|
|
760
|
+
const version = await resolveAmxmodxVersion(args, noFetch);
|
|
761
|
+
const { includeDir } = await fetchCompiler(version);
|
|
762
|
+
if (includeDir) await addSource(`stdlib ${version}`, [includeDir], '**/*.inc');
|
|
763
|
+
} catch (err) {
|
|
764
|
+
errors.push(`stdlib: ${err.message}`);
|
|
765
|
+
}
|
|
766
|
+
})());
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const deps = manifest?.globalDeps?.length
|
|
770
|
+
? manifest.globalDeps
|
|
771
|
+
: (args?.deps || []).map(parseDep);
|
|
772
|
+
if ((scope === 'all' || scope === 'deps') && deps.length) {
|
|
773
|
+
for (const dep of deps) {
|
|
774
|
+
jobs.push((async () => {
|
|
775
|
+
try {
|
|
776
|
+
const dir = await fetchDepIncludeDir(dep, tokenFor(dep.repo), noFetch);
|
|
777
|
+
await addSource(`${dep.repo}@${dep.ref}`, [dir], '**/*.inc');
|
|
778
|
+
} catch (err) {
|
|
779
|
+
errors.push(`${dep.repo}@${dep.ref}: ${err.message}`);
|
|
780
|
+
}
|
|
781
|
+
})());
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (scope === 'all' || scope === 'local') {
|
|
786
|
+
const baseDir = manifest ? path.dirname(manifest._path) : process.cwd();
|
|
787
|
+
const amxDir = manifest
|
|
788
|
+
? path.join(path.dirname(manifest._path), manifest.amxmodx.dir)
|
|
789
|
+
: path.join(process.cwd(), 'amxmodx');
|
|
790
|
+
if (fs.existsSync(amxDir)) {
|
|
791
|
+
await addSource('local project', [amxDir]);
|
|
792
|
+
} else {
|
|
793
|
+
errors.push('local: no amxmodx/ dir found next to the manifest');
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
await Promise.all(jobs);
|
|
798
|
+
|
|
799
|
+
if (!sources.length) {
|
|
800
|
+
return textResult(
|
|
801
|
+
`No searchable sources.\n\nErrors:\n` +
|
|
802
|
+
(errors.length ? errors.map((e) => ` ${e}`).join('\n') : ' (none)')
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const matches = sources.map((s) => ({
|
|
807
|
+
label: s.label,
|
|
808
|
+
results: searchIndex(s.index, args.symbol, { partial }),
|
|
809
|
+
}));
|
|
810
|
+
|
|
811
|
+
const total = matches.reduce((n, m) => n + m.results.length, 0);
|
|
812
|
+
if (total === 0) {
|
|
813
|
+
let msg =
|
|
814
|
+
`Symbol "${args.symbol}" not found in any source.\n\nSearched:\n` +
|
|
815
|
+
matches.map((m) => ` ${m.label}`).join('\n');
|
|
816
|
+
if (errors.length) msg += `\n\nFailed to search:\n` + errors.map((e) => ` ${e}`).join('\n');
|
|
817
|
+
return textResult(msg);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
let out = `Symbol "${args.symbol}" — ${total} declaration(s)${partial ? ' (partial match)' : ''}:\n`;
|
|
821
|
+
for (const m of matches) {
|
|
822
|
+
if (!m.results.length) continue;
|
|
823
|
+
const shown = m.results.slice(0, MAX_SYMBOLS_PER_SOURCE);
|
|
824
|
+
out += `\n── ${m.label} ──\n`;
|
|
825
|
+
for (const r of shown) {
|
|
826
|
+
out += ` ${r.name}\n`;
|
|
827
|
+
for (const hit of r.matches) {
|
|
828
|
+
out += ` ${hit.file}:${hit.line} [${hit.kind}] ${hit.signature}\n`;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
if (m.results.length > shown.length) {
|
|
832
|
+
out += ` … [${m.results.length - shown.length} more]`;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
if (errors.length) out += `\n\nNote — failed to search:\n` + errors.map((e) => ` ${e}`).join('\n');
|
|
836
|
+
return textResult(applyOutputLimit(out, args));
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// ─── Dispatch ──────────────────────────────────────────────────────────────────
|
|
840
|
+
|
|
841
|
+
const HANDLERS = {
|
|
842
|
+
get_dep_interface: handleGetDepInterface,
|
|
843
|
+
list_dep_incs: handleListDepIncs,
|
|
844
|
+
get_dep_tree: handleGetDepTree,
|
|
845
|
+
resolve_manifest: handleResolveManifestTool,
|
|
846
|
+
validate_manifest: handleValidateManifestTool,
|
|
847
|
+
get_cache_info: handleGetCacheInfo,
|
|
848
|
+
list_releases: handleListReleasesTool,
|
|
849
|
+
build_include_tree: handleBuildIncludeTree,
|
|
850
|
+
list_amxmodx_incs: handleListAmxmodxIncs,
|
|
851
|
+
get_amxmodx_include: handleGetAmxmodxInclude,
|
|
852
|
+
resolve_include: handleResolveInclude,
|
|
853
|
+
build_plan: handleBuildPlan,
|
|
854
|
+
list_repo_files: handleListRepoFiles,
|
|
855
|
+
read_repo_file: handleReadRepoFile,
|
|
856
|
+
compile_sma: handleCompileSma,
|
|
857
|
+
resolve_assets: handleResolveAssets,
|
|
858
|
+
manifest_schema: handleManifestSchema,
|
|
859
|
+
search_symbol: handleSearchSymbol,
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
module.exports = { HANDLERS };
|