amxx-builder 1.5.2 → 1.6.0
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 +6 -0
- package/README.md +102 -2
- package/mcp/handlers.js +163 -26
- package/mcp/registry.js +125 -0
- package/package.json +2 -1
- package/skills/amxb-migration/SKILL.md +554 -0
- package/src/build-plan.js +3 -1
- package/src/cli.js +1 -0
- package/src/commands/deps-tree.js +4 -1
- package/src/commands/dry-run.js +4 -0
- package/src/commands/init.js +19 -18
- package/src/commands/serve.js +338 -25
- package/src/compiler-fetcher.js +34 -1
- package/src/dep-docs.js +115 -0
- package/src/dep-graph.js +23 -0
- package/src/deployer.js +11 -6
- package/src/deps-resolver.js +54 -3
- package/src/deps-tree.js +18 -0
- package/src/fungun-fetcher.js +347 -0
- package/src/github-api.js +309 -0
- package/src/include-tree.js +7 -2
- package/src/jsonrpc-transport.js +2 -1
- package/src/manifest.js +86 -5
- package/templates/init-workflow.yml +1 -1
package/src/commands/init.js
CHANGED
|
@@ -80,28 +80,28 @@ async function runInitInteractive(options) {
|
|
|
80
80
|
const version = require('../../package.json').version;
|
|
81
81
|
const actionTag = `v${version.split('.')[0]}`;
|
|
82
82
|
|
|
83
|
-
writeIfAbsent('amxbuild.yml', renderTemplate('init-manifest.yml', { name, schemaUrl: SCHEMA_URL }));
|
|
83
|
+
writeIfAbsent('amxbuild.yml', renderTemplate('init-manifest.yml', { name, schemaUrl: SCHEMA_URL }), options.force);
|
|
84
84
|
|
|
85
85
|
if (doWorkflow) {
|
|
86
86
|
const dest = path.join('.github', 'workflows', 'ci.yml');
|
|
87
87
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
88
|
-
writeIfAbsent(dest, renderTemplate('init-workflow.yml', { actionTag }));
|
|
88
|
+
writeIfAbsent(dest, renderTemplate('init-workflow.yml', { actionTag }), options.force);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
if (pluginName) {
|
|
92
92
|
const dest = path.join('amxmodx', 'scripting', `${pluginName}.sma`);
|
|
93
93
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
94
|
-
writeIfAbsent(dest, '');
|
|
94
|
+
writeIfAbsent(dest, '', options.force);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
if (doGitignore) {
|
|
98
98
|
writeIfAbsent('.gitignore', [
|
|
99
99
|
'*.amxx', '*.zip', '.env', '.amxb-cache', '.claude', 'build', 'dist', '',
|
|
100
|
-
].join('\n'));
|
|
100
|
+
].join('\n'), options.force);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
if (doDeploy) {
|
|
104
|
-
writeIfAbsent('.env', renderTemplate('init-deploy.env'));
|
|
104
|
+
writeIfAbsent('.env', renderTemplate('init-deploy.env'), options.force);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
if (doOpencode) {
|
|
@@ -109,7 +109,7 @@ async function runInitInteractive(options) {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
if (doScript) {
|
|
112
|
-
writeBuildScripts();
|
|
112
|
+
writeBuildScripts(options.force);
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
|
|
@@ -118,28 +118,28 @@ function runInit(options) {
|
|
|
118
118
|
const version = require('../../package.json').version;
|
|
119
119
|
const actionTag = `v${version.split('.')[0]}`;
|
|
120
120
|
|
|
121
|
-
writeIfAbsent('amxbuild.yml', renderTemplate('init-manifest.yml', { name: pkgName, schemaUrl: SCHEMA_URL }));
|
|
121
|
+
writeIfAbsent('amxbuild.yml', renderTemplate('init-manifest.yml', { name: pkgName, schemaUrl: SCHEMA_URL }), options.force);
|
|
122
122
|
|
|
123
123
|
if (options.workflow || options.ci) {
|
|
124
124
|
const dest = path.join('.github', 'workflows', 'ci.yml');
|
|
125
125
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
126
|
-
writeIfAbsent(dest, renderTemplate('init-workflow.yml', { actionTag }));
|
|
126
|
+
writeIfAbsent(dest, renderTemplate('init-workflow.yml', { actionTag }), options.force);
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
if (options.plugin) {
|
|
130
130
|
const dest = path.join('amxmodx', 'scripting', `${options.plugin}.sma`);
|
|
131
131
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
132
|
-
writeIfAbsent(dest, '');
|
|
132
|
+
writeIfAbsent(dest, '', options.force);
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
if (options.gitignore) {
|
|
136
136
|
writeIfAbsent('.gitignore', [
|
|
137
137
|
'*.amxx', '*.zip', '.env', '.amxb-cache', '.claude', 'build', 'dist', '',
|
|
138
|
-
].join('\n'));
|
|
138
|
+
].join('\n'), options.force);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
if (options.deploy) {
|
|
142
|
-
writeIfAbsent('.env', renderTemplate('init-deploy.env'));
|
|
142
|
+
writeIfAbsent('.env', renderTemplate('init-deploy.env'), options.force);
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
if (options.opencode) {
|
|
@@ -147,13 +147,13 @@ function runInit(options) {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
if (options.script) {
|
|
150
|
-
writeBuildScripts();
|
|
150
|
+
writeBuildScripts(options.force);
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
function writeBuildScripts() {
|
|
155
|
-
const batCreated = writeIfAbsent('build.bat', renderTemplate('init-build.bat').replace(/\r?\n/g, '\r\n'));
|
|
156
|
-
const shCreated = writeIfAbsent('build.sh', renderTemplate('init-build.sh'));
|
|
154
|
+
function writeBuildScripts(force) {
|
|
155
|
+
const batCreated = writeIfAbsent('build.bat', renderTemplate('init-build.bat').replace(/\r?\n/g, '\r\n'), force);
|
|
156
|
+
const shCreated = writeIfAbsent('build.sh', renderTemplate('init-build.sh'), force);
|
|
157
157
|
|
|
158
158
|
if (shCreated && process.platform !== 'win32') {
|
|
159
159
|
try {
|
|
@@ -167,13 +167,14 @@ function writeBuildScripts() {
|
|
|
167
167
|
return batCreated || shCreated;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
function writeIfAbsent(filePath, content) {
|
|
171
|
-
|
|
170
|
+
function writeIfAbsent(filePath, content, force) {
|
|
171
|
+
const existed = fs.existsSync(filePath);
|
|
172
|
+
if (existed && !force) {
|
|
172
173
|
logger.warn(`${filePath} already exists, skipping`);
|
|
173
174
|
return false;
|
|
174
175
|
}
|
|
175
176
|
fs.writeFileSync(filePath, content);
|
|
176
|
-
logger.success(`Created ${filePath}`);
|
|
177
|
+
logger.success(existed ? `Overwritten ${filePath}` : `Created ${filePath}`);
|
|
177
178
|
return true;
|
|
178
179
|
}
|
|
179
180
|
|
package/src/commands/serve.js
CHANGED
|
@@ -22,12 +22,20 @@
|
|
|
22
22
|
* amxmodx.include.get → compiler-fetcher fetchCompiler + glob + read
|
|
23
23
|
* deps.tree → deps-tree buildDepTree + assembleRootDeps
|
|
24
24
|
* releases.list → release-lister listReleases / listTags
|
|
25
|
+
* repos.info → github-api getRepoInfo
|
|
26
|
+
* repos.branches → github-api listBranches
|
|
27
|
+
* repos.structure → github-api getRepoStructure
|
|
25
28
|
* cache.info → cache-info getCacheInfo
|
|
29
|
+
* compiler.info → compiler-fetcher getCompilerInfo
|
|
30
|
+
* dep-graph.get → dep-graph DepGraph
|
|
26
31
|
* build.plan → build-plan buildPlanData
|
|
27
32
|
* build.start → build-service runBuild (+ event notifications)
|
|
28
33
|
* build.cancel → abort the running build (AbortController)
|
|
29
|
-
* compile.single → compiler.compileSingle
|
|
34
|
+
* compile.single → compiler.compileSingle (+ captured output)
|
|
35
|
+
* deploy.start / deploy.file / deploy.remove → deployer deployBuild/deployFile/removeDeployedFile
|
|
36
|
+
* rcon.send → rcon sendRcon
|
|
30
37
|
* watch.start / watch.stop → watcher.startWatch (+ watch.changed notifications)
|
|
38
|
+
* serve.ping → process info (health check)
|
|
31
39
|
*/
|
|
32
40
|
|
|
33
41
|
const fs = require('fs');
|
|
@@ -46,14 +54,23 @@ const { resolveManifestPath } = require('../manifest-path');
|
|
|
46
54
|
const { resolveManifest, parseManifest, resolveGithubToken, parseDepString } = require('../manifest');
|
|
47
55
|
const { validateManifestFile } = require('../validate');
|
|
48
56
|
const { fetchDepIncludeDir, collectIncFiles, parseIncludeDirective, searchIncludeFile } = require('../include-tree');
|
|
49
|
-
const {
|
|
57
|
+
const { depLabel } = require('../deps-resolver');
|
|
58
|
+
const { fetchCompiler, resolveAmxmodxVersion, getCompilerInfo } = require('../compiler-fetcher');
|
|
50
59
|
const { buildDepTree, assembleRootDeps } = require('../deps-tree');
|
|
51
60
|
const { listReleases, listTags } = require('../release-lister');
|
|
61
|
+
const {
|
|
62
|
+
getRepoInfo, listBranches, getRepoStructure,
|
|
63
|
+
GithubError, isValidRepo, validateRepoStructureOptions,
|
|
64
|
+
} = require('../github-api');
|
|
52
65
|
const { getCacheInfo } = require('../cache-info');
|
|
53
66
|
const { buildPlanData } = require('../build-plan');
|
|
54
67
|
const { runBuild } = require('../build-service');
|
|
55
68
|
const { compileSingle } = require('../compiler');
|
|
69
|
+
const { deployBuild, deployFile, removeDeployedFile } = require('../deployer');
|
|
70
|
+
const { sendRcon } = require('../rcon');
|
|
71
|
+
const { DepGraph } = require('../dep-graph');
|
|
56
72
|
const { startWatch } = require('../watcher');
|
|
73
|
+
const pkg = require('../../package.json');
|
|
57
74
|
|
|
58
75
|
// ─── Small interface helpers (no domain logic) ────────────────────────────────
|
|
59
76
|
|
|
@@ -86,6 +103,69 @@ function manifestPathFor(params) {
|
|
|
86
103
|
return params?.manifest ? path.resolve(params.manifest) : resolveManifestPath().path;
|
|
87
104
|
}
|
|
88
105
|
|
|
106
|
+
/**
|
|
107
|
+
* GitHub token for repo-scope methods. Manifest tokens win: the .env next to
|
|
108
|
+
* the manifest is loaded first (it overrides the cwd .env loaded at startup —
|
|
109
|
+
* per the env convention: manifest .env is primary, cwd is the fallback).
|
|
110
|
+
* Then the explicit `token` param, then the process env, then anonymous.
|
|
111
|
+
*/
|
|
112
|
+
function resolveGithubTokenFor(params, repo) {
|
|
113
|
+
const manifestPath = manifestPathFor(params);
|
|
114
|
+
if (fs.existsSync(manifestPath)) {
|
|
115
|
+
loadEnvQuiet(manifestPath);
|
|
116
|
+
try {
|
|
117
|
+
const fromManifest = resolveGithubToken(parseManifest(manifestPath), repo);
|
|
118
|
+
if (fromManifest) return fromManifest;
|
|
119
|
+
} catch { /* unparseable manifest — fall back to explicit/env token */ }
|
|
120
|
+
}
|
|
121
|
+
return params?.token || process.env.GITHUB_TOKEN || null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Shape a GitHub API error into the JSON-RPC error contract: -32603 with
|
|
126
|
+
* error.data = { status, repo, message }. Returns null for non-GitHub errors
|
|
127
|
+
* so the caller rethrows them unchanged.
|
|
128
|
+
*/
|
|
129
|
+
function githubRpcError(err, repo) {
|
|
130
|
+
if (err instanceof GithubError) {
|
|
131
|
+
err.code = -32603;
|
|
132
|
+
err.data = { status: err.status, repo, message: err.message };
|
|
133
|
+
return err;
|
|
134
|
+
}
|
|
135
|
+
if (err && err.response && typeof err.response.status === 'number') {
|
|
136
|
+
// Raw axios error (release-lister path) — normalize to the same contract.
|
|
137
|
+
const message = (err.response.data && err.response.data.message) || err.message;
|
|
138
|
+
const shaped = new Error(message);
|
|
139
|
+
shaped.code = -32603;
|
|
140
|
+
shaped.data = { status: err.response.status, repo, message };
|
|
141
|
+
return shaped;
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function repoParamError(params) {
|
|
147
|
+
if (!params?.repo) return 'Missing required "repo" field';
|
|
148
|
+
if (!isValidRepo(params.repo)) return 'Invalid "repo": expected "owner/repo"';
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// dotenv@17 prints an "injected env" line to stdout by default, which would
|
|
153
|
+
// break the pure-JSON-RPC stdout contract — always load quietly here.
|
|
154
|
+
function loadEnvQuiet(manifestPath) {
|
|
155
|
+
loadEnv(manifestPath, { quiet: true });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Full manifest (defaults merged, set/define applied) for deploy methods.
|
|
159
|
+
function deployRequestManifest(params) {
|
|
160
|
+
const manifestPath = manifestPathFor(params);
|
|
161
|
+
loadEnvQuiet(manifestPath);
|
|
162
|
+
return resolveManifest(manifestPath, { set: params?.set, define: params?.define });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function buildDirFor(params) {
|
|
166
|
+
return params?.buildDir ? path.resolve(params.buildDir) : path.join(process.cwd(), 'build');
|
|
167
|
+
}
|
|
168
|
+
|
|
89
169
|
/**
|
|
90
170
|
* Create and configure the JSON-RPC server with all methods wired to core.
|
|
91
171
|
* Does NOT connect or set up the environment — call runServe() for that.
|
|
@@ -97,6 +177,15 @@ function createServeServer() {
|
|
|
97
177
|
let activeBuild = null; // AbortController for the running build
|
|
98
178
|
let activeWatcher = null; // chokidar watcher instance
|
|
99
179
|
|
|
180
|
+
// ─── Health check ────────────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
server.onRequest('serve.ping', () => ({
|
|
183
|
+
ok: true,
|
|
184
|
+
pid: process.pid,
|
|
185
|
+
version: pkg.version,
|
|
186
|
+
node: process.version,
|
|
187
|
+
}));
|
|
188
|
+
|
|
100
189
|
// ─── Read-only: manifest ──────────────────────────────────────────────────
|
|
101
190
|
|
|
102
191
|
server.onRequest('manifest.validate', (params) => {
|
|
@@ -105,7 +194,7 @@ function createServeServer() {
|
|
|
105
194
|
|
|
106
195
|
server.onRequest('manifest.resolve', (params) => {
|
|
107
196
|
const manifestPath = manifestPathFor(params);
|
|
108
|
-
|
|
197
|
+
loadEnvQuiet(manifestPath);
|
|
109
198
|
return resolveManifest(manifestPath, { set: params?.set, define: params?.define });
|
|
110
199
|
});
|
|
111
200
|
|
|
@@ -138,6 +227,7 @@ function createServeServer() {
|
|
|
138
227
|
let manifest = null;
|
|
139
228
|
const manifestPath = manifestPathFor(params);
|
|
140
229
|
if (fs.existsSync(manifestPath)) {
|
|
230
|
+
loadEnvQuiet(manifestPath);
|
|
141
231
|
try {
|
|
142
232
|
manifest = parseManifest(manifestPath);
|
|
143
233
|
for (const dep of manifest.globalDeps) {
|
|
@@ -146,9 +236,9 @@ function createServeServer() {
|
|
|
146
236
|
dep, resolveGithubToken(manifest, dep.repo),
|
|
147
237
|
params?.noFetch === true, manifest.github.ssh
|
|
148
238
|
);
|
|
149
|
-
searchPaths.push({ path: depDir, label:
|
|
239
|
+
searchPaths.push({ path: depDir, label: depLabel(dep) });
|
|
150
240
|
} catch (err) {
|
|
151
|
-
errors.push(`${dep
|
|
241
|
+
errors.push(`${depLabel(dep)}: ${err.message}`);
|
|
152
242
|
}
|
|
153
243
|
}
|
|
154
244
|
} catch (err) {
|
|
@@ -186,10 +276,14 @@ function createServeServer() {
|
|
|
186
276
|
err.code = -32602;
|
|
187
277
|
throw err;
|
|
188
278
|
}
|
|
279
|
+
loadEnvQuiet(manifestPath);
|
|
189
280
|
const manifest = parseManifest(manifestPath);
|
|
190
281
|
|
|
191
282
|
const deps = [];
|
|
192
283
|
for (const dep of manifest.globalDeps) {
|
|
284
|
+
const base = dep.source === 'fungun'
|
|
285
|
+
? { source: 'fungun', id: dep.id, url: dep.url }
|
|
286
|
+
: { source: dep.source || 'git', repo: dep.repo, ref: dep.ref };
|
|
193
287
|
try {
|
|
194
288
|
const includeDir = await fetchDepIncludeDir(
|
|
195
289
|
dep, resolveGithubToken(manifest, dep.repo),
|
|
@@ -197,15 +291,14 @@ function createServeServer() {
|
|
|
197
291
|
);
|
|
198
292
|
const files = await collectIncFiles(includeDir);
|
|
199
293
|
deps.push({
|
|
200
|
-
|
|
201
|
-
ref: dep.ref,
|
|
294
|
+
...base,
|
|
202
295
|
include_path: dep.include_path || null,
|
|
203
296
|
include_dir: includeDir,
|
|
204
297
|
count: files.length,
|
|
205
298
|
files: files.map((f) => ({ rel: f.rel, abs: f.abs })),
|
|
206
299
|
});
|
|
207
300
|
} catch (err) {
|
|
208
|
-
deps.push({
|
|
301
|
+
deps.push({ ...base, error: err.message, files: [], count: 0 });
|
|
209
302
|
}
|
|
210
303
|
}
|
|
211
304
|
return { manifest: manifestPath, deps };
|
|
@@ -266,7 +359,7 @@ function createServeServer() {
|
|
|
266
359
|
}
|
|
267
360
|
|
|
268
361
|
const manifestPath = manifestPathFor(params);
|
|
269
|
-
|
|
362
|
+
loadEnvQuiet(manifestPath);
|
|
270
363
|
const manifest = parseManifest(manifestPath);
|
|
271
364
|
const assembled = assembleRootDeps(manifest);
|
|
272
365
|
return buildDepTree(assembled.rootDeps, {
|
|
@@ -279,18 +372,141 @@ function createServeServer() {
|
|
|
279
372
|
});
|
|
280
373
|
});
|
|
281
374
|
|
|
282
|
-
// ───
|
|
375
|
+
// ─── Include dependency graph ────────────────────────────────────────────
|
|
376
|
+
|
|
377
|
+
server.onRequest('dep-graph.get', async (params) => {
|
|
378
|
+
if (!params?.sma_file) {
|
|
379
|
+
const err = new Error('Missing required "sma_file" parameter');
|
|
380
|
+
err.code = -32602;
|
|
381
|
+
throw err;
|
|
382
|
+
}
|
|
383
|
+
const smaPath = path.resolve(params.sma_file);
|
|
384
|
+
if (!fs.existsSync(smaPath)) {
|
|
385
|
+
const err = new Error(`File not found: ${smaPath}`);
|
|
386
|
+
err.code = -32602;
|
|
387
|
+
throw err;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const noFetch = params?.noFetch === true;
|
|
391
|
+
|
|
392
|
+
const manifestPath = manifestPathFor(params);
|
|
393
|
+
let manifest = null;
|
|
394
|
+
if (fs.existsSync(manifestPath)) {
|
|
395
|
+
loadEnvQuiet(manifestPath);
|
|
396
|
+
try { manifest = parseManifest(manifestPath); } catch { manifest = null; }
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const version = await resolveVersionFromParams(params);
|
|
400
|
+
const { includeDir } = await fetchCompiler(version);
|
|
401
|
+
|
|
402
|
+
// Dep includes come BEFORE the stdlib — matching the real build's search order.
|
|
403
|
+
const includeDirs = [];
|
|
404
|
+
if (manifest) {
|
|
405
|
+
for (const dep of manifest.globalDeps) {
|
|
406
|
+
try {
|
|
407
|
+
includeDirs.push(await fetchDepIncludeDir(
|
|
408
|
+
dep, resolveGithubToken(manifest, dep.repo), noFetch, manifest.github.ssh
|
|
409
|
+
));
|
|
410
|
+
} catch (err) { /* keep the rest of the dirs on partial failure */ }
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (includeDir) includeDirs.push(includeDir);
|
|
414
|
+
for (const d of (params?.include_dirs || [])) includeDirs.push(path.resolve(d));
|
|
415
|
+
|
|
416
|
+
const graph = new DepGraph(includeDirs);
|
|
417
|
+
graph.parseFile(smaPath);
|
|
418
|
+
|
|
419
|
+
const result = {
|
|
420
|
+
sma_file: smaPath,
|
|
421
|
+
version,
|
|
422
|
+
include_dirs: includeDirs,
|
|
423
|
+
...graph.snapshot(),
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
// Reverse query: which .sma files transitively depend on this .inc?
|
|
427
|
+
if (params?.inc) {
|
|
428
|
+
const incAbs = path.resolve(params.inc);
|
|
429
|
+
result.smas_depending_on = [...graph.getSmasDependingOn(incAbs)].sort();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return result;
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// ─── Releases / repos / cache / plan ─────────────────────────────────────
|
|
283
436
|
|
|
284
437
|
server.onRequest('releases.list', async (params) => {
|
|
285
|
-
|
|
286
|
-
|
|
438
|
+
const invalid = repoParamError(params);
|
|
439
|
+
if (invalid) {
|
|
440
|
+
const err = new Error(invalid);
|
|
287
441
|
err.code = -32602;
|
|
288
442
|
throw err;
|
|
289
443
|
}
|
|
290
|
-
const
|
|
444
|
+
const repo = params.repo;
|
|
445
|
+
const token = resolveGithubTokenFor(params, repo);
|
|
291
446
|
const limit = params?.limit || 10;
|
|
292
|
-
|
|
293
|
-
|
|
447
|
+
try {
|
|
448
|
+
if (params?.tags) return await listTags(repo, { token, limit });
|
|
449
|
+
return await listReleases(repo, { token, limit, includeAssets: params?.includeAssets });
|
|
450
|
+
} catch (err) {
|
|
451
|
+
throw githubRpcError(err, repo) || err;
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
server.onRequest('repos.info', async (params) => {
|
|
456
|
+
const invalid = repoParamError(params);
|
|
457
|
+
if (invalid) {
|
|
458
|
+
const err = new Error(invalid);
|
|
459
|
+
err.code = -32602;
|
|
460
|
+
throw err;
|
|
461
|
+
}
|
|
462
|
+
const repo = params.repo;
|
|
463
|
+
const token = resolveGithubTokenFor(params, repo);
|
|
464
|
+
try {
|
|
465
|
+
return await getRepoInfo(repo, { token });
|
|
466
|
+
} catch (err) {
|
|
467
|
+
throw githubRpcError(err, repo) || err;
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
server.onRequest('repos.branches', async (params) => {
|
|
472
|
+
const invalid = repoParamError(params);
|
|
473
|
+
if (invalid) {
|
|
474
|
+
const err = new Error(invalid);
|
|
475
|
+
err.code = -32602;
|
|
476
|
+
throw err;
|
|
477
|
+
}
|
|
478
|
+
const repo = params.repo;
|
|
479
|
+
const token = resolveGithubTokenFor(params, repo);
|
|
480
|
+
const limit = Math.min(100, Math.max(1, params?.limit ?? 10));
|
|
481
|
+
const page = Math.max(1, params?.page ?? 1);
|
|
482
|
+
try {
|
|
483
|
+
return await listBranches(repo, { token, limit, page });
|
|
484
|
+
} catch (err) {
|
|
485
|
+
throw githubRpcError(err, repo) || err;
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
server.onRequest('repos.structure', async (params) => {
|
|
490
|
+
const invalid = repoParamError(params) || validateRepoStructureOptions(params);
|
|
491
|
+
if (invalid) {
|
|
492
|
+
const err = new Error(invalid);
|
|
493
|
+
err.code = -32602;
|
|
494
|
+
throw err;
|
|
495
|
+
}
|
|
496
|
+
const repo = params.repo;
|
|
497
|
+
const token = resolveGithubTokenFor(params, repo);
|
|
498
|
+
try {
|
|
499
|
+
return await getRepoStructure(repo, {
|
|
500
|
+
token,
|
|
501
|
+
ref: params?.ref || null,
|
|
502
|
+
depth: params?.depth,
|
|
503
|
+
dirsOnly: params?.dirsOnly === true,
|
|
504
|
+
ext: params?.ext,
|
|
505
|
+
maxEntries: params?.maxEntries,
|
|
506
|
+
});
|
|
507
|
+
} catch (err) {
|
|
508
|
+
throw githubRpcError(err, repo) || err;
|
|
509
|
+
}
|
|
294
510
|
});
|
|
295
511
|
|
|
296
512
|
server.onRequest('cache.info', (params) => {
|
|
@@ -298,6 +514,11 @@ function createServeServer() {
|
|
|
298
514
|
return getCacheInfo(manifestPath);
|
|
299
515
|
});
|
|
300
516
|
|
|
517
|
+
server.onRequest('compiler.info', async (params) => {
|
|
518
|
+
const version = await resolveVersionFromParams(params);
|
|
519
|
+
return getCompilerInfo(version, { noFetch: params?.noFetch === true });
|
|
520
|
+
});
|
|
521
|
+
|
|
301
522
|
server.onRequest('build.plan', (params) => {
|
|
302
523
|
const manifestPath = manifestPathFor(params);
|
|
303
524
|
loadEnv(manifestPath, { quiet: true, override: false });
|
|
@@ -321,7 +542,7 @@ function createServeServer() {
|
|
|
321
542
|
activeBuild = controller;
|
|
322
543
|
|
|
323
544
|
const manifestPath = manifestPathFor(params);
|
|
324
|
-
|
|
545
|
+
loadEnvQuiet(manifestPath);
|
|
325
546
|
const manifest = resolveManifest(manifestPath, { set: params?.set, define: params?.define });
|
|
326
547
|
|
|
327
548
|
// Forward core lifecycle events as server→client notifications while the
|
|
@@ -361,6 +582,85 @@ function createServeServer() {
|
|
|
361
582
|
return { ok: true };
|
|
362
583
|
});
|
|
363
584
|
|
|
585
|
+
// ─── Deploy ──────────────────────────────────────────────────────────────
|
|
586
|
+
|
|
587
|
+
server.onRequest('deploy.start', async (params) => {
|
|
588
|
+
const manifest = deployRequestManifest(params);
|
|
589
|
+
if (!manifest.deploy || !manifest.deploy.path) {
|
|
590
|
+
return { ok: false, message: 'Deploy path not configured.\n → Set AMXB_DEPLOY_PATH in .env, or add deploy.path to your manifest' };
|
|
591
|
+
}
|
|
592
|
+
const copied = await deployBuild(manifest, buildDirFor(params), {
|
|
593
|
+
incremental: params?.incremental === true,
|
|
594
|
+
});
|
|
595
|
+
return { ok: true, copied };
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
server.onRequest('deploy.file', (params) => {
|
|
599
|
+
if (!params?.relPath) {
|
|
600
|
+
const err = new Error('Missing required "relPath" parameter');
|
|
601
|
+
err.code = -32602;
|
|
602
|
+
throw err;
|
|
603
|
+
}
|
|
604
|
+
const section = params?.section === 'assets' ? 'assets' : 'amxmodx';
|
|
605
|
+
const manifest = deployRequestManifest(params);
|
|
606
|
+
if (!manifest.deploy || !manifest.deploy.path) {
|
|
607
|
+
return { ok: false, message: 'Deploy path not configured' };
|
|
608
|
+
}
|
|
609
|
+
const dest = deployFile(manifest, buildDirFor(params), params.relPath, section);
|
|
610
|
+
return { ok: dest != null, dest: dest || null };
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
server.onRequest('deploy.remove', (params) => {
|
|
614
|
+
if (!params?.relPath) {
|
|
615
|
+
const err = new Error('Missing required "relPath" parameter');
|
|
616
|
+
err.code = -32602;
|
|
617
|
+
throw err;
|
|
618
|
+
}
|
|
619
|
+
const section = params?.section === 'assets' ? 'assets' : 'amxmodx';
|
|
620
|
+
const manifest = deployRequestManifest(params);
|
|
621
|
+
if (!manifest.deploy || !manifest.deploy.path) {
|
|
622
|
+
return { ok: false, message: 'Deploy path not configured' };
|
|
623
|
+
}
|
|
624
|
+
const dest = removeDeployedFile(manifest, buildDirFor(params), params.relPath, section);
|
|
625
|
+
return { ok: dest != null, dest: dest || null };
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
// ─── RCON ────────────────────────────────────────────────────────────────
|
|
629
|
+
|
|
630
|
+
server.onRequest('rcon.send', async (params) => {
|
|
631
|
+
if (!params?.command) {
|
|
632
|
+
const err = new Error('Missing required "command" parameter');
|
|
633
|
+
err.code = -32602;
|
|
634
|
+
throw err;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
let host = params?.host;
|
|
638
|
+
let port = params?.port;
|
|
639
|
+
let password = params?.password;
|
|
640
|
+
|
|
641
|
+
// Fall back to the manifest's deploy.rcon config when host/password are omitted.
|
|
642
|
+
if ((host == null || password == null) && fs.existsSync(manifestPathFor(params))) {
|
|
643
|
+
try {
|
|
644
|
+
const rconCfg = parseManifest(manifestPathFor(params)).deploy?.rcon;
|
|
645
|
+
if (rconCfg) {
|
|
646
|
+
if (host == null) host = rconCfg.host;
|
|
647
|
+
if (port == null) port = rconCfg.port;
|
|
648
|
+
if (password == null) password = rconCfg.password;
|
|
649
|
+
}
|
|
650
|
+
} catch { /* unparseable manifest — explicit params only */ }
|
|
651
|
+
}
|
|
652
|
+
if (port == null) port = 27015;
|
|
653
|
+
|
|
654
|
+
if (!host || !password) {
|
|
655
|
+
const err = new Error('RCON host/password not provided (pass explicitly or configure deploy.rcon in the manifest)');
|
|
656
|
+
err.code = -32602;
|
|
657
|
+
throw err;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const response = await sendRcon({ host, port, password, command: params.command });
|
|
661
|
+
return { ok: true, response };
|
|
662
|
+
});
|
|
663
|
+
|
|
364
664
|
// ─── Single-file compile ─────────────────────────────────────────────────
|
|
365
665
|
|
|
366
666
|
server.onRequest('compile.single', async (params) => {
|
|
@@ -381,6 +681,7 @@ function createServeServer() {
|
|
|
381
681
|
const manifestPath = manifestPathFor(params);
|
|
382
682
|
let manifest = null;
|
|
383
683
|
if (fs.existsSync(manifestPath)) {
|
|
684
|
+
loadEnvQuiet(manifestPath);
|
|
384
685
|
try { manifest = parseManifest(manifestPath); } catch { manifest = null; }
|
|
385
686
|
}
|
|
386
687
|
|
|
@@ -397,7 +698,7 @@ function createServeServer() {
|
|
|
397
698
|
dep, resolveGithubToken(manifest, dep.repo), noFetch, manifest.github.ssh
|
|
398
699
|
));
|
|
399
700
|
} catch (err) {
|
|
400
|
-
depErrors.push(`${dep
|
|
701
|
+
depErrors.push(`${depLabel(dep)}: ${err.message}`);
|
|
401
702
|
}
|
|
402
703
|
}
|
|
403
704
|
}
|
|
@@ -408,18 +709,30 @@ function createServeServer() {
|
|
|
408
709
|
|
|
409
710
|
const buildDir = path.join(os.tmpdir(), 'amxb-serve-compile');
|
|
410
711
|
const compileManifest = manifest || { amxmodx: { defines: [] } };
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
712
|
+
|
|
713
|
+
// Capture the compiler output emitted as an event by compileSingle.
|
|
714
|
+
const baseName = path.basename(smaPath);
|
|
715
|
+
let compiled = null;
|
|
716
|
+
const onCompiled = (p) => { if (p.baseName === baseName) compiled = p; };
|
|
717
|
+
on(EVENTS.COMPILED, onCompiled);
|
|
718
|
+
let amxxName;
|
|
719
|
+
try {
|
|
720
|
+
amxxName = await compileSingle(
|
|
721
|
+
compileManifest,
|
|
722
|
+
smaPath,
|
|
723
|
+
compilerPath,
|
|
724
|
+
includeDirs,
|
|
725
|
+
buildDir,
|
|
726
|
+
params?.scripting_root ? path.resolve(params.scripting_root) : undefined
|
|
727
|
+
);
|
|
728
|
+
} finally {
|
|
729
|
+
off(EVENTS.COMPILED, onCompiled);
|
|
730
|
+
}
|
|
419
731
|
|
|
420
732
|
return {
|
|
421
733
|
ok: amxxName != null,
|
|
422
734
|
amxxName,
|
|
735
|
+
output: compiled ? compiled.output : undefined,
|
|
423
736
|
output_path: amxxName ? path.join(buildDir, 'amxmodx', 'plugins', amxxName) : null,
|
|
424
737
|
dep_errors: depErrors.length ? depErrors : undefined,
|
|
425
738
|
};
|
package/src/compiler-fetcher.js
CHANGED
|
@@ -65,6 +65,39 @@ async function fetchCompiler(version, options = {}) {
|
|
|
65
65
|
return { compilerPath: binaryPath, includeDir: fs.existsSync(includeDir) ? includeDir : null };
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Compiler info without necessarily downloading. With noFetch the report only
|
|
70
|
+
* reflects what is already cached; otherwise the compiler is ensured to be
|
|
71
|
+
* present (downloaded on first use), exactly like fetchCompiler.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} [version] - explicit version (else resolved like the CLI: manifest → latest)
|
|
74
|
+
* @param {object} [options]
|
|
75
|
+
* @param {boolean} [options.noFetch] - don't download; report cache state only
|
|
76
|
+
* @returns {Promise<{version: string, platform: string, compilerPath: string|null, includeDir: string|null, cached: boolean}>}
|
|
77
|
+
*/
|
|
78
|
+
async function getCompilerInfo(version, options = {}) {
|
|
79
|
+
const { noFetch = false } = options;
|
|
80
|
+
const resolved = version || await fetchLatestVersion({ noFetch });
|
|
81
|
+
const platform = getPlatform();
|
|
82
|
+
const cacheDir = path.join(getCacheDir(), 'amxxpc', resolved, platform);
|
|
83
|
+
const binaryName = platform === 'windows' ? 'amxxpc.exe' : 'amxxpc';
|
|
84
|
+
const compilerPath = path.join(cacheDir, binaryName);
|
|
85
|
+
const includeDir = path.join(cacheDir, 'include');
|
|
86
|
+
|
|
87
|
+
if (!fs.existsSync(compilerPath) && !noFetch) {
|
|
88
|
+
await fetchCompiler(resolved);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const cached = fs.existsSync(compilerPath);
|
|
92
|
+
return {
|
|
93
|
+
version: resolved,
|
|
94
|
+
platform,
|
|
95
|
+
compilerPath: cached ? compilerPath : null,
|
|
96
|
+
includeDir: fs.existsSync(includeDir) ? includeDir : null,
|
|
97
|
+
cached,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
68
101
|
// "1.10.5428" → { major: '1', minor: '10', build: '5428' }
|
|
69
102
|
function parseVersion(versionStr) {
|
|
70
103
|
const parts = String(versionStr).split('.');
|
|
@@ -324,4 +357,4 @@ function findDir(root, name) {
|
|
|
324
357
|
return null;
|
|
325
358
|
}
|
|
326
359
|
|
|
327
|
-
module.exports = { fetchCompiler, getAmxmodxFullDir, getHostPlatform, fetchLatestVersion, resolveAmxmodxVersion };
|
|
360
|
+
module.exports = { fetchCompiler, getCompilerInfo, getAmxmodxFullDir, getHostPlatform, fetchLatestVersion, resolveAmxmodxVersion };
|