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
package/src/cli.js ADDED
@@ -0,0 +1,302 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { program } = require('commander');
5
+
6
+ const logger = require('./logger');
7
+ const { checkForUpdate } = require('./update-check');
8
+
9
+ const { runBuild } = require('./commands/build');
10
+ const { runClean } = require('./commands/clean');
11
+ const { runDeploy } = require('./commands/deploy');
12
+ const { runWatch } = require('./commands/watch');
13
+ const { runCacheInfo, runCacheClean } = require('./commands/cache');
14
+ const { runDoctor } = require('./commands/doctor');
15
+ const { runDepsTree } = require('./commands/deps-tree');
16
+ const { runResolveManifest } = require('./commands/resolve-manifest');
17
+ const { runValidate } = require('./commands/validate');
18
+ const { runReleases } = require('./commands/releases');
19
+ const { runInit, runInitInteractive } = require('./commands/init');
20
+ const { runMcp } = require('./commands/mcp');
21
+ const { runServe } = require('./commands/serve');
22
+
23
+ program
24
+ .name('amxx-builder')
25
+ .description('Build and package AMX Mod X server plugins')
26
+ .version(require('../package.json').version);
27
+
28
+ // ─── Update check ──────────────────────────────────────────────────────────────
29
+
30
+ program.hook('preAction', async () => {
31
+ // The MCP server and the serve JSON-RPC server own stdout; an update notice
32
+ // would corrupt the protocol stream.
33
+ if (program.args[0] === 'mcp' || program.args[0] === 'serve') return;
34
+ try {
35
+ const latest = await checkForUpdate();
36
+ if (latest) {
37
+ logger.info(`Доступна новая версия: ${latest} (текущая: ${require('../package.json').version})`);
38
+ logger.dim(` Обновить: npm install -g github:AmxxModularEcosystem/amxx-builder`);
39
+ }
40
+ } catch { /* update check never blocks */ }
41
+ });
42
+
43
+ // ─── build ────────────────────────────────────────────────────────────────────
44
+
45
+ program
46
+ .command('build')
47
+ .description('Build plugins from manifest')
48
+ .option('--manifest <path>', 'Path to manifest file (default: amxbuild.yml, fallback: manifest.yml)')
49
+ .option('--build-dir <path>', 'Override build staging directory (default: ./build)')
50
+ .option('--set <key=value...>', 'Override manifest field (e.g. --set version=1.2.3 --set output.archive_name="{name}-{version}.zip")')
51
+ .option('--define <flag...>', 'Add compiler define, e.g. --define DEBUG --define "VERSION=1.2.3" (appends to amxmodx.defines)')
52
+ .option('--no-fetch', 'Use cached repos without re-cloning')
53
+ .option('--no-archive', 'Compile only, skip archiving')
54
+ .option('--dry-run', 'Show plan without executing')
55
+ .option('--verbose', 'Show detailed output (compiler commands, per-file copies, include dirs)')
56
+ .action(async (options) => {
57
+ try {
58
+ await runBuild(options);
59
+ } catch (err) {
60
+ logger.error(err.message);
61
+ process.exit(1);
62
+ }
63
+ });
64
+
65
+ // ─── clean ────────────────────────────────────────────────────────────────────
66
+
67
+ program
68
+ .command('clean')
69
+ .description('Clean build directory and repo clone cache')
70
+ .option('--build-dir <path>', 'Override build staging directory (default: ./build)')
71
+ .option('--all', 'Also clean compiler cache')
72
+ .action(async (options) => {
73
+ try {
74
+ await runClean(options);
75
+ } catch (err) {
76
+ logger.error(err.message);
77
+ process.exit(1);
78
+ }
79
+ });
80
+
81
+ // ─── cache ────────────────────────────────────────────────────────────────────
82
+
83
+ const cacheCmd = program
84
+ .command('cache')
85
+ .description('Manage the local cache');
86
+
87
+ cacheCmd
88
+ .command('info', { isDefault: true })
89
+ .description('Show cache contents and disk usage')
90
+ .option('--manifest <path>', 'Show local .amxb-cache/ for this manifest')
91
+ .action((options) => {
92
+ try {
93
+ runCacheInfo(options);
94
+ } catch (err) {
95
+ logger.error(err.message);
96
+ process.exit(1);
97
+ }
98
+ });
99
+
100
+ cacheCmd
101
+ .command('clean')
102
+ .description('Remove cached files')
103
+ .option('--compiler', 'Clean compiler cache (amxxpc binaries)')
104
+ .option('--repos', 'Clean repository clones')
105
+ .option('--deps', 'Clean release dependency clones')
106
+ .option('--all', 'Clean all caches')
107
+ .action((options) => {
108
+ try {
109
+ runCacheClean(options);
110
+ } catch (err) {
111
+ logger.error(err.message);
112
+ process.exit(1);
113
+ }
114
+ });
115
+
116
+ // ─── deps-tree ────────────────────────────────────────────────────────────────
117
+
118
+ program
119
+ .command('deps-tree')
120
+ .description('Show recursive dependency tree for manifest or inline deps')
121
+ .option('--manifest <path>', 'Path to manifest file')
122
+ .option('--depth <n>', 'Max recursion depth (0 = unlimited)', parseInt)
123
+ .option('--json', 'Output as JSON instead of tree view')
124
+ .option('--cycle-only', 'Show only cycles')
125
+ .option('--no-fetch', 'Use cached repos without re-cloning')
126
+ .action(async (options) => {
127
+ try {
128
+ await runDepsTree(options);
129
+ } catch (err) {
130
+ logger.error(err.message);
131
+ process.exit(1);
132
+ }
133
+ });
134
+
135
+ // ─── resolve-manifest ──────────────────────────────────────────────────────────
136
+
137
+ program
138
+ .command('resolve-manifest')
139
+ .description('Parse and fully resolve manifest (defaults + overrides)')
140
+ .option('--manifest <path>', 'Path to manifest file')
141
+ .option('--set <key=value...>', 'Override manifest field (dot notation)')
142
+ .option('--define <flag...>', 'Add compiler define')
143
+ .option('--json', 'Output as JSON')
144
+ .action(async (options) => {
145
+ try {
146
+ await runResolveManifest(options);
147
+ } catch (err) {
148
+ logger.error(err.message);
149
+ process.exit(1);
150
+ }
151
+ });
152
+
153
+ // ─── validate ──────────────────────────────────────────────────────────────────
154
+
155
+ program
156
+ .command('validate')
157
+ .description('Validate manifest and show diagnostics')
158
+ .option('--manifest <path>', 'Path to manifest file')
159
+ .option('--json', 'Output as JSON')
160
+ .action(async (options) => {
161
+ try {
162
+ await runValidate(options);
163
+ } catch (err) {
164
+ logger.error(err.message);
165
+ process.exit(1);
166
+ }
167
+ });
168
+
169
+ // ─── releases ──────────────────────────────────────────────────────────────────
170
+
171
+ program
172
+ .command('releases')
173
+ .description('List GitHub releases or tags for a repository')
174
+ .argument('<repo>', 'Repository in format owner/repo')
175
+ .option('--limit <n>', 'Max results (default: 10)', parseInt)
176
+ .option('--tags', 'List git tags instead of releases')
177
+ .option('--assets', 'Include asset details')
178
+ .option('--json', 'Output as JSON')
179
+ .action(async (repo, options) => {
180
+ try {
181
+ await runReleases(repo, options);
182
+ } catch (err) {
183
+ logger.error(err.message);
184
+ process.exit(1);
185
+ }
186
+ });
187
+
188
+ // ─── deploy ───────────────────────────────────────────────────────────────────
189
+
190
+ program
191
+ .command('deploy')
192
+ .description('Deploy build output to the server directory')
193
+ .option('--manifest <path>', 'Path to manifest file')
194
+ .option('--build-dir <path>', 'Build staging directory (default: ./build)')
195
+ .option('--incremental', 'Only copy files newer than the destination')
196
+ .option('--build', 'Run a full build before deploying')
197
+ .action(async (options) => {
198
+ try {
199
+ await runDeploy(options);
200
+ } catch (err) {
201
+ logger.error(err.message);
202
+ process.exit(1);
203
+ }
204
+ });
205
+
206
+ // ─── watch ────────────────────────────────────────────────────────────────────
207
+
208
+ program
209
+ .command('watch')
210
+ .description('Watch local files and incrementally build + deploy on changes')
211
+ .option('--manifest <path>', 'Path to manifest file')
212
+ .option('--build-dir <path>', 'Build staging directory (default: ./build)')
213
+ .option('--no-deploy', 'Watch and rebuild only, skip deploy')
214
+ .action(async (options) => {
215
+ try {
216
+ await runWatch(options);
217
+ } catch (err) {
218
+ logger.error(err.message);
219
+ process.exit(1);
220
+ }
221
+ });
222
+
223
+ // ─── doctor ───────────────────────────────────────────────────────────────────
224
+
225
+ program
226
+ .command('doctor')
227
+ .description('Check system health and validate manifest')
228
+ .option('--manifest <path>', 'Path to manifest file to validate')
229
+ .action(async (options) => {
230
+ try {
231
+ await runDoctor(options);
232
+ } catch (err) {
233
+ logger.error(err.message);
234
+ process.exit(1);
235
+ }
236
+ });
237
+
238
+ // ─── init ─────────────────────────────────────────────────────────────────────
239
+
240
+ program
241
+ .command('init')
242
+ .description('Scaffold a new plugin project in the current directory')
243
+ .option('--name <name>', 'Package name (default: current directory name)')
244
+ .option('--workflow', 'Generate .github/workflows/ci.yml')
245
+ .option('--ci', 'Alias for --workflow')
246
+ .option('--plugin <name>', 'Create amxmodx/scripting/<name>.sma')
247
+ .option('--gitignore', 'Create .gitignore')
248
+ .option('--opencode', 'Create .opencode/opencode.json with MCP config (amxb mcp)')
249
+ .option('--deploy', 'Create .env with deploy stubs (AMXB_DEPLOY_*)')
250
+ .option('--script', 'Create build.bat and build.sh quick-build scripts')
251
+ .option('-i, --interactive', 'Interactive mode with prompts')
252
+ .action(async (options) => {
253
+ try {
254
+ if (options.interactive) {
255
+ await runInitInteractive(options);
256
+ } else {
257
+ runInit(options);
258
+ }
259
+ } catch (err) {
260
+ logger.error(err.message);
261
+ process.exit(1);
262
+ }
263
+ });
264
+
265
+ // ─── mcp ──────────────────────────────────────────────────────────────────────
266
+
267
+ program
268
+ .command('mcp')
269
+ .description('Start the MCP (Model Context Protocol) server for AMXX dependency resolution (stdio transport)')
270
+ .action(async () => {
271
+ try {
272
+ await runMcp();
273
+ } catch (err) {
274
+ logger.error(err.message);
275
+ process.exit(1);
276
+ }
277
+ });
278
+
279
+ // ─── serve ───────────────────────────────────────────────────────────────────
280
+
281
+ program
282
+ .command('serve')
283
+ .description('Start JSON-RPC server for editor integration (stdio)')
284
+ .action(async () => {
285
+ try {
286
+ await runServe();
287
+ } catch (err) {
288
+ logger.error(err.message);
289
+ process.exit(1);
290
+ }
291
+ });
292
+
293
+ // ─── version ───────────────────────────────────────────────────────────────────
294
+
295
+ program
296
+ .command('version')
297
+ .description('Show current version')
298
+ .action(() => {
299
+ console.log(require('../package.json').version);
300
+ });
301
+
302
+ program.parse(process.argv);
@@ -0,0 +1,89 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const glob = require('fast-glob');
4
+ const logger = require('./logger');
5
+ const { repoKey } = require('./deps-resolver');
6
+
7
+ /**
8
+ * Copies everything from each repo's amxmodx_dir into build/amxmodx/,
9
+ * then merges local amxmodx/ and assets/ directories (next to amxbuild.yml).
10
+ *
11
+ * .sma files ARE copied as-is (like any other file); the compiler step
12
+ * recompiles them and overwrites the .amxx outputs. Exclude sources from a
13
+ * repo via its exclude_files patterns if they should not be shipped.
14
+ *
15
+ * Repo-vs-repo file conflicts are handled according to output.on_conflict:
16
+ * last_wins (default) — later repo in list wins, warning emitted
17
+ * first_wins — first repo wins, later duplicates skipped with warning
18
+ * error — build fails on first conflict
19
+ *
20
+ * Local amxmodx/ always wins over repo files (intentional override layer, no warning).
21
+ */
22
+ async function collectAll(manifest, repoLocalDirs, buildDir) {
23
+ const onConflict = manifest.output.on_conflict;
24
+ const amxmodxBuildDir = path.join(buildDir, 'amxmodx');
25
+ fs.mkdirSync(amxmodxBuildDir, { recursive: true });
26
+
27
+ const origins = new Map(); // rel path → repo label (conflict tracking)
28
+
29
+ // Copy from each remote repo
30
+ for (const repoConfig of manifest.repos) {
31
+ const repoDir = repoLocalDirs[repoKey(repoConfig)];
32
+ const srcDir = path.join(repoDir, repoConfig.amxmodx_dir);
33
+
34
+ if (!fs.existsSync(srcDir)) {
35
+ logger.warn(`${repoConfig.repo}: amxmodx dir not found: ${repoConfig.amxmodx_dir}/`);
36
+ continue;
37
+ }
38
+
39
+ const files = await glob(['**/*', ...repoConfig.exclude_files.map((p) => `!${p}`)], {
40
+ cwd: srcDir,
41
+ onlyFiles: true,
42
+ });
43
+
44
+ let copied = 0;
45
+ for (const f of files) {
46
+ if (origins.has(f)) {
47
+ const prev = origins.get(f);
48
+ if (onConflict === 'error') {
49
+ throw new Error(`File conflict: "${f}" — provided by both "${prev}" and "${repoConfig.repo}"`);
50
+ }
51
+ if (onConflict === 'first_wins') {
52
+ logger.warn(`Conflict (kept "${prev}"): ${f}`);
53
+ continue;
54
+ }
55
+ // last_wins (default)
56
+ logger.warn(`Conflict (overwriting "${prev}"): ${f}`);
57
+ }
58
+ const dest = path.join(amxmodxBuildDir, f);
59
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
60
+ fs.copyFileSync(path.join(srcDir, f), dest);
61
+ origins.set(f, repoConfig.repo);
62
+ logger.verbose(` + ${f}`);
63
+ copied++;
64
+ }
65
+
66
+ logger.dim(` ${repoConfig.repo}: ${copied}/${files.length} files from ${repoConfig.amxmodx_dir}/`);
67
+ }
68
+
69
+ // Merge local amxmodx/ dir — always wins over repo files (intentional override layer)
70
+ const manifestDir = path.dirname(manifest._path);
71
+ const localAmxmodxDir = path.join(manifestDir, manifest.amxmodx.dir);
72
+
73
+ if (fs.existsSync(localAmxmodxDir)) {
74
+ const files = await glob('**/*', {
75
+ cwd: localAmxmodxDir,
76
+ onlyFiles: true,
77
+ ignore: [],
78
+ });
79
+ for (const f of files) {
80
+ const dest = path.join(amxmodxBuildDir, f);
81
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
82
+ fs.copyFileSync(path.join(localAmxmodxDir, f), dest);
83
+ }
84
+ if (files.length) logger.info(`Local ${manifest.amxmodx.dir}/: ${files.length} files merged`);
85
+ }
86
+
87
+ }
88
+
89
+ module.exports = { collectAll };
@@ -0,0 +1,49 @@
1
+ 'use strict';
2
+
3
+ const logger = require('../logger');
4
+ const { parseManifest, applyOverrides } = require('../manifest');
5
+ const { resolveManifestPath, loadEnv } = require('./shared');
6
+ const { printDryRun } = require('./dry-run');
7
+ const { subscribeCompiledRendering } = require('./compile-renderer');
8
+ const { runBuild } = require('../build-service');
9
+ const { on, EVENTS } = require('../events');
10
+
11
+ // Render compiler 'compiled' events (previously direct stdout/stderr writes).
12
+ subscribeCompiledRendering();
13
+
14
+ // Render build-service 'done' events (previously a direct logger.success at the
15
+ // end of the CLI runBuild). Keeps the final `Done in Xs` message byte-identical.
16
+ let doneSubscribed = false;
17
+ function subscribeDoneRendering() {
18
+ if (doneSubscribed) return;
19
+ doneSubscribed = true;
20
+ on(EVENTS.DONE, (payload) => {
21
+ if (payload && payload.ok && payload.message) logger.success(payload.message);
22
+ });
23
+ }
24
+ subscribeDoneRendering();
25
+
26
+ async function runBuildCLI(options) {
27
+ if (options.verbose) logger.setVerbose(true);
28
+
29
+ const manifestPath = resolveManifestPath(options.manifest);
30
+ loadEnv(manifestPath);
31
+
32
+ const manifest = parseManifest(manifestPath);
33
+ if (options.set?.length) applyOverrides(manifest, options.set);
34
+ if (options.define?.length) manifest.amxmodx.defines.push(...options.define);
35
+ logger.info(`Manifest: ${manifest.name} v${manifest.version}`);
36
+
37
+ if (options.dryRun) {
38
+ printDryRun(manifest);
39
+ return;
40
+ }
41
+
42
+ await runBuild(manifest, {
43
+ buildDir: options.buildDir,
44
+ fetch: options.fetch,
45
+ archive: options.archive,
46
+ });
47
+ }
48
+
49
+ module.exports = { runBuild: runBuildCLI };
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const logger = require('../logger');
7
+ const { getCacheInfo, dirSize, fmtSize, parseCacheKey } = require('../cache-info');
8
+ const { getCacheDir } = require('../cache-dir');
9
+
10
+ function runCacheInfo(options = {}) {
11
+ const manifestPath = options.manifest ? path.resolve(options.manifest) : undefined;
12
+ const info = getCacheInfo(manifestPath);
13
+
14
+ logger.info(`Cache: ${info.cacheDir} (${info.totalSizeHuman} total)`);
15
+
16
+ if (info.totalSize === 0) {
17
+ logger.dim(' (empty)');
18
+ return;
19
+ }
20
+
21
+ if (info.compiler.versions.length) {
22
+ logger.info('\nCompiler (amxxpc):');
23
+ for (const ver of info.compiler.versions) {
24
+ for (const [platform, size] of Object.entries(ver.platforms)) {
25
+ logger.dim(` ${ver.version.padEnd(14)} ${platform.padEnd(10)} ${fmtSize(size)}`);
26
+ }
27
+ }
28
+ }
29
+
30
+ if (info.repos.count) {
31
+ logger.info(`\nRepos (${info.repos.count}, ${info.repos.totalSizeHuman} total):`);
32
+ for (const e of info.repos.entries) {
33
+ const label = parseCacheKey(e.key);
34
+ logger.dim(` ${label.padEnd(52)} ${fmtSize(e.size)}`);
35
+ }
36
+ }
37
+
38
+ if (info.releaseDeps.count) {
39
+ logger.info(`\nRelease deps (${info.releaseDeps.count}, ${info.releaseDeps.totalSizeHuman} total):`);
40
+ for (const e of info.releaseDeps.entries) {
41
+ const label = parseCacheKey(e.key);
42
+ logger.dim(` ${label.padEnd(52)} ${fmtSize(e.size)}`);
43
+ }
44
+ }
45
+
46
+ if (info.localAssetCache) {
47
+ logger.info(`\nLocal asset cache (${info.localAssetCache.count}, ${info.localAssetCache.totalSizeHuman}):`);
48
+ logger.dim(` ${info.localAssetCache.path}`);
49
+ }
50
+ }
51
+
52
+ function runCacheClean(options) {
53
+ const { all, compiler, repos, deps } = options;
54
+
55
+ if (!all && !compiler && !repos && !deps) {
56
+ logger.error('Specify what to clean: --compiler, --repos, --deps, or --all');
57
+ process.exit(1);
58
+ }
59
+
60
+ const cacheRoot = getCacheDir();
61
+ const targets = [];
62
+ if (all || compiler) targets.push({ dir: path.join(cacheRoot, 'amxxpc'), label: 'compiler' });
63
+ if (all || repos) targets.push({ dir: path.join(cacheRoot, 'repos'), label: 'repos' });
64
+ if (all || deps) targets.push({ dir: path.join(cacheRoot, 'release-deps'), label: 'release deps' });
65
+
66
+ for (const { dir, label } of targets) {
67
+ if (!fs.existsSync(dir)) {
68
+ logger.dim(` ${label}: already empty`);
69
+ continue;
70
+ }
71
+ const freed = dirSize(dir);
72
+ fs.rmSync(dir, { recursive: true, force: true });
73
+ logger.success(`Cleaned ${label} (${fmtSize(freed)} freed)`);
74
+ }
75
+ }
76
+
77
+ module.exports = { runCacheInfo, runCacheClean };
@@ -0,0 +1,33 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const logger = require('../logger');
7
+ const { getCacheDir } = require('../cache-dir');
8
+
9
+ async function runClean(options) {
10
+ const buildDir = path.resolve(options.buildDir || './build');
11
+ const reposDir = path.join(getCacheDir(), 'repos');
12
+ const releasesDir = path.join(getCacheDir(), 'release-deps');
13
+ const compDir = path.join(getCacheDir(), 'amxxpc');
14
+
15
+ if (fs.existsSync(buildDir)) {
16
+ fs.rmSync(buildDir, { recursive: true, force: true });
17
+ logger.info(`Cleaned: ${buildDir}`);
18
+ }
19
+ if (fs.existsSync(reposDir)) {
20
+ fs.rmSync(reposDir, { recursive: true, force: true });
21
+ logger.info(`Cleaned: ${reposDir}`);
22
+ }
23
+ if (fs.existsSync(releasesDir)) {
24
+ fs.rmSync(releasesDir, { recursive: true, force: true });
25
+ logger.info(`Cleaned: ${releasesDir}`);
26
+ }
27
+ if (options.all && fs.existsSync(compDir)) {
28
+ fs.rmSync(compDir, { recursive: true, force: true });
29
+ logger.info(`Cleaned: ${compDir}`);
30
+ }
31
+ }
32
+
33
+ module.exports = { runClean };
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const logger = require('../logger');
5
+ const { on, EVENTS } = require('../events');
6
+
7
+ let subscribed = false;
8
+
9
+ function dots(filename) {
10
+ return chalk.dim(' ' + '.'.repeat(Math.max(1, 42 - filename.length)) + ' ');
11
+ }
12
+
13
+ /**
14
+ * Renders a single EVENTS.COMPILED payload exactly as src/compiler.js used to
15
+ * write it directly:
16
+ * - ok: `[amxx-builder] <baseName> <dots> OK` on stdout
17
+ * (logger.info adds the `[amxx-builder] ` prefix + trailing newline;
18
+ * the leading ` ` yields the original 3-space gap)
19
+ * - !ok: `[amxx-builder] FAILED: <baseName>` + verbatim compiler output on stderr
20
+ */
21
+ function renderCompiled(payload) {
22
+ const { baseName, ok, output } = payload;
23
+ if (ok) {
24
+ logger.info(` ${baseName} ${dots(baseName)} ${chalk.green('OK')}`);
25
+ } else {
26
+ logger.error(`FAILED: ${baseName}`);
27
+ const out = (output || '').trim();
28
+ if (out) logger.rawError(out + '\n');
29
+ }
30
+ }
31
+
32
+ function subscribeCompiledRendering() {
33
+ if (subscribed) return;
34
+ subscribed = true;
35
+ on(EVENTS.COMPILED, renderCompiled);
36
+ }
37
+
38
+ module.exports = { subscribeCompiledRendering, renderCompiled, dots };
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const logger = require('../logger');
7
+ const { parseManifest } = require('../manifest');
8
+ const { deployBuild } = require('../deployer');
9
+ const { sendRconForPlugins } = require('../rcon');
10
+ const { resolveManifestPath, loadEnv } = require('./shared');
11
+ const { runBuild } = require('./build');
12
+
13
+ function gatherPluginNames(buildDir) {
14
+ const pluginsDir = path.join(buildDir, 'amxmodx', 'plugins');
15
+ if (!fs.existsSync(pluginsDir)) return [];
16
+ return fs.readdirSync(pluginsDir)
17
+ .filter(f => f.endsWith('.amxx'))
18
+ .map(f => f.replace(/\.amxx$/, ''));
19
+ }
20
+
21
+ async function runDeploy(options) {
22
+ const manifestPath = resolveManifestPath(options.manifest);
23
+ const buildDir = path.resolve(options.buildDir || './build');
24
+
25
+ loadEnv(manifestPath);
26
+
27
+ if (options.build) {
28
+ await runBuild({ ...options, manifest: manifestPath });
29
+ } else if (!fs.existsSync(buildDir)) {
30
+ throw new Error(`Build directory not found: ${buildDir}\n → Run "amxb build" first, or use "amxb deploy --build"`);
31
+ }
32
+
33
+ const manifest = parseManifest(manifestPath);
34
+ await deployBuild(manifest, buildDir, { incremental: options.incremental || false });
35
+
36
+ const pluginNames = gatherPluginNames(buildDir);
37
+ await sendRconForPlugins(manifest.deploy, pluginNames);
38
+ }
39
+
40
+ module.exports = { runDeploy };