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
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `amxb serve` — thin JSON-RPC interface adapter for editor integration.
|
|
6
|
+
*
|
|
7
|
+
* Generic JSON-RPC 2.0 over stdio (src/jsonrpc-transport.js). Every method is a
|
|
8
|
+
* thin mapping: normalize args → call the core single-source function → shape
|
|
9
|
+
* the result. NO domain logic lives here (per AGENTS.md); if a behavior is
|
|
10
|
+
* needed in more than one interface it belongs in src/.
|
|
11
|
+
*
|
|
12
|
+
* Environment: stdout must stay pure JSON-RPC, so logs go to stderr
|
|
13
|
+
* (logger.setStderr) and progress bars are disabled. .env is loaded from the
|
|
14
|
+
* workspace root (cwd), like the CLI.
|
|
15
|
+
*
|
|
16
|
+
* Method table:
|
|
17
|
+
* manifest.validate → validate.manifestFile
|
|
18
|
+
* manifest.resolve → env.loadEnv + manifest.resolveManifest
|
|
19
|
+
* include.resolve → include-tree parseIncludeDirective + searchIncludeFile
|
|
20
|
+
* include.list → include-tree fetchDepIncludeDir + collectIncFiles
|
|
21
|
+
* amxmodx.includes.list → compiler-fetcher fetchCompiler + glob
|
|
22
|
+
* amxmodx.include.get → compiler-fetcher fetchCompiler + glob + read
|
|
23
|
+
* deps.tree → deps-tree buildDepTree + assembleRootDeps
|
|
24
|
+
* releases.list → release-lister listReleases / listTags
|
|
25
|
+
* cache.info → cache-info getCacheInfo
|
|
26
|
+
* build.plan → build-plan buildPlanData
|
|
27
|
+
* build.start → build-service runBuild (+ event notifications)
|
|
28
|
+
* build.cancel → abort the running build (AbortController)
|
|
29
|
+
* compile.single → compiler.compileSingle
|
|
30
|
+
* watch.start / watch.stop → watcher.startWatch (+ watch.changed notifications)
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const fs = require('fs');
|
|
34
|
+
const os = require('os');
|
|
35
|
+
const path = require('path');
|
|
36
|
+
const glob = require('fast-glob');
|
|
37
|
+
const dotenv = require('dotenv');
|
|
38
|
+
|
|
39
|
+
const logger = require('../logger');
|
|
40
|
+
const progress = require('../progress');
|
|
41
|
+
const { JsonRpcServer } = require('../jsonrpc-transport');
|
|
42
|
+
const { on, off, EVENTS } = require('../events');
|
|
43
|
+
|
|
44
|
+
const { loadEnv } = require('../env');
|
|
45
|
+
const { resolveManifestPath } = require('../manifest-path');
|
|
46
|
+
const { resolveManifest, parseManifest, resolveGithubToken, parseDepString } = require('../manifest');
|
|
47
|
+
const { validateManifestFile } = require('../validate');
|
|
48
|
+
const { fetchDepIncludeDir, collectIncFiles, parseIncludeDirective, searchIncludeFile } = require('../include-tree');
|
|
49
|
+
const { fetchCompiler, resolveAmxmodxVersion } = require('../compiler-fetcher');
|
|
50
|
+
const { buildDepTree, assembleRootDeps } = require('../deps-tree');
|
|
51
|
+
const { listReleases, listTags } = require('../release-lister');
|
|
52
|
+
const { getCacheInfo } = require('../cache-info');
|
|
53
|
+
const { buildPlanData } = require('../build-plan');
|
|
54
|
+
const { runBuild } = require('../build-service');
|
|
55
|
+
const { compileSingle } = require('../compiler');
|
|
56
|
+
const { startWatch } = require('../watcher');
|
|
57
|
+
|
|
58
|
+
// ─── Small interface helpers (no domain logic) ────────────────────────────────
|
|
59
|
+
|
|
60
|
+
function readFileSafe(absPath) {
|
|
61
|
+
try {
|
|
62
|
+
const text = fs.readFileSync(absPath, 'utf8');
|
|
63
|
+
return text;
|
|
64
|
+
} catch (err) {
|
|
65
|
+
return `[error reading file: ${err.message}]`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Resolve the AMX Mod X version for a request: explicit `version` arg wins,
|
|
70
|
+
// then the project manifest's amxmodx.version, then latest. Priority logic
|
|
71
|
+
// lives in core (compiler-fetcher.resolveAmxmodxVersion).
|
|
72
|
+
async function resolveVersionFromParams(params) {
|
|
73
|
+
if (params?.version) return resolveAmxmodxVersion(null, { version: params.version });
|
|
74
|
+
|
|
75
|
+
const manifestPath = params?.manifest
|
|
76
|
+
? path.resolve(params.manifest)
|
|
77
|
+
: resolveManifestPath().path;
|
|
78
|
+
let manifest = null;
|
|
79
|
+
if (fs.existsSync(manifestPath)) {
|
|
80
|
+
try { manifest = parseManifest(manifestPath); } catch { manifest = null; }
|
|
81
|
+
}
|
|
82
|
+
return resolveAmxmodxVersion(manifest, { noFetch: params?.noFetch === true });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function manifestPathFor(params) {
|
|
86
|
+
return params?.manifest ? path.resolve(params.manifest) : resolveManifestPath().path;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Create and configure the JSON-RPC server with all methods wired to core.
|
|
91
|
+
* Does NOT connect or set up the environment — call runServe() for that.
|
|
92
|
+
*/
|
|
93
|
+
function createServeServer() {
|
|
94
|
+
const server = new JsonRpcServer();
|
|
95
|
+
|
|
96
|
+
// One build / one watcher at a time (per-process).
|
|
97
|
+
let activeBuild = null; // AbortController for the running build
|
|
98
|
+
let activeWatcher = null; // chokidar watcher instance
|
|
99
|
+
|
|
100
|
+
// ─── Read-only: manifest ──────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
server.onRequest('manifest.validate', (params) => {
|
|
103
|
+
return validateManifestFile(manifestPathFor(params));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
server.onRequest('manifest.resolve', (params) => {
|
|
107
|
+
const manifestPath = manifestPathFor(params);
|
|
108
|
+
loadEnv(manifestPath);
|
|
109
|
+
return resolveManifest(manifestPath, { set: params?.set, define: params?.define });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// ─── Include resolution ──────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
server.onRequest('include.resolve', async (params) => {
|
|
115
|
+
let parsed;
|
|
116
|
+
try {
|
|
117
|
+
parsed = parseIncludeDirective(params?.directive || params?.include);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
err.code = -32602;
|
|
120
|
+
throw err;
|
|
121
|
+
}
|
|
122
|
+
const { filename, localFirst } = parsed;
|
|
123
|
+
const searchPaths = [];
|
|
124
|
+
|
|
125
|
+
if (localFirst) {
|
|
126
|
+
const smaDir = params?.sma_file
|
|
127
|
+
? path.dirname(path.resolve(params.sma_file))
|
|
128
|
+
: process.cwd();
|
|
129
|
+
searchPaths.push({
|
|
130
|
+
path: smaDir,
|
|
131
|
+
label: params?.sma_file ? `local (${path.basename(params.sma_file)})` : 'local (current directory)',
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Dep includes come BEFORE the stdlib — matching the real build's search
|
|
136
|
+
// order (deps first, then the compiler bundle).
|
|
137
|
+
const errors = [];
|
|
138
|
+
let manifest = null;
|
|
139
|
+
const manifestPath = manifestPathFor(params);
|
|
140
|
+
if (fs.existsSync(manifestPath)) {
|
|
141
|
+
try {
|
|
142
|
+
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
|
+
} catch (err) {
|
|
155
|
+
errors.push(`manifest ${manifestPath}: ${err.message}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const version = await resolveVersionFromParams(params);
|
|
160
|
+
const { includeDir } = await fetchCompiler(version);
|
|
161
|
+
if (includeDir) searchPaths.push({ path: includeDir, label: `AMXX stdlib ${version}` });
|
|
162
|
+
|
|
163
|
+
const result = searchIncludeFile(searchPaths, filename);
|
|
164
|
+
if (!result) {
|
|
165
|
+
return {
|
|
166
|
+
found: false,
|
|
167
|
+
filename,
|
|
168
|
+
searched: searchPaths.map((s) => s.label),
|
|
169
|
+
errors: errors.length ? errors : undefined,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
found: true,
|
|
174
|
+
filename,
|
|
175
|
+
absPath: result.foundPath,
|
|
176
|
+
source: result.label,
|
|
177
|
+
searched: searchPaths.map((s) => s.label),
|
|
178
|
+
errors: errors.length ? errors : undefined,
|
|
179
|
+
};
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
server.onRequest('include.list', async (params) => {
|
|
183
|
+
const manifestPath = manifestPathFor(params);
|
|
184
|
+
if (!fs.existsSync(manifestPath)) {
|
|
185
|
+
const err = new Error(`Manifest not found: ${manifestPath}`);
|
|
186
|
+
err.code = -32602;
|
|
187
|
+
throw err;
|
|
188
|
+
}
|
|
189
|
+
const manifest = parseManifest(manifestPath);
|
|
190
|
+
|
|
191
|
+
const deps = [];
|
|
192
|
+
for (const dep of manifest.globalDeps) {
|
|
193
|
+
try {
|
|
194
|
+
const includeDir = await fetchDepIncludeDir(
|
|
195
|
+
dep, resolveGithubToken(manifest, dep.repo),
|
|
196
|
+
params?.noFetch === true, manifest.github.ssh
|
|
197
|
+
);
|
|
198
|
+
const files = await collectIncFiles(includeDir);
|
|
199
|
+
deps.push({
|
|
200
|
+
repo: dep.repo,
|
|
201
|
+
ref: dep.ref,
|
|
202
|
+
include_path: dep.include_path || null,
|
|
203
|
+
include_dir: includeDir,
|
|
204
|
+
count: files.length,
|
|
205
|
+
files: files.map((f) => ({ rel: f.rel, abs: f.abs })),
|
|
206
|
+
});
|
|
207
|
+
} catch (err) {
|
|
208
|
+
deps.push({ repo: dep.repo, ref: dep.ref, error: err.message, files: [], count: 0 });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return { manifest: manifestPath, deps };
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// ─── AMXX standard includes ──────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
server.onRequest('amxmodx.includes.list', async (params) => {
|
|
217
|
+
const version = await resolveVersionFromParams(params);
|
|
218
|
+
const pattern = params?.pattern || '*.inc';
|
|
219
|
+
|
|
220
|
+
const { includeDir } = await fetchCompiler(version);
|
|
221
|
+
if (!includeDir) return { version, includeDir: null, pattern, count: 0, files: [] };
|
|
222
|
+
|
|
223
|
+
const files = await glob(pattern, { cwd: includeDir, dot: false });
|
|
224
|
+
files.sort();
|
|
225
|
+
return { version, includeDir, pattern, count: files.length, files };
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
server.onRequest('amxmodx.include.get', async (params) => {
|
|
229
|
+
const version = await resolveVersionFromParams(params);
|
|
230
|
+
const pattern = params?.file || params?.pattern || '*.inc';
|
|
231
|
+
|
|
232
|
+
const { includeDir } = await fetchCompiler(version);
|
|
233
|
+
if (!includeDir) return { version, includeDir: null, count: 0, files: [] };
|
|
234
|
+
|
|
235
|
+
const files = await glob(pattern, { cwd: includeDir, dot: false });
|
|
236
|
+
files.sort();
|
|
237
|
+
return {
|
|
238
|
+
version,
|
|
239
|
+
includeDir,
|
|
240
|
+
count: files.length,
|
|
241
|
+
files: files.map((rel) => ({ rel, content: readFileSafe(path.join(includeDir, rel)) })),
|
|
242
|
+
};
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// ─── Deps tree ───────────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
server.onRequest('deps.tree', async (params) => {
|
|
248
|
+
const depth = params?.depth || 0;
|
|
249
|
+
const noFetch = params?.noFetch === true;
|
|
250
|
+
|
|
251
|
+
if (params?.deps) {
|
|
252
|
+
const rootDeps = params.deps.map((entry) => {
|
|
253
|
+
if (typeof entry === 'string') {
|
|
254
|
+
const parsed = parseDepString(entry);
|
|
255
|
+
return { repo: parsed.repo, ref: parsed.ref, source: parsed.source, include_path: parsed.include_path, asset: parsed.asset };
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
repo: entry.repo,
|
|
259
|
+
ref: entry.ref,
|
|
260
|
+
source: entry.source || 'git',
|
|
261
|
+
include_path: entry.include_path || null,
|
|
262
|
+
asset: entry.asset != null ? entry.asset : null,
|
|
263
|
+
};
|
|
264
|
+
});
|
|
265
|
+
return buildDepTree(rootDeps, { token: params?.token, noFetch, depth, from: 'user' });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const manifestPath = manifestPathFor(params);
|
|
269
|
+
loadEnv(manifestPath);
|
|
270
|
+
const manifest = parseManifest(manifestPath);
|
|
271
|
+
const assembled = assembleRootDeps(manifest);
|
|
272
|
+
return buildDepTree(assembled.rootDeps, {
|
|
273
|
+
token: params?.token,
|
|
274
|
+
tokenFor: (repo) => resolveGithubToken(manifest, repo),
|
|
275
|
+
noFetch,
|
|
276
|
+
depth,
|
|
277
|
+
from: 'manifest',
|
|
278
|
+
getDepsOverride: assembled.getDepsOverride,
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// ─── Releases / cache / plan ─────────────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
server.onRequest('releases.list', async (params) => {
|
|
285
|
+
if (!params?.repo) {
|
|
286
|
+
const err = new Error('Missing required "repo" field');
|
|
287
|
+
err.code = -32602;
|
|
288
|
+
throw err;
|
|
289
|
+
}
|
|
290
|
+
const token = params?.token || process.env.GITHUB_TOKEN || null;
|
|
291
|
+
const limit = params?.limit || 10;
|
|
292
|
+
if (params?.tags) return listTags(params.repo, { token, limit });
|
|
293
|
+
return listReleases(params.repo, { token, limit, includeAssets: params?.includeAssets });
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
server.onRequest('cache.info', (params) => {
|
|
297
|
+
const manifestPath = params?.manifest ? path.resolve(params.manifest) : undefined;
|
|
298
|
+
return getCacheInfo(manifestPath);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
server.onRequest('build.plan', (params) => {
|
|
302
|
+
const manifestPath = manifestPathFor(params);
|
|
303
|
+
loadEnv(manifestPath, { quiet: true, override: false });
|
|
304
|
+
const manifest = resolveManifest(manifestPath, { set: params?.set, define: params?.define });
|
|
305
|
+
return buildPlanData(manifest, {
|
|
306
|
+
detailedAssets: params?.detailedAssets === true,
|
|
307
|
+
listLocal: params?.listLocal !== false,
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// ─── Build ───────────────────────────────────────────────────────────────
|
|
312
|
+
|
|
313
|
+
server.onRequest('build.start', async (params) => {
|
|
314
|
+
if (activeBuild) {
|
|
315
|
+
const err = new Error('Build already running');
|
|
316
|
+
err.code = -32000;
|
|
317
|
+
throw err;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const controller = new AbortController();
|
|
321
|
+
activeBuild = controller;
|
|
322
|
+
|
|
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
|
+
try {
|
|
340
|
+
const result = await runBuild(manifest, {
|
|
341
|
+
buildDir: params?.buildDir,
|
|
342
|
+
fetch: params?.fetch,
|
|
343
|
+
archive: params?.archive,
|
|
344
|
+
signal: controller.signal,
|
|
345
|
+
});
|
|
346
|
+
return result;
|
|
347
|
+
} catch (err) {
|
|
348
|
+
if (err.code === 'CANCELLED') {
|
|
349
|
+
return { ok: false, cancelled: true, message: err.message };
|
|
350
|
+
}
|
|
351
|
+
return { ok: false, message: err.message };
|
|
352
|
+
} finally {
|
|
353
|
+
for (const [ev, fn] of listeners) off(ev, fn);
|
|
354
|
+
activeBuild = null;
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
server.onRequest('build.cancel', () => {
|
|
359
|
+
if (!activeBuild) return { ok: false, error: 'No build running' };
|
|
360
|
+
activeBuild.abort();
|
|
361
|
+
return { ok: true };
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// ─── Single-file compile ─────────────────────────────────────────────────
|
|
365
|
+
|
|
366
|
+
server.onRequest('compile.single', async (params) => {
|
|
367
|
+
if (!params?.sma_file) {
|
|
368
|
+
const err = new Error('Missing required "sma_file" parameter');
|
|
369
|
+
err.code = -32602;
|
|
370
|
+
throw err;
|
|
371
|
+
}
|
|
372
|
+
const smaPath = path.resolve(params.sma_file);
|
|
373
|
+
if (!fs.existsSync(smaPath)) {
|
|
374
|
+
const err = new Error(`File not found: ${smaPath}`);
|
|
375
|
+
err.code = -32602;
|
|
376
|
+
throw err;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const noFetch = params?.noFetch === true;
|
|
380
|
+
|
|
381
|
+
const manifestPath = manifestPathFor(params);
|
|
382
|
+
let manifest = null;
|
|
383
|
+
if (fs.existsSync(manifestPath)) {
|
|
384
|
+
try { manifest = parseManifest(manifestPath); } catch { manifest = null; }
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const version = await resolveVersionFromParams(params);
|
|
388
|
+
const { compilerPath, includeDir } = await fetchCompiler(version);
|
|
389
|
+
|
|
390
|
+
// Dep includes come BEFORE the stdlib — matching the real build order.
|
|
391
|
+
const depDirs = [];
|
|
392
|
+
const depErrors = [];
|
|
393
|
+
if (manifest) {
|
|
394
|
+
for (const dep of manifest.globalDeps) {
|
|
395
|
+
try {
|
|
396
|
+
depDirs.push(await fetchDepIncludeDir(
|
|
397
|
+
dep, resolveGithubToken(manifest, dep.repo), noFetch, manifest.github.ssh
|
|
398
|
+
));
|
|
399
|
+
} catch (err) {
|
|
400
|
+
depErrors.push(`${dep.repo}@${dep.ref}: ${err.message}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const includeDirs = [...depDirs];
|
|
406
|
+
if (includeDir) includeDirs.push(includeDir);
|
|
407
|
+
for (const d of (params?.include_dirs || [])) includeDirs.push(path.resolve(d));
|
|
408
|
+
|
|
409
|
+
const buildDir = path.join(os.tmpdir(), 'amxb-serve-compile');
|
|
410
|
+
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
|
+
|
|
420
|
+
return {
|
|
421
|
+
ok: amxxName != null,
|
|
422
|
+
amxxName,
|
|
423
|
+
output_path: amxxName ? path.join(buildDir, 'amxmodx', 'plugins', amxxName) : null,
|
|
424
|
+
dep_errors: depErrors.length ? depErrors : undefined,
|
|
425
|
+
};
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// ─── Watch ───────────────────────────────────────────────────────────────
|
|
429
|
+
|
|
430
|
+
server.onRequest('watch.start', (params) => {
|
|
431
|
+
if (activeWatcher) {
|
|
432
|
+
const err = new Error('Watch already running');
|
|
433
|
+
err.code = -32000;
|
|
434
|
+
throw err;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const manifestPath = manifestPathFor(params);
|
|
438
|
+
const manifest = parseManifest(manifestPath);
|
|
439
|
+
|
|
440
|
+
const notify = (kind, extra = {}) => server.notify('watch.changed', { kind, ...extra });
|
|
441
|
+
const watcher = startWatch(manifest, manifestPath, {
|
|
442
|
+
onSmaChange: (p) => notify('sma', { path: p }),
|
|
443
|
+
onIncChange: (p) => notify('inc', { path: p }),
|
|
444
|
+
onFileChange: (rel, section) => notify('file', { rel, section }),
|
|
445
|
+
onFileDelete: (rel, section) => notify('delete', { rel, section }),
|
|
446
|
+
onManifestChange: () => notify('manifest'),
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
activeWatcher = watcher;
|
|
450
|
+
return { ok: true, watching: manifestPath };
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
server.onRequest('watch.stop', async () => {
|
|
454
|
+
if (!activeWatcher) return { ok: false, error: 'No watcher running' };
|
|
455
|
+
await activeWatcher.close();
|
|
456
|
+
activeWatcher = null;
|
|
457
|
+
return { ok: true };
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
return server;
|
|
461
|
+
}
|
|
462
|
+
|
|
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
|
+
dotenv.config({ path: path.join(process.cwd(), '.env'), quiet: true });
|
|
467
|
+
logger.setStderr(true);
|
|
468
|
+
progress.setEnabled(false);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Start the serve server — listens on stdin/stdout forever.
|
|
473
|
+
*/
|
|
474
|
+
async function runServe() {
|
|
475
|
+
prepareEnvironment();
|
|
476
|
+
const server = createServeServer();
|
|
477
|
+
await server.connect();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
module.exports = { runServe, createServeServer };
|
|
481
|
+
|
|
482
|
+
// ─── Direct execution guard ───────────────────────────────────────────────────
|
|
483
|
+
|
|
484
|
+
if (require.main === module) {
|
|
485
|
+
runServe().catch((err) => {
|
|
486
|
+
process.stderr.write(`Fatal serve error: ${err && err.message ? err.message : String(err)}\n`);
|
|
487
|
+
process.exit(1);
|
|
488
|
+
});
|
|
489
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const { loadEnv } = require('../env');
|
|
6
|
+
const { resolveManifestPath: resolveManifestPathCore } = require('../manifest-path');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolve manifest file path from explicit arg or auto-detection.
|
|
10
|
+
*
|
|
11
|
+
* CLI wrapper around the core src/manifest-path.js: returns a plain string
|
|
12
|
+
* (absolute path) and renders the CLI-only 'manifest.yml is deprecated'
|
|
13
|
+
* warning. All discovery logic lives in the core.
|
|
14
|
+
*/
|
|
15
|
+
function resolveManifestPath(explicit) {
|
|
16
|
+
const { path: manifestPath, usedDefault } = resolveManifestPathCore(explicit);
|
|
17
|
+
if (usedDefault && path.basename(manifestPath) === 'manifest.yml') {
|
|
18
|
+
const logger = require('../logger');
|
|
19
|
+
logger.warn('manifest.yml is deprecated — rename it to amxbuild.yml');
|
|
20
|
+
}
|
|
21
|
+
return manifestPath;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { resolveManifestPath, loadEnv };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const logger = require('../logger');
|
|
6
|
+
const { validateManifestFile } = require('../validate');
|
|
7
|
+
const { resolveManifestPath } = require('./shared');
|
|
8
|
+
|
|
9
|
+
async function runValidate(options) {
|
|
10
|
+
const manifestPath = options.manifest ? path.resolve(options.manifest) : resolveManifestPath(undefined);
|
|
11
|
+
const result = validateManifestFile(manifestPath);
|
|
12
|
+
|
|
13
|
+
if (options.json) {
|
|
14
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
15
|
+
if (!result.valid) process.exitCode = 1;
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (result.valid) {
|
|
20
|
+
logger.success('Manifest is valid');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
logger.error(`Manifest has ${result.errors.length} error(s) and ${result.warnings.length} warning(s):`);
|
|
25
|
+
for (const err of result.errors) {
|
|
26
|
+
logger.dim(` ${err.path}: ${err.message}`);
|
|
27
|
+
}
|
|
28
|
+
for (const warn of result.warnings) {
|
|
29
|
+
logger.warn(` ${warn.path}: ${warn.message}`);
|
|
30
|
+
}
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { runValidate };
|