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,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 };
|
package/src/cache-dir.js
ADDED
|
@@ -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 };
|