amxx-builder 1.5.2 → 1.6.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 +12 -2
- package/README.md +157 -3
- package/defaults/amxbuild.defaults.yml +7 -0
- package/mcp/handlers.js +378 -92
- package/mcp/registry.js +298 -0
- package/package.json +2 -1
- package/skills/amxb-migration/SKILL.md +568 -0
- package/src/agent-assets.js +206 -0
- package/src/asset-fetcher.js +33 -48
- package/src/build-plan.js +3 -1
- package/src/build-service.js +17 -8
- package/src/cli.js +37 -4
- package/src/collector.js +9 -1
- package/src/commands/deps-tree.js +4 -1
- package/src/commands/dry-run.js +4 -0
- package/src/commands/init.js +72 -20
- package/src/commands/opencode-skills.js +47 -0
- package/src/commands/serve.js +620 -116
- package/src/commands/skills-dir.js +21 -0
- package/src/commands/watch.js +48 -19
- package/src/compile-utils.js +11 -2
- package/src/compiler-fetcher.js +229 -41
- package/src/compiler.js +26 -14
- package/src/dep-graph.js +50 -0
- package/src/deployer.js +50 -21
- package/src/deps-resolver.js +160 -7
- package/src/deps-tree.js +20 -1
- package/src/download.js +132 -0
- package/src/fs-utils.js +35 -1
- package/src/fungun-fetcher.js +367 -0
- package/src/github-api.js +309 -0
- package/src/include-tree.js +29 -57
- package/src/jsonrpc-transport.js +55 -6
- package/src/manifest-path.js +18 -1
- package/src/manifest.js +144 -5
- package/src/opencode-skills.js +262 -0
- package/src/release-fetcher.js +26 -33
- package/src/repo-fetcher.js +276 -33
- package/src/retry.js +3 -1
- package/templates/init-workflow.yml +1 -1
package/src/commands/serve.js
CHANGED
|
@@ -16,18 +16,29 @@
|
|
|
16
16
|
* Method table:
|
|
17
17
|
* manifest.validate → validate.manifestFile
|
|
18
18
|
* manifest.resolve → env.loadEnv + manifest.resolveManifest
|
|
19
|
+
* manifest.dep → agent-assets readDepManifest + parseDocEntries/parseSkillEntries
|
|
20
|
+
* docs.list / docs.get → agent-assets collectDepAssets / collectLocalAssets
|
|
21
|
+
* skills.list / skills.get → agent-assets collectDepAssets / collectLocalAssets
|
|
19
22
|
* include.resolve → include-tree parseIncludeDirective + searchIncludeFile
|
|
20
|
-
* include.list →
|
|
23
|
+
* include.list → deps-resolver collectDepIncludeDirs + include-tree collectIncFiles
|
|
21
24
|
* amxmodx.includes.list → compiler-fetcher fetchCompiler + glob
|
|
22
25
|
* amxmodx.include.get → compiler-fetcher fetchCompiler + glob + read
|
|
23
26
|
* deps.tree → deps-tree buildDepTree + assembleRootDeps
|
|
24
27
|
* releases.list → release-lister listReleases / listTags
|
|
28
|
+
* repos.info → github-api getRepoInfo
|
|
29
|
+
* repos.branches → github-api listBranches
|
|
30
|
+
* repos.structure → github-api getRepoStructure
|
|
25
31
|
* cache.info → cache-info getCacheInfo
|
|
32
|
+
* compiler.info → compiler-fetcher getCompilerInfo
|
|
33
|
+
* dep-graph.get → dep-graph DepGraph
|
|
26
34
|
* build.plan → build-plan buildPlanData
|
|
27
35
|
* build.start → build-service runBuild (+ event notifications)
|
|
28
36
|
* build.cancel → abort the running build (AbortController)
|
|
29
|
-
* compile.single → compiler.compileSingle
|
|
37
|
+
* compile.single → compiler.compileSingle (+ captured output)
|
|
38
|
+
* deploy.start / deploy.file / deploy.remove → deployer deployBuild/deployFile/removeDeployedFile
|
|
39
|
+
* rcon.send → rcon sendRcon
|
|
30
40
|
* watch.start / watch.stop → watcher.startWatch (+ watch.changed notifications)
|
|
41
|
+
* serve.ping → process info (health check)
|
|
31
42
|
*/
|
|
32
43
|
|
|
33
44
|
const fs = require('fs');
|
|
@@ -43,17 +54,27 @@ const { on, off, EVENTS } = require('../events');
|
|
|
43
54
|
|
|
44
55
|
const { loadEnv } = require('../env');
|
|
45
56
|
const { resolveManifestPath } = require('../manifest-path');
|
|
46
|
-
const { resolveManifest, parseManifest, resolveGithubToken, parseDepString } = require('../manifest');
|
|
57
|
+
const { resolveManifest, parseManifest, resolveGithubToken, parseDepString, parseDocEntries, parseSkillEntries } = require('../manifest');
|
|
58
|
+
const { readDepManifest, collectDepAssets, collectLocalAssets } = require('../agent-assets');
|
|
47
59
|
const { validateManifestFile } = require('../validate');
|
|
48
|
-
const {
|
|
49
|
-
const {
|
|
60
|
+
const { collectIncFiles, parseIncludeDirective, searchIncludeFile } = require('../include-tree');
|
|
61
|
+
const { depLabel, collectDepIncludeDirs } = require('../deps-resolver');
|
|
62
|
+
const { fetchCompiler, resolveAmxmodxVersion, getCompilerInfo, getHostPlatform, resolveStdlibVersion } = require('../compiler-fetcher');
|
|
50
63
|
const { buildDepTree, assembleRootDeps } = require('../deps-tree');
|
|
51
64
|
const { listReleases, listTags } = require('../release-lister');
|
|
65
|
+
const {
|
|
66
|
+
getRepoInfo, listBranches, getRepoStructure,
|
|
67
|
+
GithubError, isValidRepo, validateRepoStructureOptions,
|
|
68
|
+
} = require('../github-api');
|
|
52
69
|
const { getCacheInfo } = require('../cache-info');
|
|
53
70
|
const { buildPlanData } = require('../build-plan');
|
|
54
71
|
const { runBuild } = require('../build-service');
|
|
55
72
|
const { compileSingle } = require('../compiler');
|
|
73
|
+
const { deployBuild, deployFile, removeDeployedFile } = require('../deployer');
|
|
74
|
+
const { sendRcon } = require('../rcon');
|
|
75
|
+
const { DepGraph } = require('../dep-graph');
|
|
56
76
|
const { startWatch } = require('../watcher');
|
|
77
|
+
const pkg = require('../../package.json');
|
|
57
78
|
|
|
58
79
|
// ─── Small interface helpers (no domain logic) ────────────────────────────────
|
|
59
80
|
|
|
@@ -66,26 +87,175 @@ function readFileSafe(absPath) {
|
|
|
66
87
|
}
|
|
67
88
|
}
|
|
68
89
|
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
|
|
90
|
+
// JSON-RPC params spell no-fetch either `noFetch` (serve convention) or
|
|
91
|
+
// `no_fetch` (snake_case convention) — normalize once here.
|
|
92
|
+
function noFetchParam(params) {
|
|
93
|
+
return params?.noFetch === true || params?.no_fetch === true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Resolve a concrete compiler version string for handlers that go on to call
|
|
97
|
+
// fetchCompiler() (dep-graph.get, compile.single). Explicit `version` arg wins,
|
|
98
|
+
// then the project manifest's amxmodx.version, then latest — priority lives in
|
|
99
|
+
// core (compiler-fetcher.resolveAmxmodxVersion). An invalid explicit version is
|
|
100
|
+
// a JSON-RPC param error (-32602); everything else (incl. LATEST_NOT_CACHED)
|
|
101
|
+
// propagates — these handlers fetch a real compiler, so a graceful empty state
|
|
102
|
+
// is meaningless here.
|
|
72
103
|
async function resolveVersionFromParams(params) {
|
|
73
|
-
|
|
104
|
+
try {
|
|
105
|
+
if (params?.version) {
|
|
106
|
+
// await is required: a returned non-awaited promise bypasses this
|
|
107
|
+
// try/catch, so INVALID_AMXMODX_VERSION would never map to -32602.
|
|
108
|
+
return await resolveAmxmodxVersion(null, { version: params.version, noFetch: noFetchParam(params) });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const manifestPath = params?.manifest
|
|
112
|
+
? path.resolve(params.manifest)
|
|
113
|
+
: resolveManifestPath().path;
|
|
114
|
+
let manifest = null;
|
|
115
|
+
if (fs.existsSync(manifestPath)) {
|
|
116
|
+
try { manifest = parseManifest(manifestPath); } catch { manifest = null; }
|
|
117
|
+
}
|
|
118
|
+
return await resolveAmxmodxVersion(manifest, { noFetch: noFetchParam(params) });
|
|
119
|
+
} catch (err) {
|
|
120
|
+
if (err && err.code === 'INVALID_AMXMODX_VERSION') {
|
|
121
|
+
err.code = -32602;
|
|
122
|
+
throw err;
|
|
123
|
+
}
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
74
127
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
128
|
+
// Informational stdlib lookup for manifest-less / offline clients. Never
|
|
129
|
+
// downloads when noFetch; if latest cannot be resolved offline, degrades to
|
|
130
|
+
// the newest cached compiler instead of failing (degraded: true). Nothing
|
|
131
|
+
// cached at all → graceful empty state (version: null), not an error.
|
|
132
|
+
// Version-priority + offline degradation live in core
|
|
133
|
+
// (compiler-fetcher.resolveStdlibVersion); this wrapper only does arg/manifest
|
|
134
|
+
// discovery, the INVALID→-32602 remap and getCompilerInfo state shaping.
|
|
135
|
+
async function resolveStdlibState(params) {
|
|
136
|
+
const noFetch = noFetchParam(params);
|
|
78
137
|
let manifest = null;
|
|
79
|
-
if (
|
|
80
|
-
|
|
138
|
+
if (!params?.version) {
|
|
139
|
+
const manifestPath = manifestPathFor(params);
|
|
140
|
+
if (fs.existsSync(manifestPath)) {
|
|
141
|
+
try { manifest = parseManifest(manifestPath); } catch { manifest = null; }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const { version, degraded, error } = await resolveStdlibVersion({
|
|
145
|
+
version: params?.version,
|
|
146
|
+
manifest,
|
|
147
|
+
noFetch,
|
|
148
|
+
});
|
|
149
|
+
if (error) {
|
|
150
|
+
if (error.code === 'INVALID_AMXMODX_VERSION') error.code = -32602;
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
if (version === null) {
|
|
154
|
+
return { version: null, degraded: false, platform: getHostPlatform(), compilerPath: null, includeDir: null, cached: false };
|
|
81
155
|
}
|
|
82
|
-
|
|
156
|
+
const info = await getCompilerInfo(version, { noFetch });
|
|
157
|
+
return { version: info.version, degraded, platform: info.platform, compilerPath: info.compilerPath, includeDir: info.includeDir, cached: info.cached };
|
|
83
158
|
}
|
|
84
159
|
|
|
85
160
|
function manifestPathFor(params) {
|
|
86
161
|
return params?.manifest ? path.resolve(params.manifest) : resolveManifestPath().path;
|
|
87
162
|
}
|
|
88
163
|
|
|
164
|
+
/**
|
|
165
|
+
* GitHub token for repo-scope methods. Manifest tokens win: the .env next to
|
|
166
|
+
* the manifest is loaded first (it overrides the cwd .env loaded at startup —
|
|
167
|
+
* per the env convention: manifest .env is primary, cwd is the fallback).
|
|
168
|
+
* Then the explicit `token` param, then the process env, then anonymous.
|
|
169
|
+
*/
|
|
170
|
+
function resolveGithubTokenFor(params, repo) {
|
|
171
|
+
const manifestPath = manifestPathFor(params);
|
|
172
|
+
if (fs.existsSync(manifestPath)) {
|
|
173
|
+
loadEnvQuiet(manifestPath);
|
|
174
|
+
try {
|
|
175
|
+
const fromManifest = resolveGithubToken(parseManifest(manifestPath), repo);
|
|
176
|
+
if (fromManifest) return fromManifest;
|
|
177
|
+
} catch { /* unparseable manifest — fall back to explicit/env token */ }
|
|
178
|
+
}
|
|
179
|
+
return params?.token || process.env.GITHUB_TOKEN || null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Build a parsed dep object from params: either a full `dep` string/object or
|
|
183
|
+
// explicit { repo, ref?, source?, include_path?, asset? } fields.
|
|
184
|
+
function depFromParams(params) {
|
|
185
|
+
let dep;
|
|
186
|
+
if (params?.dep) {
|
|
187
|
+
dep = typeof params.dep === 'string' ? parseDepString(params.dep) : { ...params.dep };
|
|
188
|
+
} else {
|
|
189
|
+
if (!params?.repo) throw new Error('Provide either "dep" or "repo"');
|
|
190
|
+
const source = params.source || 'git';
|
|
191
|
+
const ref = params.ref || (source === 'release' ? 'latest' : null);
|
|
192
|
+
dep = { repo: params.repo, ref, source, include_path: params.include_path || null, asset: params.asset ?? null };
|
|
193
|
+
}
|
|
194
|
+
if (params?.source) dep.source = params.source;
|
|
195
|
+
if (params?.include_path) dep.include_path = params.include_path;
|
|
196
|
+
if (params?.asset != null) dep.asset = params.asset;
|
|
197
|
+
return dep;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Agent docs/skills: dep mode when dep/repo is present, otherwise the local
|
|
201
|
+
// project's own manifest.
|
|
202
|
+
async function collectAgentAssets(params) {
|
|
203
|
+
const noFetch = noFetchParam(params);
|
|
204
|
+
if (params?.dep || params?.repo) {
|
|
205
|
+
const dep = depFromParams(params);
|
|
206
|
+
const token = resolveGithubTokenFor(params, dep.repo);
|
|
207
|
+
return collectDepAssets(dep, { token, noFetch });
|
|
208
|
+
}
|
|
209
|
+
const manifestPath = manifestPathFor(params);
|
|
210
|
+
loadEnvQuiet(manifestPath);
|
|
211
|
+
return collectLocalAssets(parseManifest(manifestPath));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Shape a GitHub API error into the JSON-RPC error contract: -32603 with
|
|
216
|
+
* error.data = { status, repo, message }. Returns null for non-GitHub errors
|
|
217
|
+
* so the caller rethrows them unchanged.
|
|
218
|
+
*/
|
|
219
|
+
function githubRpcError(err, repo) {
|
|
220
|
+
if (err instanceof GithubError) {
|
|
221
|
+
err.code = -32603;
|
|
222
|
+
err.data = { status: err.status, repo, message: err.message };
|
|
223
|
+
return err;
|
|
224
|
+
}
|
|
225
|
+
if (err && err.response && typeof err.response.status === 'number') {
|
|
226
|
+
// Raw axios error (release-lister path) — normalize to the same contract.
|
|
227
|
+
const message = (err.response.data && err.response.data.message) || err.message;
|
|
228
|
+
const shaped = new Error(message);
|
|
229
|
+
shaped.code = -32603;
|
|
230
|
+
shaped.data = { status: err.response.status, repo, message };
|
|
231
|
+
return shaped;
|
|
232
|
+
}
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function repoParamError(params) {
|
|
237
|
+
if (!params?.repo) return 'Missing required "repo" field';
|
|
238
|
+
if (!isValidRepo(params.repo)) return 'Invalid "repo": expected "owner/repo"';
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// dotenv@17 prints an "injected env" line to stdout by default, which would
|
|
243
|
+
// break the pure-JSON-RPC stdout contract — always load quietly here.
|
|
244
|
+
function loadEnvQuiet(manifestPath) {
|
|
245
|
+
loadEnv(manifestPath, { quiet: true });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Full manifest (defaults merged, set/define applied) for deploy methods.
|
|
249
|
+
function deployRequestManifest(params) {
|
|
250
|
+
const manifestPath = manifestPathFor(params);
|
|
251
|
+
loadEnvQuiet(manifestPath);
|
|
252
|
+
return resolveManifest(manifestPath, { set: params?.set, define: params?.define });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function buildDirFor(params) {
|
|
256
|
+
return params?.buildDir ? path.resolve(params.buildDir) : path.join(process.cwd(), 'build');
|
|
257
|
+
}
|
|
258
|
+
|
|
89
259
|
/**
|
|
90
260
|
* Create and configure the JSON-RPC server with all methods wired to core.
|
|
91
261
|
* Does NOT connect or set up the environment — call runServe() for that.
|
|
@@ -97,6 +267,15 @@ function createServeServer() {
|
|
|
97
267
|
let activeBuild = null; // AbortController for the running build
|
|
98
268
|
let activeWatcher = null; // chokidar watcher instance
|
|
99
269
|
|
|
270
|
+
// ─── Health check ────────────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
server.onRequest('serve.ping', () => ({
|
|
273
|
+
ok: true,
|
|
274
|
+
pid: process.pid,
|
|
275
|
+
version: pkg.version,
|
|
276
|
+
node: process.version,
|
|
277
|
+
}));
|
|
278
|
+
|
|
100
279
|
// ─── Read-only: manifest ──────────────────────────────────────────────────
|
|
101
280
|
|
|
102
281
|
server.onRequest('manifest.validate', (params) => {
|
|
@@ -105,10 +284,93 @@ function createServeServer() {
|
|
|
105
284
|
|
|
106
285
|
server.onRequest('manifest.resolve', (params) => {
|
|
107
286
|
const manifestPath = manifestPathFor(params);
|
|
108
|
-
|
|
287
|
+
loadEnvQuiet(manifestPath);
|
|
109
288
|
return resolveManifest(manifestPath, { set: params?.set, define: params?.define });
|
|
110
289
|
});
|
|
111
290
|
|
|
291
|
+
// ─── Dependency manifest + agent assets ──────────────────────────────────
|
|
292
|
+
|
|
293
|
+
server.onRequest('manifest.dep', async (params) => {
|
|
294
|
+
let dep;
|
|
295
|
+
try {
|
|
296
|
+
dep = depFromParams(params);
|
|
297
|
+
} catch (err) {
|
|
298
|
+
err.code = -32602;
|
|
299
|
+
throw err;
|
|
300
|
+
}
|
|
301
|
+
const token = resolveGithubTokenFor(params, dep.repo);
|
|
302
|
+
const m = await readDepManifest(dep, { token, noFetch: noFetchParam(params) });
|
|
303
|
+
|
|
304
|
+
let docs = [];
|
|
305
|
+
let skills = [];
|
|
306
|
+
try { docs = parseDocEntries(m.raw?.docs || []); } catch { docs = []; }
|
|
307
|
+
try { skills = parseSkillEntries(m.raw?.skills || []); } catch { skills = []; }
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
label: m.label,
|
|
311
|
+
manifestPath: m.manifestPath,
|
|
312
|
+
manifestName: m.raw?.name || null,
|
|
313
|
+
raw: m.raw,
|
|
314
|
+
docs: docs.map((d) => ({ name: d.name, description: d.description, file: d.file })),
|
|
315
|
+
skills: skills.map((s) => ({ name: s.name, description: s.description, file: s.file, dir: s.dir })),
|
|
316
|
+
};
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
server.onRequest('docs.list', async (params) => {
|
|
320
|
+
const assets = await collectAgentAssets(params);
|
|
321
|
+
return {
|
|
322
|
+
label: assets.label || null,
|
|
323
|
+
docs: assets.docs.map((d) => ({ name: d.name, description: d.description, file: d.file })),
|
|
324
|
+
missing: assets.missing,
|
|
325
|
+
};
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
server.onRequest('docs.get', async (params) => {
|
|
329
|
+
const assets = await collectAgentAssets(params);
|
|
330
|
+
let docs = assets.docs;
|
|
331
|
+
if (params?.name || params?.file) {
|
|
332
|
+
docs = docs.filter(
|
|
333
|
+
(d) => (params.name && d.name === params.name) || (params.file && d.file === params.file)
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
return {
|
|
337
|
+
label: assets.label || null,
|
|
338
|
+
docs: docs.map((d) => ({ name: d.name, description: d.description, file: d.file, content: d.content })),
|
|
339
|
+
missing: assets.missing,
|
|
340
|
+
};
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
server.onRequest('skills.list', async (params) => {
|
|
344
|
+
const assets = await collectAgentAssets(params);
|
|
345
|
+
return {
|
|
346
|
+
label: assets.label || null,
|
|
347
|
+
skills: assets.skills.map((s) => ({
|
|
348
|
+
name: s.name,
|
|
349
|
+
description: s.description,
|
|
350
|
+
kind: s.kind,
|
|
351
|
+
file: s.file,
|
|
352
|
+
dir: s.dir,
|
|
353
|
+
files: s.kind === 'dir' ? s.files.map((f) => f.rel) : undefined,
|
|
354
|
+
})),
|
|
355
|
+
missing: assets.missing,
|
|
356
|
+
};
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
server.onRequest('skills.get', async (params) => {
|
|
360
|
+
const assets = await collectAgentAssets(params);
|
|
361
|
+
let skills = assets.skills;
|
|
362
|
+
if (params?.name) skills = skills.filter((s) => s.name === params.name);
|
|
363
|
+
return {
|
|
364
|
+
label: assets.label || null,
|
|
365
|
+
skills: skills.map((s) => (
|
|
366
|
+
s.kind === 'dir'
|
|
367
|
+
? { name: s.name, description: s.description, kind: 'dir', dir: s.dir, files: s.files.map((f) => ({ rel: f.rel, content: f.content })) }
|
|
368
|
+
: { name: s.name, description: s.description, kind: 'file', file: s.file, content: s.content }
|
|
369
|
+
)),
|
|
370
|
+
missing: assets.missing,
|
|
371
|
+
};
|
|
372
|
+
});
|
|
373
|
+
|
|
112
374
|
// ─── Include resolution ──────────────────────────────────────────────────
|
|
113
375
|
|
|
114
376
|
server.onRequest('include.resolve', async (params) => {
|
|
@@ -138,27 +400,26 @@ function createServeServer() {
|
|
|
138
400
|
let manifest = null;
|
|
139
401
|
const manifestPath = manifestPathFor(params);
|
|
140
402
|
if (fs.existsSync(manifestPath)) {
|
|
403
|
+
loadEnvQuiet(manifestPath);
|
|
141
404
|
try {
|
|
142
405
|
manifest = parseManifest(manifestPath);
|
|
143
|
-
for (const dep of manifest.globalDeps) {
|
|
144
|
-
try {
|
|
145
|
-
const depDir = await fetchDepIncludeDir(
|
|
146
|
-
dep, resolveGithubToken(manifest, dep.repo),
|
|
147
|
-
params?.noFetch === true, manifest.github.ssh
|
|
148
|
-
);
|
|
149
|
-
searchPaths.push({ path: depDir, label: `${dep.repo}@${dep.ref}` });
|
|
150
|
-
} catch (err) {
|
|
151
|
-
errors.push(`${dep.repo}@${dep.ref}: ${err.message}`);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
406
|
} catch (err) {
|
|
155
407
|
errors.push(`manifest ${manifestPath}: ${err.message}`);
|
|
156
408
|
}
|
|
157
409
|
}
|
|
410
|
+
if (manifest) {
|
|
411
|
+
const { dirs, errors: depErrors } = await collectDepIncludeDirs(manifest, {
|
|
412
|
+
noFetch: noFetchParam(params),
|
|
413
|
+
ssh: manifest.github.ssh,
|
|
414
|
+
});
|
|
415
|
+
manifest.globalDeps.forEach((dep, i) => {
|
|
416
|
+
if (dirs[i]) searchPaths.push({ path: dirs[i], label: depLabel(dep) });
|
|
417
|
+
else if (depErrors[i]) errors.push(`${depLabel(dep)}: ${depErrors[i]}`);
|
|
418
|
+
});
|
|
419
|
+
}
|
|
158
420
|
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
if (includeDir) searchPaths.push({ path: includeDir, label: `AMXX stdlib ${version}` });
|
|
421
|
+
const stdlib = await resolveStdlibState(params);
|
|
422
|
+
if (stdlib.includeDir) searchPaths.push({ path: stdlib.includeDir, label: `AMXX stdlib ${stdlib.version}` });
|
|
162
423
|
|
|
163
424
|
const result = searchIncludeFile(searchPaths, filename);
|
|
164
425
|
if (!result) {
|
|
@@ -186,26 +447,35 @@ function createServeServer() {
|
|
|
186
447
|
err.code = -32602;
|
|
187
448
|
throw err;
|
|
188
449
|
}
|
|
450
|
+
loadEnvQuiet(manifestPath);
|
|
189
451
|
const manifest = parseManifest(manifestPath);
|
|
190
452
|
|
|
453
|
+
const { dirs, errors } = await collectDepIncludeDirs(manifest, {
|
|
454
|
+
noFetch: noFetchParam(params),
|
|
455
|
+
ssh: manifest.github.ssh,
|
|
456
|
+
});
|
|
457
|
+
|
|
191
458
|
const deps = [];
|
|
192
|
-
for (
|
|
459
|
+
for (let i = 0; i < manifest.globalDeps.length; i++) {
|
|
460
|
+
const dep = manifest.globalDeps[i];
|
|
461
|
+
const base = dep.source === 'fungun'
|
|
462
|
+
? { source: 'fungun', id: dep.id, url: dep.url }
|
|
463
|
+
: { source: dep.source || 'git', repo: dep.repo, ref: dep.ref };
|
|
464
|
+
if (errors[i]) {
|
|
465
|
+
deps.push({ ...base, error: errors[i], files: [], count: 0 });
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
193
468
|
try {
|
|
194
|
-
const
|
|
195
|
-
dep, resolveGithubToken(manifest, dep.repo),
|
|
196
|
-
params?.noFetch === true, manifest.github.ssh
|
|
197
|
-
);
|
|
198
|
-
const files = await collectIncFiles(includeDir);
|
|
469
|
+
const files = await collectIncFiles(dirs[i]);
|
|
199
470
|
deps.push({
|
|
200
|
-
|
|
201
|
-
ref: dep.ref,
|
|
471
|
+
...base,
|
|
202
472
|
include_path: dep.include_path || null,
|
|
203
|
-
include_dir:
|
|
473
|
+
include_dir: dirs[i],
|
|
204
474
|
count: files.length,
|
|
205
475
|
files: files.map((f) => ({ rel: f.rel, abs: f.abs })),
|
|
206
476
|
});
|
|
207
477
|
} catch (err) {
|
|
208
|
-
deps.push({
|
|
478
|
+
deps.push({ ...base, error: err.message, files: [], count: 0 });
|
|
209
479
|
}
|
|
210
480
|
}
|
|
211
481
|
return { manifest: manifestPath, deps };
|
|
@@ -214,39 +484,49 @@ function createServeServer() {
|
|
|
214
484
|
// ─── AMXX standard includes ──────────────────────────────────────────────
|
|
215
485
|
|
|
216
486
|
server.onRequest('amxmodx.includes.list', async (params) => {
|
|
217
|
-
const
|
|
487
|
+
const state = await resolveStdlibState(params);
|
|
218
488
|
const pattern = params?.pattern || '*.inc';
|
|
219
489
|
|
|
220
|
-
|
|
221
|
-
|
|
490
|
+
if (!state.includeDir) {
|
|
491
|
+
const result = { version: state.version, includeDir: null, pattern, count: 0, files: [] };
|
|
492
|
+
if (state.degraded) result.degraded = true;
|
|
493
|
+
return result;
|
|
494
|
+
}
|
|
222
495
|
|
|
223
|
-
const files = await glob(pattern, { cwd: includeDir, dot: false });
|
|
496
|
+
const files = await glob(pattern, { cwd: state.includeDir, dot: false });
|
|
224
497
|
files.sort();
|
|
225
|
-
|
|
498
|
+
const result = { version: state.version, includeDir: state.includeDir, pattern, count: files.length, files };
|
|
499
|
+
if (state.degraded) result.degraded = true;
|
|
500
|
+
return result;
|
|
226
501
|
});
|
|
227
502
|
|
|
228
503
|
server.onRequest('amxmodx.include.get', async (params) => {
|
|
229
|
-
const
|
|
504
|
+
const state = await resolveStdlibState(params);
|
|
230
505
|
const pattern = params?.file || params?.pattern || '*.inc';
|
|
231
506
|
|
|
232
|
-
|
|
233
|
-
|
|
507
|
+
if (!state.includeDir) {
|
|
508
|
+
const result = { version: state.version, includeDir: null, count: 0, files: [] };
|
|
509
|
+
if (state.degraded) result.degraded = true;
|
|
510
|
+
return result;
|
|
511
|
+
}
|
|
234
512
|
|
|
235
|
-
const files = await glob(pattern, { cwd: includeDir, dot: false });
|
|
513
|
+
const files = await glob(pattern, { cwd: state.includeDir, dot: false });
|
|
236
514
|
files.sort();
|
|
237
|
-
|
|
238
|
-
version,
|
|
239
|
-
includeDir,
|
|
515
|
+
const result = {
|
|
516
|
+
version: state.version,
|
|
517
|
+
includeDir: state.includeDir,
|
|
240
518
|
count: files.length,
|
|
241
|
-
files: files.map((rel) => ({ rel, content: readFileSafe(path.join(includeDir, rel)) })),
|
|
519
|
+
files: files.map((rel) => ({ rel, content: readFileSafe(path.join(state.includeDir, rel)) })),
|
|
242
520
|
};
|
|
521
|
+
if (state.degraded) result.degraded = true;
|
|
522
|
+
return result;
|
|
243
523
|
});
|
|
244
524
|
|
|
245
525
|
// ─── Deps tree ───────────────────────────────────────────────────────────
|
|
246
526
|
|
|
247
527
|
server.onRequest('deps.tree', async (params) => {
|
|
248
528
|
const depth = params?.depth || 0;
|
|
249
|
-
const noFetch = params
|
|
529
|
+
const noFetch = noFetchParam(params);
|
|
250
530
|
|
|
251
531
|
if (params?.deps) {
|
|
252
532
|
const rootDeps = params.deps.map((entry) => {
|
|
@@ -266,7 +546,7 @@ function createServeServer() {
|
|
|
266
546
|
}
|
|
267
547
|
|
|
268
548
|
const manifestPath = manifestPathFor(params);
|
|
269
|
-
|
|
549
|
+
loadEnvQuiet(manifestPath);
|
|
270
550
|
const manifest = parseManifest(manifestPath);
|
|
271
551
|
const assembled = assembleRootDeps(manifest);
|
|
272
552
|
return buildDepTree(assembled.rootDeps, {
|
|
@@ -279,18 +559,136 @@ function createServeServer() {
|
|
|
279
559
|
});
|
|
280
560
|
});
|
|
281
561
|
|
|
282
|
-
// ───
|
|
562
|
+
// ─── Include dependency graph ────────────────────────────────────────────
|
|
563
|
+
|
|
564
|
+
server.onRequest('dep-graph.get', async (params) => {
|
|
565
|
+
if (!params?.sma_file) {
|
|
566
|
+
const err = new Error('Missing required "sma_file" parameter');
|
|
567
|
+
err.code = -32602;
|
|
568
|
+
throw err;
|
|
569
|
+
}
|
|
570
|
+
const smaPath = path.resolve(params.sma_file);
|
|
571
|
+
if (!fs.existsSync(smaPath)) {
|
|
572
|
+
const err = new Error(`File not found: ${smaPath}`);
|
|
573
|
+
err.code = -32602;
|
|
574
|
+
throw err;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const noFetch = noFetchParam(params);
|
|
578
|
+
|
|
579
|
+
const manifestPath = manifestPathFor(params);
|
|
580
|
+
let manifest = null;
|
|
581
|
+
if (fs.existsSync(manifestPath)) {
|
|
582
|
+
loadEnvQuiet(manifestPath);
|
|
583
|
+
try { manifest = parseManifest(manifestPath); } catch { manifest = null; }
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const version = await resolveVersionFromParams(params);
|
|
587
|
+
const { includeDir } = await fetchCompiler(version);
|
|
588
|
+
|
|
589
|
+
// Dep includes come BEFORE the stdlib — matching the real build's search order.
|
|
590
|
+
const includeDirs = [];
|
|
591
|
+
if (manifest) {
|
|
592
|
+
const { dirs } = await collectDepIncludeDirs(manifest, { noFetch, ssh: manifest.github.ssh });
|
|
593
|
+
for (const dir of dirs) if (dir) includeDirs.push(dir);
|
|
594
|
+
}
|
|
595
|
+
if (includeDir) includeDirs.push(includeDir);
|
|
596
|
+
for (const d of (params?.include_dirs || [])) includeDirs.push(path.resolve(d));
|
|
597
|
+
|
|
598
|
+
const graph = new DepGraph(includeDirs);
|
|
599
|
+
graph.parseFile(smaPath);
|
|
600
|
+
|
|
601
|
+
const result = {
|
|
602
|
+
sma_file: smaPath,
|
|
603
|
+
version,
|
|
604
|
+
include_dirs: includeDirs,
|
|
605
|
+
...graph.snapshot(),
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
// Reverse query: which .sma files transitively depend on this .inc?
|
|
609
|
+
if (params?.inc) {
|
|
610
|
+
const incAbs = path.resolve(params.inc);
|
|
611
|
+
result.smas_depending_on = [...graph.getSmasDependingOn(incAbs)].sort();
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
return result;
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
// ─── Releases / repos / cache / plan ─────────────────────────────────────
|
|
283
618
|
|
|
284
619
|
server.onRequest('releases.list', async (params) => {
|
|
285
|
-
|
|
286
|
-
|
|
620
|
+
const invalid = repoParamError(params);
|
|
621
|
+
if (invalid) {
|
|
622
|
+
const err = new Error(invalid);
|
|
287
623
|
err.code = -32602;
|
|
288
624
|
throw err;
|
|
289
625
|
}
|
|
290
|
-
const
|
|
626
|
+
const repo = params.repo;
|
|
627
|
+
const token = resolveGithubTokenFor(params, repo);
|
|
291
628
|
const limit = params?.limit || 10;
|
|
292
|
-
|
|
293
|
-
|
|
629
|
+
try {
|
|
630
|
+
if (params?.tags) return await listTags(repo, { token, limit });
|
|
631
|
+
return await listReleases(repo, { token, limit, includeAssets: params?.includeAssets });
|
|
632
|
+
} catch (err) {
|
|
633
|
+
throw githubRpcError(err, repo) || err;
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
server.onRequest('repos.info', async (params) => {
|
|
638
|
+
const invalid = repoParamError(params);
|
|
639
|
+
if (invalid) {
|
|
640
|
+
const err = new Error(invalid);
|
|
641
|
+
err.code = -32602;
|
|
642
|
+
throw err;
|
|
643
|
+
}
|
|
644
|
+
const repo = params.repo;
|
|
645
|
+
const token = resolveGithubTokenFor(params, repo);
|
|
646
|
+
try {
|
|
647
|
+
return await getRepoInfo(repo, { token });
|
|
648
|
+
} catch (err) {
|
|
649
|
+
throw githubRpcError(err, repo) || err;
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
server.onRequest('repos.branches', async (params) => {
|
|
654
|
+
const invalid = repoParamError(params);
|
|
655
|
+
if (invalid) {
|
|
656
|
+
const err = new Error(invalid);
|
|
657
|
+
err.code = -32602;
|
|
658
|
+
throw err;
|
|
659
|
+
}
|
|
660
|
+
const repo = params.repo;
|
|
661
|
+
const token = resolveGithubTokenFor(params, repo);
|
|
662
|
+
const limit = Math.min(100, Math.max(1, params?.limit ?? 10));
|
|
663
|
+
const page = Math.max(1, params?.page ?? 1);
|
|
664
|
+
try {
|
|
665
|
+
return await listBranches(repo, { token, limit, page });
|
|
666
|
+
} catch (err) {
|
|
667
|
+
throw githubRpcError(err, repo) || err;
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
server.onRequest('repos.structure', async (params) => {
|
|
672
|
+
const invalid = repoParamError(params) || validateRepoStructureOptions(params);
|
|
673
|
+
if (invalid) {
|
|
674
|
+
const err = new Error(invalid);
|
|
675
|
+
err.code = -32602;
|
|
676
|
+
throw err;
|
|
677
|
+
}
|
|
678
|
+
const repo = params.repo;
|
|
679
|
+
const token = resolveGithubTokenFor(params, repo);
|
|
680
|
+
try {
|
|
681
|
+
return await getRepoStructure(repo, {
|
|
682
|
+
token,
|
|
683
|
+
ref: params?.ref || null,
|
|
684
|
+
depth: params?.depth,
|
|
685
|
+
dirsOnly: params?.dirsOnly === true,
|
|
686
|
+
ext: params?.ext,
|
|
687
|
+
maxEntries: params?.maxEntries,
|
|
688
|
+
});
|
|
689
|
+
} catch (err) {
|
|
690
|
+
throw githubRpcError(err, repo) || err;
|
|
691
|
+
}
|
|
294
692
|
});
|
|
295
693
|
|
|
296
694
|
server.onRequest('cache.info', (params) => {
|
|
@@ -298,6 +696,13 @@ function createServeServer() {
|
|
|
298
696
|
return getCacheInfo(manifestPath);
|
|
299
697
|
});
|
|
300
698
|
|
|
699
|
+
server.onRequest('compiler.info', async (params) => {
|
|
700
|
+
const state = await resolveStdlibState(params);
|
|
701
|
+
const result = { version: state.version, platform: state.platform, compilerPath: state.compilerPath, includeDir: state.includeDir, cached: state.cached };
|
|
702
|
+
if (state.degraded) result.degraded = true;
|
|
703
|
+
return result;
|
|
704
|
+
});
|
|
705
|
+
|
|
301
706
|
server.onRequest('build.plan', (params) => {
|
|
302
707
|
const manifestPath = manifestPathFor(params);
|
|
303
708
|
loadEnv(manifestPath, { quiet: true, override: false });
|
|
@@ -320,37 +725,43 @@ function createServeServer() {
|
|
|
320
725
|
const controller = new AbortController();
|
|
321
726
|
activeBuild = controller;
|
|
322
727
|
|
|
323
|
-
const manifestPath = manifestPathFor(params);
|
|
324
|
-
loadEnv(manifestPath);
|
|
325
|
-
const manifest = resolveManifest(manifestPath, { set: params?.set, define: params?.define });
|
|
326
|
-
|
|
327
|
-
// Forward core lifecycle events as server→client notifications while the
|
|
328
|
-
// build runs. COMPILED/PROGRESS are emitted by compiler.js/progress.js on
|
|
329
|
-
// the bus; STAGE/DONE/ERROR by build-service.
|
|
330
|
-
const listeners = [
|
|
331
|
-
[EVENTS.STAGE, (p) => server.notify('build.stage', p)],
|
|
332
|
-
[EVENTS.COMPILED, (p) => server.notify('build.compiled', p)],
|
|
333
|
-
[EVENTS.PROGRESS, (p) => server.notify('build.progress', p)],
|
|
334
|
-
[EVENTS.DONE, (p) => server.notify('build.done', p)],
|
|
335
|
-
[EVENTS.ERROR, (p) => server.notify('build.error', p)],
|
|
336
|
-
];
|
|
337
|
-
for (const [ev, fn] of listeners) on(ev, fn);
|
|
338
|
-
|
|
339
728
|
try {
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
729
|
+
const manifestPath = manifestPathFor(params);
|
|
730
|
+
loadEnvQuiet(manifestPath);
|
|
731
|
+
const manifest = resolveManifest(manifestPath, { set: params?.set, define: params?.define });
|
|
732
|
+
|
|
733
|
+
// Forward core lifecycle events as server→client notifications while the
|
|
734
|
+
// build runs. COMPILED/PROGRESS are emitted by compiler.js/progress.js on
|
|
735
|
+
// the bus; STAGE/DONE/ERROR by build-service.
|
|
736
|
+
const listeners = [
|
|
737
|
+
[EVENTS.STAGE, (p) => server.notify('build.stage', p)],
|
|
738
|
+
[EVENTS.COMPILED, (p) => server.notify('build.compiled', p)],
|
|
739
|
+
[EVENTS.PROGRESS, (p) => server.notify('build.progress', p)],
|
|
740
|
+
[EVENTS.DONE, (p) => server.notify('build.done', p)],
|
|
741
|
+
[EVENTS.ERROR, (p) => server.notify('build.error', p)],
|
|
742
|
+
];
|
|
743
|
+
for (const [ev, fn] of listeners) on(ev, fn);
|
|
744
|
+
|
|
745
|
+
try {
|
|
746
|
+
const result = await runBuild(manifest, {
|
|
747
|
+
buildDir: params?.buildDir,
|
|
748
|
+
fetch: params?.fetch,
|
|
749
|
+
archive: params?.archive,
|
|
750
|
+
signal: controller.signal,
|
|
751
|
+
});
|
|
752
|
+
return result;
|
|
753
|
+
} catch (err) {
|
|
754
|
+
if (err.code === 'CANCELLED') {
|
|
755
|
+
return { ok: false, cancelled: true, message: err.message };
|
|
756
|
+
}
|
|
757
|
+
return { ok: false, message: err.message };
|
|
758
|
+
} finally {
|
|
759
|
+
for (const [ev, fn] of listeners) off(ev, fn);
|
|
350
760
|
}
|
|
351
|
-
return { ok: false, message: err.message };
|
|
352
761
|
} finally {
|
|
353
|
-
|
|
762
|
+
// Even a pre-build failure (manifest missing, bad --set) must release the
|
|
763
|
+
// single-build lock, or every later build.start dies with "Build already
|
|
764
|
+
// running" until the process is restarted.
|
|
354
765
|
activeBuild = null;
|
|
355
766
|
}
|
|
356
767
|
});
|
|
@@ -361,6 +772,85 @@ function createServeServer() {
|
|
|
361
772
|
return { ok: true };
|
|
362
773
|
});
|
|
363
774
|
|
|
775
|
+
// ─── Deploy ──────────────────────────────────────────────────────────────
|
|
776
|
+
|
|
777
|
+
server.onRequest('deploy.start', async (params) => {
|
|
778
|
+
const manifest = deployRequestManifest(params);
|
|
779
|
+
if (!manifest.deploy || !manifest.deploy.path) {
|
|
780
|
+
return { ok: false, message: 'Deploy path not configured.\n → Set AMXB_DEPLOY_PATH in .env, or add deploy.path to your manifest' };
|
|
781
|
+
}
|
|
782
|
+
const copied = await deployBuild(manifest, buildDirFor(params), {
|
|
783
|
+
incremental: params?.incremental === true,
|
|
784
|
+
});
|
|
785
|
+
return { ok: true, copied };
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
server.onRequest('deploy.file', (params) => {
|
|
789
|
+
if (!params?.relPath) {
|
|
790
|
+
const err = new Error('Missing required "relPath" parameter');
|
|
791
|
+
err.code = -32602;
|
|
792
|
+
throw err;
|
|
793
|
+
}
|
|
794
|
+
const section = params?.section === 'assets' ? 'assets' : 'amxmodx';
|
|
795
|
+
const manifest = deployRequestManifest(params);
|
|
796
|
+
if (!manifest.deploy || !manifest.deploy.path) {
|
|
797
|
+
return { ok: false, message: 'Deploy path not configured' };
|
|
798
|
+
}
|
|
799
|
+
const dest = deployFile(manifest, buildDirFor(params), params.relPath, section);
|
|
800
|
+
return { ok: dest != null, dest: dest || null };
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
server.onRequest('deploy.remove', (params) => {
|
|
804
|
+
if (!params?.relPath) {
|
|
805
|
+
const err = new Error('Missing required "relPath" parameter');
|
|
806
|
+
err.code = -32602;
|
|
807
|
+
throw err;
|
|
808
|
+
}
|
|
809
|
+
const section = params?.section === 'assets' ? 'assets' : 'amxmodx';
|
|
810
|
+
const manifest = deployRequestManifest(params);
|
|
811
|
+
if (!manifest.deploy || !manifest.deploy.path) {
|
|
812
|
+
return { ok: false, message: 'Deploy path not configured' };
|
|
813
|
+
}
|
|
814
|
+
const dest = removeDeployedFile(manifest, buildDirFor(params), params.relPath, section);
|
|
815
|
+
return { ok: dest != null, dest: dest || null };
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
// ─── RCON ────────────────────────────────────────────────────────────────
|
|
819
|
+
|
|
820
|
+
server.onRequest('rcon.send', async (params) => {
|
|
821
|
+
if (!params?.command) {
|
|
822
|
+
const err = new Error('Missing required "command" parameter');
|
|
823
|
+
err.code = -32602;
|
|
824
|
+
throw err;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
let host = params?.host;
|
|
828
|
+
let port = params?.port;
|
|
829
|
+
let password = params?.password;
|
|
830
|
+
|
|
831
|
+
// Fall back to the manifest's deploy.rcon config when host/password are omitted.
|
|
832
|
+
if ((host == null || password == null) && fs.existsSync(manifestPathFor(params))) {
|
|
833
|
+
try {
|
|
834
|
+
const rconCfg = parseManifest(manifestPathFor(params)).deploy?.rcon;
|
|
835
|
+
if (rconCfg) {
|
|
836
|
+
if (host == null) host = rconCfg.host;
|
|
837
|
+
if (port == null) port = rconCfg.port;
|
|
838
|
+
if (password == null) password = rconCfg.password;
|
|
839
|
+
}
|
|
840
|
+
} catch { /* unparseable manifest — explicit params only */ }
|
|
841
|
+
}
|
|
842
|
+
if (port == null) port = 27015;
|
|
843
|
+
|
|
844
|
+
if (!host || !password) {
|
|
845
|
+
const err = new Error('RCON host/password not provided (pass explicitly or configure deploy.rcon in the manifest)');
|
|
846
|
+
err.code = -32602;
|
|
847
|
+
throw err;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const response = await sendRcon({ host, port, password, command: params.command });
|
|
851
|
+
return { ok: true, response };
|
|
852
|
+
});
|
|
853
|
+
|
|
364
854
|
// ─── Single-file compile ─────────────────────────────────────────────────
|
|
365
855
|
|
|
366
856
|
server.onRequest('compile.single', async (params) => {
|
|
@@ -376,11 +866,12 @@ function createServeServer() {
|
|
|
376
866
|
throw err;
|
|
377
867
|
}
|
|
378
868
|
|
|
379
|
-
const noFetch = params
|
|
869
|
+
const noFetch = noFetchParam(params);
|
|
380
870
|
|
|
381
871
|
const manifestPath = manifestPathFor(params);
|
|
382
872
|
let manifest = null;
|
|
383
873
|
if (fs.existsSync(manifestPath)) {
|
|
874
|
+
loadEnvQuiet(manifestPath);
|
|
384
875
|
try { manifest = parseManifest(manifestPath); } catch { manifest = null; }
|
|
385
876
|
}
|
|
386
877
|
|
|
@@ -391,36 +882,48 @@ function createServeServer() {
|
|
|
391
882
|
const depDirs = [];
|
|
392
883
|
const depErrors = [];
|
|
393
884
|
if (manifest) {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
} catch (err) {
|
|
400
|
-
depErrors.push(`${dep.repo}@${dep.ref}: ${err.message}`);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
885
|
+
const { dirs, errors } = await collectDepIncludeDirs(manifest, { noFetch, ssh: manifest.github.ssh });
|
|
886
|
+
manifest.globalDeps.forEach((dep, i) => {
|
|
887
|
+
if (dirs[i]) depDirs.push(dirs[i]);
|
|
888
|
+
else if (errors[i]) depErrors.push(`${depLabel(dep)}: ${errors[i]}`);
|
|
889
|
+
});
|
|
403
890
|
}
|
|
404
891
|
|
|
405
892
|
const includeDirs = [...depDirs];
|
|
406
893
|
if (includeDir) includeDirs.push(includeDir);
|
|
407
894
|
for (const d of (params?.include_dirs || [])) includeDirs.push(path.resolve(d));
|
|
408
895
|
|
|
409
|
-
|
|
896
|
+
// Unique per-call build dir + event tag: the transport dispatches requests
|
|
897
|
+
// concurrently, so two compile.single calls for same-named plugins must not
|
|
898
|
+
// share an output path nor mis-attribute COMPILED events (cf. MCP).
|
|
899
|
+
const runId = `${process.pid}_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
900
|
+
const buildDir = path.join(os.tmpdir(), 'amxb-serve-compile', runId);
|
|
410
901
|
const compileManifest = manifest || { amxmodx: { defines: [] } };
|
|
411
|
-
const amxxName = await compileSingle(
|
|
412
|
-
compileManifest,
|
|
413
|
-
smaPath,
|
|
414
|
-
compilerPath,
|
|
415
|
-
includeDirs,
|
|
416
|
-
buildDir,
|
|
417
|
-
params?.scripting_root ? path.resolve(params.scripting_root) : undefined
|
|
418
|
-
);
|
|
419
902
|
|
|
903
|
+
let compiled = null;
|
|
904
|
+
const onCompiled = (p) => { if (p.tag === runId) compiled = p; };
|
|
905
|
+
on(EVENTS.COMPILED, onCompiled);
|
|
906
|
+
let amxxName;
|
|
907
|
+
try {
|
|
908
|
+
amxxName = await compileSingle(
|
|
909
|
+
compileManifest,
|
|
910
|
+
smaPath,
|
|
911
|
+
compilerPath,
|
|
912
|
+
includeDirs,
|
|
913
|
+
buildDir,
|
|
914
|
+
params?.scripting_root ? path.resolve(params.scripting_root) : undefined,
|
|
915
|
+
runId
|
|
916
|
+
);
|
|
917
|
+
} finally {
|
|
918
|
+
off(EVENTS.COMPILED, onCompiled);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
const outputPath = amxxName ? path.join(buildDir, 'amxmodx', 'plugins', amxxName) : null;
|
|
420
922
|
return {
|
|
421
923
|
ok: amxxName != null,
|
|
422
924
|
amxxName,
|
|
423
|
-
|
|
925
|
+
output: compiled ? compiled.output : undefined,
|
|
926
|
+
output_path: outputPath,
|
|
424
927
|
dep_errors: depErrors.length ? depErrors : undefined,
|
|
425
928
|
};
|
|
426
929
|
});
|
|
@@ -435,6 +938,7 @@ function createServeServer() {
|
|
|
435
938
|
}
|
|
436
939
|
|
|
437
940
|
const manifestPath = manifestPathFor(params);
|
|
941
|
+
loadEnvQuiet(manifestPath);
|
|
438
942
|
const manifest = parseManifest(manifestPath);
|
|
439
943
|
|
|
440
944
|
const notify = (kind, extra = {}) => server.notify('watch.changed', { kind, ...extra });
|
|
@@ -460,13 +964,13 @@ function createServeServer() {
|
|
|
460
964
|
return server;
|
|
461
965
|
}
|
|
462
966
|
|
|
463
|
-
// Load project .env from the workspace root like the CLI does; keep stdout free
|
|
464
|
-
// for JSON-RPC (logs → stderr, progress bars disabled).
|
|
465
|
-
function prepareEnvironment() {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
}
|
|
967
|
+
// Load project .env from the workspace root like the CLI does; keep stdout free
|
|
968
|
+
// for JSON-RPC (logs → stderr, progress bars disabled).
|
|
969
|
+
function prepareEnvironment() {
|
|
970
|
+
dotenv.config({ path: path.join(process.cwd(), '.env'), quiet: true });
|
|
971
|
+
logger.setStderr(true);
|
|
972
|
+
progress.setEnabled(false);
|
|
973
|
+
}
|
|
470
974
|
|
|
471
975
|
/**
|
|
472
976
|
* Start the serve server — listens on stdin/stdout forever.
|