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.
Files changed (68) hide show
  1. package/AGENTS.md +111 -0
  2. package/README.md +485 -0
  3. package/action-entry.js +42 -0
  4. package/action.yml +55 -0
  5. package/defaults/amxbuild.defaults.yml +40 -0
  6. package/index.js +4 -0
  7. package/mcp/dep-resolver.js +75 -0
  8. package/mcp/handlers.js +862 -0
  9. package/mcp/mcp-server.js +112 -0
  10. package/mcp/registry.js +863 -0
  11. package/mcp/symbol-index.js +171 -0
  12. package/package.json +68 -0
  13. package/src/archiver.js +140 -0
  14. package/src/asset-fetcher.js +277 -0
  15. package/src/build-plan.js +101 -0
  16. package/src/build-service.js +188 -0
  17. package/src/cache-dir.js +21 -0
  18. package/src/cache-info.js +104 -0
  19. package/src/cli.js +302 -0
  20. package/src/collector.js +89 -0
  21. package/src/commands/build.js +49 -0
  22. package/src/commands/cache.js +77 -0
  23. package/src/commands/clean.js +33 -0
  24. package/src/commands/compile-renderer.js +38 -0
  25. package/src/commands/deploy.js +40 -0
  26. package/src/commands/deps-tree.js +92 -0
  27. package/src/commands/doctor.js +77 -0
  28. package/src/commands/dry-run.js +64 -0
  29. package/src/commands/init.js +228 -0
  30. package/src/commands/mcp.js +13 -0
  31. package/src/commands/releases.js +45 -0
  32. package/src/commands/resolve-manifest.js +27 -0
  33. package/src/commands/serve.js +489 -0
  34. package/src/commands/shared.js +24 -0
  35. package/src/commands/validate.js +34 -0
  36. package/src/commands/watch.js +209 -0
  37. package/src/compile-utils.js +65 -0
  38. package/src/compiler-fetcher.js +327 -0
  39. package/src/compiler.js +228 -0
  40. package/src/dep-graph.js +92 -0
  41. package/src/deployer.js +197 -0
  42. package/src/deps-resolver.js +127 -0
  43. package/src/deps-tree.js +202 -0
  44. package/src/env.js +18 -0
  45. package/src/events.js +29 -0
  46. package/src/format.js +23 -0
  47. package/src/fs-utils.js +87 -0
  48. package/src/include-tree.js +845 -0
  49. package/src/ini-builder.js +44 -0
  50. package/src/jsonrpc-transport.js +195 -0
  51. package/src/logger.js +50 -0
  52. package/src/manifest-path.js +34 -0
  53. package/src/manifest.js +373 -0
  54. package/src/progress.js +66 -0
  55. package/src/rcon.js +103 -0
  56. package/src/release-fetcher.js +206 -0
  57. package/src/release-lister.js +79 -0
  58. package/src/repo-fetcher.js +273 -0
  59. package/src/retry.js +50 -0
  60. package/src/schema.js +54 -0
  61. package/src/update-check.js +114 -0
  62. package/src/validate.js +69 -0
  63. package/src/watcher.js +135 -0
  64. package/templates/init-build.bat +11 -0
  65. package/templates/init-build.sh +7 -0
  66. package/templates/init-deploy.env +14 -0
  67. package/templates/init-manifest.yml +6 -0
  68. package/templates/init-workflow.yml +59 -0
@@ -0,0 +1,101 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const glob = require('fast-glob');
6
+
7
+ /**
8
+ * Structured build plan — mirrors what printDryRun shows, but as data.
9
+ * Single source of truth for the CLI dry-run command and the MCP build_plan /
10
+ * resolve_assets tools.
11
+ *
12
+ * @param {object} manifest - fully resolved manifest (parseManifest output)
13
+ * @param {object} [options]
14
+ * @param {boolean} [options.detailedAssets=false] - when true, `assets` entries
15
+ * carry the richer per-source shape ({ type, map, source, cache }, plus a
16
+ * glob-listed `files` set for local sources) needed by resolve_assets.
17
+ * Default keeps the compact CLI dry-run / build_plan shape.
18
+ * @param {boolean} [options.listLocal=true] - when detailedAssets and listLocal
19
+ * are true, local sources get a `files` array listing the assets/ contents.
20
+ */
21
+ function buildPlanData(manifest, options = {}) {
22
+ const detailed = options.detailedAssets === true;
23
+ const listLocal = options.listLocal !== false;
24
+ const out = manifest.output;
25
+ const expand = (tpl) => tpl.replaceAll('{name}', manifest.name).replaceAll('{version}', manifest.version);
26
+
27
+ return {
28
+ name: manifest.name,
29
+ version: manifest.version,
30
+ compiler: {
31
+ version: manifest.amxmodx.version || 'latest',
32
+ dir: manifest.amxmodx.dir,
33
+ platform: manifest.platform || null,
34
+ defines: manifest.amxmodx.defines,
35
+ },
36
+ repos: manifest.repos.map((r) => ({
37
+ repo: r.repo,
38
+ ref: r.ref || 'default branch',
39
+ amxmodx_dir: r.amxmodx_dir,
40
+ deps_override: r.deps_override || null,
41
+ })),
42
+ globalDeps: manifest.globalDeps.map((d) => ({
43
+ source: d.source,
44
+ repo: d.repo,
45
+ ref: d.ref,
46
+ include_path: d.include_path || null,
47
+ asset: d.asset ?? null,
48
+ })),
49
+ assets: manifest.assets.sources.map((s) => {
50
+ if (detailed) {
51
+ if (s.type === 'amxmodx') {
52
+ return {
53
+ type: 'amxmodx',
54
+ map: s.map,
55
+ source: `amxmodx ${manifest.amxmodx.version || 'latest'} (${manifest.platform || 'host'})`,
56
+ };
57
+ }
58
+ if (s.type === 'release') {
59
+ return {
60
+ type: 'release',
61
+ map: s.map,
62
+ source: `${s.repo}@${s.ref}`,
63
+ asset: s.asset ?? null,
64
+ cache: 'global',
65
+ };
66
+ }
67
+ if (s.type === 'local') {
68
+ const entry = { type: 'local', map: s.map, source: 'assets/ (next to manifest)' };
69
+ if (listLocal) {
70
+ const dir = path.join(path.dirname(manifest._path), 'assets');
71
+ entry.files = fs.existsSync(dir)
72
+ ? glob.sync('**/*', { cwd: dir, dot: false }).sort()
73
+ : [];
74
+ }
75
+ return entry;
76
+ }
77
+ return { type: 'url', map: s.map, source: s.url, cache: s.cache || 'none' };
78
+ }
79
+ if (s.type === 'amxmodx') {
80
+ return { type: 'amxmodx', version: manifest.amxmodx.version || 'latest', platform: manifest.platform || 'host' };
81
+ }
82
+ if (s.type === 'release') {
83
+ return { type: 'release', repo: s.repo, ref: s.ref, asset: s.asset ?? null, cache: s.cache || 'global' };
84
+ }
85
+ if (s.type === 'local') return { type: 'local', source: 'assets/' };
86
+ return { type: 'url', url: s.url, cache: s.cache || 'none' };
87
+ }),
88
+ output: {
89
+ pack: out.pack,
90
+ target: out.pack === false
91
+ ? `${path.join(path.resolve(out.dir), expand(out.amxmodx_path))}/`
92
+ : path.join(path.resolve(out.dir), expand(out.archive_name)),
93
+ amxmodx_path: expand(out.amxmodx_path) + '/',
94
+ assets_path: out.assets_path ? expand(out.assets_path) + '/' : null,
95
+ generate_ini: out.generate_ini,
96
+ on_conflict: out.on_conflict,
97
+ },
98
+ };
99
+ }
100
+
101
+ module.exports = { buildPlanData };
@@ -0,0 +1,188 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Staged build orchestration — the single source of truth for the AMXX build
5
+ * pipeline. Interface-agnostic core: it emits structured lifecycle events on
6
+ * the shared event bus (src/events.js) and optionally via an `onEvent`
7
+ * callback; it never touches process.argv/stdout directly (rendering is the
8
+ * job of the calling interface).
9
+ *
10
+ * This is an EXTRACTION of the pipeline that used to live in
11
+ * src/commands/build.js — same core functions, same order, no new domain
12
+ * logic. Interfaces that need a full build (CLI `amxb build`, serve
13
+ * `build.start`, watch's manifest-triggered rebuild) call runBuild here.
14
+ *
15
+ * Event contract (emitted on the bus, plus delivered to `options.onEvent`):
16
+ * EVENTS.STAGE { stage, message } — 'compiler'|'repos'|'deps'|
17
+ * 'collect'|'assets'|'compile'|
18
+ * 'ini'|'archive'
19
+ * EVENTS.COMPILED { baseName, ok, ... } — emitted by src/compiler.js during
20
+ * the compile stage (not here)
21
+ * EVENTS.PROGRESS { label, current, total } — emitted by src/progress.js
22
+ * (downloads/archiving)
23
+ * EVENTS.DONE { ok: true, elapsed, noArchive?, message }
24
+ * EVENTS.ERROR { ok: false, message }
25
+ *
26
+ * Cancellation: pass an AbortSignal (`signal`) or an `isCancelled()` predicate.
27
+ * Checked between stages; a cancelled build throws an Error with
28
+ * `err.code === 'CANCELLED'`. The compile stage itself runs to completion and
29
+ * cannot be aborted mid-flight (each amxxpc invocation is atomic); cancellation
30
+ * only takes effect between stages.
31
+ */
32
+
33
+ const fs = require('fs');
34
+ const path = require('path');
35
+
36
+ const logger = require('./logger');
37
+ const { emit, EVENTS } = require('./events');
38
+ const { resolveGithubToken } = require('./manifest');
39
+ const { fetchCompiler } = require('./compiler-fetcher');
40
+ const { fetchRepo, resolveRepoRefs } = require('./repo-fetcher');
41
+ const { resolveDeps, repoKey } = require('./deps-resolver');
42
+ const { compilePlugins } = require('./compiler');
43
+ const { collectAll } = require('./collector');
44
+ const { fetchAssets } = require('./asset-fetcher');
45
+ const { buildIniFiles } = require('./ini-builder');
46
+ const { createArchive, copyOutput } = require('./archiver');
47
+
48
+ /**
49
+ * Run the full build pipeline for an already-resolved manifest.
50
+ *
51
+ * @param {object} manifest - fully resolved manifest (parseManifest + any
52
+ * applyOverrides/resolveManifest output). Manifest parsing is the caller's
53
+ * job so every interface controls its own resolution boundary.
54
+ * @param {object} [options]
55
+ * @param {string} [options.buildDir='./build'] - build staging directory
56
+ * @param {boolean} [options.fetch=true] - false skips cloning/downloads
57
+ * @param {boolean} [options.archive=true] - false skips archiving/copying
58
+ * @param {function} [options.onEvent] - optional callback, called with
59
+ * every emitted event ({ type, ...payload }) in addition to the bus emit
60
+ * @param {AbortSignal} [options.signal] - abort between stages
61
+ * @param {function} [options.isCancelled] - () => boolean, checked between stages
62
+ * @returns {Promise<{ ok: true, elapsed: string, noArchive?: boolean }>}
63
+ * @throws {Error} on failure (after emitting EVENTS.ERROR); err.code ===
64
+ * 'CANCELLED' when the build was cancelled between stages.
65
+ */
66
+ async function runBuild(manifest, options = {}) {
67
+ const buildStart = Date.now();
68
+
69
+ const buildDir = path.resolve(options.buildDir || './build');
70
+ const noFetch = options.fetch === false;
71
+ const noArchive = options.archive === false;
72
+ const onEvent = typeof options.onEvent === 'function' ? options.onEvent : null;
73
+
74
+ const emitEvent = (name, payload) => {
75
+ emit(name, payload);
76
+ if (onEvent) onEvent({ type: name, ...payload });
77
+ };
78
+
79
+ // The shared bus is a plain EventEmitter where an 'error' event with no
80
+ // subscriber throws ERR_UNHANDLED_ERROR. Subscribers (serve) receive the
81
+ // event normally; when nobody listens, swallow so the original error still
82
+ // propagates up to the caller unchanged.
83
+ const emitError = (payload) => {
84
+ try {
85
+ emitEvent(EVENTS.ERROR, payload);
86
+ } catch (_) { /* no subscriber — keep the original error surface */ }
87
+ };
88
+
89
+ // Cancellation is checked between stages. Compilation itself is atomic per
90
+ // plugin and runs to completion — cancellation only takes effect at the next
91
+ // stage boundary (documented in the module docstring).
92
+ const checkCancelled = () => {
93
+ const cancelled = typeof options.isCancelled === 'function'
94
+ ? options.isCancelled()
95
+ : !!(options.signal && options.signal.aborted);
96
+ if (cancelled) {
97
+ const err = new Error('Build cancelled');
98
+ err.code = 'CANCELLED';
99
+ throw err;
100
+ }
101
+ };
102
+
103
+ try {
104
+ fs.rmSync(buildDir, { recursive: true, force: true });
105
+ fs.mkdirSync(buildDir, { recursive: true });
106
+
107
+ const hasRepos = manifest.repos.length > 0;
108
+
109
+ // ── 1. Compiler ─────────────────────────────────────────────────────────
110
+ emitEvent(EVENTS.STAGE, { stage: 'compiler', message: 'Fetching compiler' });
111
+ const { compilerPath, includeDir: compilerIncludeDir } = await fetchCompiler(manifest.amxmodx.version, { noFetch });
112
+ checkCancelled();
113
+
114
+ // ── 2. Repos — resolve refs + clone (deduped by repo@resolved_ref) ──────
115
+ const repoLocalDirs = {};
116
+ if (hasRepos) {
117
+ emitEvent(EVENTS.STAGE, { stage: 'repos', message: 'Resolving refs and cloning repos' });
118
+ await resolveRepoRefs(manifest.repos, (repo) => resolveGithubToken(manifest, repo));
119
+ checkCancelled();
120
+
121
+ const cloneJobs = new Map();
122
+ for (const repoConfig of manifest.repos) {
123
+ const key = repoKey(repoConfig);
124
+ if (!cloneJobs.has(key)) {
125
+ cloneJobs.set(key,
126
+ fetchRepo(repoConfig.repo, repoConfig._resolvedRef, resolveGithubToken(manifest, repoConfig.repo), noFetch, manifest.github.ssh)
127
+ );
128
+ }
129
+ }
130
+ const cloned = await Promise.all(
131
+ [...cloneJobs.entries()].map(async ([key, p]) => ({ key, dir: await p }))
132
+ );
133
+ for (const { key, dir } of cloned) repoLocalDirs[key] = dir;
134
+ checkCancelled();
135
+ }
136
+
137
+ // ── 3. Deps — resolve + collect .inc ────────────────────────────────────
138
+ emitEvent(EVENTS.STAGE, { stage: 'deps', message: 'Resolving dependencies' });
139
+ const depsIncludeDirs = await resolveDeps(manifest, repoLocalDirs, noFetch, buildDir);
140
+ const includeDirs = compilerIncludeDir ? [...depsIncludeDirs, compilerIncludeDir] : depsIncludeDirs;
141
+ checkCancelled();
142
+
143
+ // ── 4. Collect — repos + local amxmodx + assets ─────────────────────────
144
+ emitEvent(EVENTS.STAGE, { stage: 'collect', message: 'Collecting files' });
145
+ await collectAll(manifest, repoLocalDirs, buildDir);
146
+
147
+ // ── 5. Fetch remote assets ──────────────────────────────────────────────
148
+ emitEvent(EVENTS.STAGE, { stage: 'assets', message: 'Fetching assets' });
149
+ await fetchAssets(manifest, buildDir, noFetch);
150
+ checkCancelled();
151
+
152
+ // ── 6. Compile — all .sma → .amxx (emits EVENTS.COMPILED per plugin) ────
153
+ emitEvent(EVENTS.STAGE, { stage: 'compile', message: 'Compiling plugins' });
154
+ const compiledPlugins = await compilePlugins(
155
+ manifest, repoLocalDirs, compilerPath, includeDirs, buildDir
156
+ );
157
+
158
+ // ── 7. Generate plugins-*.ini ───────────────────────────────────────────
159
+ if (manifest.output.generate_ini) {
160
+ emitEvent(EVENTS.STAGE, { stage: 'ini', message: 'Generating plugins ini files' });
161
+ buildIniFiles(compiledPlugins, buildDir);
162
+ }
163
+ checkCancelled();
164
+
165
+ // ── 8. Archive or copy output ───────────────────────────────────────────
166
+ if (noArchive) {
167
+ logger.info('--no-archive: skipping zip creation');
168
+ const elapsed = ((Date.now() - buildStart) / 1000).toFixed(1);
169
+ return { ok: true, elapsed, noArchive: true };
170
+ }
171
+
172
+ emitEvent(EVENTS.STAGE, { stage: 'archive', message: 'Archiving output' });
173
+ if (manifest.output.pack === false) {
174
+ copyOutput(manifest, buildDir);
175
+ } else {
176
+ await createArchive(manifest, buildDir);
177
+ }
178
+
179
+ const elapsed = ((Date.now() - buildStart) / 1000).toFixed(1);
180
+ emitEvent(EVENTS.DONE, { ok: true, elapsed, message: `Done in ${elapsed}s` });
181
+ return { ok: true, elapsed };
182
+ } catch (err) {
183
+ emitError({ ok: false, message: err.message });
184
+ throw err;
185
+ }
186
+ }
187
+
188
+ module.exports = { runBuild };
@@ -0,0 +1,21 @@
1
+ const path = require('path');
2
+ const os = require('os');
3
+
4
+ function getCacheDir() {
5
+ if (process.env.AMXX_BUILDER_CACHE) {
6
+ return process.env.AMXX_BUILDER_CACHE;
7
+ }
8
+ if (process.platform === 'win32') {
9
+ const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
10
+ return path.join(localAppData, 'amxx-builder');
11
+ }
12
+ if (process.platform === 'darwin') {
13
+ return path.join(os.homedir(), 'Library', 'Caches', 'amxx-builder');
14
+ }
15
+ // Linux — follow the XDG spec when XDG_CACHE_HOME is set.
16
+ const xdg = process.env.XDG_CACHE_HOME;
17
+ if (xdg) return path.join(xdg, 'amxx-builder');
18
+ return path.join(os.homedir(), '.cache', 'amxx-builder');
19
+ }
20
+
21
+ module.exports = { getCacheDir };
@@ -0,0 +1,104 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { getCacheDir } = require('./cache-dir');
6
+ const { formatBytes } = require('./format');
7
+
8
+ // ─── Helpers ────────────────────────────────────────────────────────────────────
9
+
10
+ function dirSize(dir) {
11
+ if (!fs.existsSync(dir)) return 0;
12
+ let total = 0;
13
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
14
+ if (entry.isSymbolicLink()) continue; // avoid loops and broken links
15
+ const p = path.join(dir, entry.name);
16
+ if (entry.isDirectory()) {
17
+ total += dirSize(p);
18
+ } else {
19
+ try { total += fs.statSync(p).size; } catch { /* unreadable file */ }
20
+ }
21
+ }
22
+ return total;
23
+ }
24
+
25
+ function fmtSize(bytes) {
26
+ return formatBytes(bytes);
27
+ }
28
+
29
+ function parseCacheKey(key) {
30
+ // owner__repo__ref → owner/repo @ ref
31
+ const parts = key.split('__');
32
+ if (parts.length < 3) return key;
33
+ return `${parts.slice(0, -1).join('/')} @ ${parts[parts.length - 1]}`;
34
+ }
35
+
36
+ // ─── Cache info ─────────────────────────────────────────────────────────────────
37
+
38
+ function getCacheInfo(manifestPath) {
39
+ const cacheRoot = getCacheDir();
40
+ const totalSize = dirSize(cacheRoot);
41
+
42
+ const info = {
43
+ cacheDir: cacheRoot,
44
+ totalSize,
45
+ totalSizeHuman: fmtSize(totalSize),
46
+ compiler: scanCompilerCache(path.join(cacheRoot, 'amxxpc')),
47
+ repos: scanDirEntries(path.join(cacheRoot, 'repos'), parseCacheKey),
48
+ releaseDeps: scanDirEntries(path.join(cacheRoot, 'release-deps'), parseCacheKey),
49
+ localAssetCache: null,
50
+ };
51
+
52
+ // Local .amxb-cache/ next to manifest
53
+ if (manifestPath) {
54
+ const localDir = path.join(path.dirname(path.resolve(manifestPath)), '.amxb-cache', 'assets');
55
+ if (fs.existsSync(localDir)) {
56
+ const entries = fs.readdirSync(localDir, { withFileTypes: true }).filter(e => e.isDirectory());
57
+ info.localAssetCache = {
58
+ path: localDir,
59
+ count: entries.length,
60
+ totalSize: dirSize(localDir),
61
+ totalSizeHuman: fmtSize(dirSize(localDir)),
62
+ };
63
+ }
64
+ }
65
+
66
+ return info;
67
+ }
68
+
69
+ function scanCompilerCache(compDir) {
70
+ if (!fs.existsSync(compDir)) return { versions: [] };
71
+
72
+ const versions = fs.readdirSync(compDir, { withFileTypes: true })
73
+ .filter(e => e.isDirectory())
74
+ .map(e => {
75
+ const verDir = path.join(compDir, e.name);
76
+ const platforms = {};
77
+ for (const plat of fs.readdirSync(verDir, { withFileTypes: true }).filter(e => e.isDirectory())) {
78
+ platforms[plat.name] = dirSize(path.join(verDir, plat.name));
79
+ }
80
+ return { version: e.name, platforms };
81
+ });
82
+
83
+ return { versions };
84
+ }
85
+
86
+ function scanDirEntries(dir, labelFn) {
87
+ if (!fs.existsSync(dir)) return { count: 0, totalSize: 0, totalSizeHuman: '0 B', entries: [] };
88
+
89
+ const dirs = fs.readdirSync(dir, { withFileTypes: true }).filter(e => e.isDirectory());
90
+ const entries = dirs.map(e => {
91
+ const full = path.join(dir, e.name);
92
+ const size = dirSize(full);
93
+ return { key: e.name, label: labelFn(e.name), size };
94
+ });
95
+
96
+ return {
97
+ count: entries.length,
98
+ totalSize: dirSize(dir),
99
+ totalSizeHuman: fmtSize(dirSize(dir)),
100
+ entries,
101
+ };
102
+ }
103
+
104
+ module.exports = { getCacheInfo, dirSize, fmtSize, parseCacheKey };