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,202 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Recursive dependency tree builder.
5
+ *
6
+ * Walks a list of dep entries, fetches each repo, reads its DEPS_LIST (or
7
+ * deps_override callback), and recursively resolves sub-dependencies.
8
+ *
9
+ * Used by:
10
+ * - CLI: amxb deps-tree
11
+ * - MCP: get_dep_tree tool
12
+ *
13
+ * No domain-specific logic — pure tree traversal with cycle detection.
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ const { fetchRepo, resolveRef } = require('./repo-fetcher');
20
+ const { parseDepsLines } = require('./manifest');
21
+ const { normalize } = require('./deps-resolver');
22
+
23
+ // ─── Public API ────────────────────────────────────────────────────────────────
24
+
25
+ /**
26
+ * Build a recursive dependency tree.
27
+ *
28
+ * @param {Object[]} rootDeps — root dep entries.
29
+ * Each entry: { repo, ref, source?, include_path?, asset? }
30
+ * @param {Object} [options]
31
+ * @param {string} [options.token] — GitHub PAT (falls back to env)
32
+ * @param {Function} [options.tokenFor] — (repo) => token; per-owner token resolver.
33
+ * When provided, takes precedence over options.token for every dep node.
34
+ * @param {boolean} [options.noFetch] — only use cache, skip network
35
+ * @param {number} [options.depth] — max depth (0 = unlimited)
36
+ * @param {string} [options.from] — origin label for root deps ('manifest')
37
+ * @param {Function} [options.getDepsOverride] — (repo) => dep[] | null;
38
+ * Called for each dep node before reading DEPS_LIST. If it returns an array,
39
+ * those entries are used instead of reading the repo's DEPS_LIST file.
40
+ * @returns {Promise<{ dependencies: Object[] }>}
41
+ */
42
+ async function buildDepTree(rootDeps, options = {}) {
43
+ const {
44
+ token = null,
45
+ tokenFor = null,
46
+ noFetch = false,
47
+ depth = 0,
48
+ from: rootFrom = 'manifest',
49
+ getDepsOverride = null,
50
+ } = options;
51
+
52
+ const resolveToken = tokenFor || (() => token);
53
+
54
+ const visited = new Set(); // Set<"owner/repo@resolvedRef"> — already expanded globally
55
+ const pathStack = new Set(); // current recursion path — a dep seen here is a TRUE cycle
56
+ const tree = [];
57
+
58
+ for (const dep of rootDeps) {
59
+ const node = await walkDep(dep, {
60
+ resolveToken, noFetch, depth, visited, pathStack,
61
+ from: rootFrom,
62
+ currentDepth: 0,
63
+ getDepsOverride,
64
+ });
65
+ tree.push(node);
66
+ }
67
+
68
+ return { dependencies: tree };
69
+ }
70
+
71
+ /**
72
+ * Assemble root deps for the dependency tree from a resolved manifest.
73
+ * Single source of truth shared by the CLI `amxb deps-tree` command and the
74
+ * MCP `get_dep_tree` tool.
75
+ *
76
+ * Mirrors the historical behavior of both callers:
77
+ * - manifest.repos → { ...repoConfig, _from: 'repo' } (repo/ref plus
78
+ * preserved repo fields, tagged with their origin)
79
+ * - manifest.globalDeps → { ...dep, _from: 'manifest' }
80
+ * - getDepsOverride(repo) → the repo config's deps_override, or null
81
+ *
82
+ * @param {object} manifest - resolved manifest (parseManifest output)
83
+ * @returns {{ rootDeps: Object[], getDepsOverride: (repo: string) => Object[]|null }}
84
+ */
85
+ function assembleRootDeps(manifest) {
86
+ const rootDeps = [];
87
+ for (const repoConfig of manifest.repos) {
88
+ rootDeps.push({ ...repoConfig, _from: 'repo' });
89
+ }
90
+ for (const dep of manifest.globalDeps) {
91
+ rootDeps.push({ ...dep, _from: 'manifest' });
92
+ }
93
+ const getDepsOverride = (repo) => {
94
+ const config = manifest.repos.find((r) => r.repo === repo);
95
+ return config ? config.deps_override : null;
96
+ };
97
+ return { rootDeps, getDepsOverride };
98
+ }
99
+
100
+ // ─── Recursive walk ────────────────────────────────────────────────────────────
101
+
102
+ async function walkDep(dep, ctx) {
103
+ const { resolveToken, noFetch, depth, visited, pathStack, getDepsOverride } = ctx;
104
+
105
+ const repo = dep.repo;
106
+ const ref = dep.ref || 'HEAD';
107
+ const token = resolveToken(repo);
108
+
109
+ // ── Resolve ref (e.g. "latest" → concrete tag) ──────────────────────────
110
+ let resolvedRef;
111
+ let refError = null;
112
+ try {
113
+ resolvedRef = await resolveRef(repo, dep.ref, token);
114
+ } catch (err) {
115
+ resolvedRef = null;
116
+ refError = err.message;
117
+ }
118
+
119
+ // ── Cycle vs shared detection ───────────────────────────────────────────
120
+ const normRepo = normalize(repo);
121
+ const visitedKey = resolvedRef
122
+ ? `${normRepo}@${resolvedRef}`
123
+ : `${normRepo}@${ref}`; // if resolve failed, use original ref
124
+
125
+ const isCycle = pathStack.has(visitedKey); // on the current path → real cycle
126
+ const isShared = visited.has(visitedKey); // expanded elsewhere → diamond/shared dep
127
+ const skipExpand = isCycle || isShared;
128
+
129
+ if (resolvedRef) {
130
+ pathStack.add(visitedKey);
131
+ visited.add(visitedKey);
132
+ }
133
+
134
+ // ── Check depth ─────────────────────────────────────────────────────────
135
+ const currentDepth = ctx.currentDepth || 0;
136
+ const atDepthLimit = depth > 0 && currentDepth >= depth;
137
+
138
+ // ── Sub-dependencies ────────────────────────────────────────────────────
139
+ let subDeps = [];
140
+ let fetchError = null;
141
+
142
+ if (!skipExpand && !atDepthLimit && resolvedRef && dep.source !== 'release') {
143
+ try {
144
+ const result = await getSubDeps(dep, resolvedRef, token, noFetch, getDepsOverride);
145
+ for (const subDep of result.deps) {
146
+ const childNode = await walkDep(subDep, {
147
+ ...ctx,
148
+ from: result.from,
149
+ currentDepth: currentDepth + 1,
150
+ });
151
+ subDeps.push(childNode);
152
+ }
153
+ } catch (err) {
154
+ fetchError = err.message;
155
+ }
156
+ }
157
+
158
+ if (resolvedRef) pathStack.delete(visitedKey);
159
+
160
+ // ── Build node ──────────────────────────────────────────────────────────
161
+ return {
162
+ repo,
163
+ ref: dep.ref || null,
164
+ resolvedRef,
165
+ source: dep.source || 'git',
166
+ include_path: dep.include_path || null,
167
+ asset: dep.asset != null ? dep.asset : null,
168
+ from: ctx.from,
169
+ error: refError || fetchError || null,
170
+ cycle: isCycle,
171
+ shared: isShared,
172
+ dependencies: subDeps,
173
+ };
174
+ }
175
+
176
+ // ─── Read sub-deps from repo or override ──────────────────────────────────────
177
+
178
+ async function getSubDeps(dep, resolvedRef, token, noFetch, getDepsOverride) {
179
+ // 1. Check for deps_override first
180
+ if (typeof getDepsOverride === 'function') {
181
+ const override = getDepsOverride(dep.repo);
182
+ if (override && Array.isArray(override) && override.length > 0) {
183
+ return { deps: override, from: 'deps_override' };
184
+ }
185
+ }
186
+
187
+ // 2. Clone repo (or use cache) and read DEPS_LIST
188
+ const repoDir = await fetchRepo(dep.repo, resolvedRef, token, noFetch, false);
189
+ const depsPath = path.join(repoDir, 'DEPS_LIST');
190
+
191
+ if (!fs.existsSync(depsPath)) {
192
+ return { deps: [], from: 'deps_list' };
193
+ }
194
+
195
+ const lines = fs.readFileSync(depsPath, 'utf8').split(/\r?\n/);
196
+ const parsed = parseDepsLines(lines);
197
+ return { deps: parsed, from: 'deps_list' };
198
+ }
199
+
200
+ // ─── Exports ────────────────────────────────────────────────────────────────────
201
+
202
+ module.exports = { buildDepTree, assembleRootDeps };
package/src/env.js ADDED
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+
5
+ /**
6
+ * Load .env from the manifest's directory.
7
+ *
8
+ * @param {string} manifestPath - Path to the manifest (absolute or relative).
9
+ * @param {object} [options] - Extra dotenv config options merged over the defaults
10
+ * ({ override: true }). Callers needing different semantics (e.g. the MCP
11
+ * build_plan tool's non-overriding quiet load) pass them here.
12
+ */
13
+ function loadEnv(manifestPath, options = {}) {
14
+ const manifestDir = path.dirname(path.resolve(manifestPath));
15
+ require('dotenv').config({ path: path.join(manifestDir, '.env'), override: true, ...options });
16
+ }
17
+
18
+ module.exports = { loadEnv };
package/src/events.js ADDED
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ const { EventEmitter } = require('events');
4
+
5
+ // Module-level shared event bus. All of src/ emits and subscribes through this
6
+ // single instance, so interface layers (CLI renderer, MCP, serve/JSON-RPC) can
7
+ // observe the same stream of structured events without touching the core.
8
+ const bus = new EventEmitter();
9
+
10
+ // Thin wrappers bound to the shared instance (EventEmitter methods are
11
+ // unbound, so they must be bound here to work when destructured).
12
+ const on = bus.on.bind(bus);
13
+ const off = bus.off.bind(bus);
14
+ const once = bus.once.bind(bus);
15
+ const emit = bus.emit.bind(bus);
16
+ const removeAllListeners = bus.removeAllListeners.bind(bus);
17
+
18
+ // Canonical event names for the core render channel.
19
+ const EVENTS = {
20
+ LOG: 'log',
21
+ PROGRESS: 'progress',
22
+ STAGE: 'stage',
23
+ COMPILED: 'compiled',
24
+ DIAG: 'diag',
25
+ DONE: 'done',
26
+ ERROR: 'error',
27
+ };
28
+
29
+ module.exports = { on, off, once, emit, removeAllListeners, EVENTS };
package/src/format.js ADDED
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Format a byte count as a human-readable string.
5
+ *
6
+ * Tiers:
7
+ * B — bytes < 1024 (exact)
8
+ * KB — bytes < 1024^2 (default precision 1)
9
+ * MB — bytes < 1024^3 (default precision 1)
10
+ * GB — bytes >= 1024^3 (default precision 2)
11
+ *
12
+ * @param {number} bytes
13
+ * @param {{ precision?: number }} [opts] - Override the per-tier default precision.
14
+ * @returns {string}
15
+ */
16
+ function formatBytes(bytes, { precision } = {}) {
17
+ if (bytes < 1024) return `${bytes} B`;
18
+ if (bytes < 1024 ** 2) return `${(bytes / 1024).toFixed(precision == null ? 1 : precision)} KB`;
19
+ if (bytes < 1024 ** 3) return `${(bytes / 1024 ** 2).toFixed(precision == null ? 1 : precision)} MB`;
20
+ return `${(bytes / 1024 ** 3).toFixed(precision == null ? 2 : precision)} GB`;
21
+ }
22
+
23
+ module.exports = { formatBytes };
@@ -0,0 +1,87 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawnSync } = require('child_process');
6
+
7
+ /**
8
+ * Recursively copy all contents from srcDir to destDir.
9
+ * Creates destDir if it does not exist.
10
+ */
11
+ function copyDirContents(src, dest) {
12
+ fs.mkdirSync(dest, { recursive: true });
13
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
14
+ const srcPath = path.join(src, entry.name);
15
+ const destPath = path.join(dest, entry.name);
16
+ if (entry.isSymbolicLink()) {
17
+ // Recreate the link; fall back to copying the file content when the
18
+ // target cannot be recreated (e.g. no symlink privilege on Windows).
19
+ let target;
20
+ try { target = fs.readlinkSync(srcPath); } catch { continue; }
21
+ try {
22
+ fs.symlinkSync(target, destPath);
23
+ } catch {
24
+ const st = fs.statSync(srcPath);
25
+ if (st.isFile()) {
26
+ fs.copyFileSync(srcPath, destPath);
27
+ copyMode(srcPath, destPath);
28
+ }
29
+ }
30
+ continue;
31
+ }
32
+ if (entry.isDirectory()) {
33
+ copyDirContents(srcPath, destPath);
34
+ } else {
35
+ fs.copyFileSync(srcPath, destPath);
36
+ copyMode(srcPath, destPath);
37
+ }
38
+ }
39
+ }
40
+
41
+ // copyFileSync creates files with 0666 & ~umask — restore the source mode
42
+ // so executable bits survive (needed for binaries shipped via assets/).
43
+ function copyMode(src, dest) {
44
+ try {
45
+ const mode = fs.statSync(src).mode & 0o777;
46
+ fs.chmodSync(dest, mode);
47
+ } catch (_) {}
48
+ }
49
+
50
+ /**
51
+ * Recursively count files inside a directory (excludes directories themselves).
52
+ */
53
+ function countFiles(dir) {
54
+ let count = 0;
55
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
56
+ const p = path.join(dir, entry.name);
57
+ if (entry.isDirectory()) {
58
+ count += countFiles(p);
59
+ } else {
60
+ count++;
61
+ }
62
+ }
63
+ return count;
64
+ }
65
+
66
+ /**
67
+ * Safe tar extraction — uses spawnSync to avoid shell injection.
68
+ * Supports .tar.gz / .tgz and .tar.bz2 archives.
69
+ * stripComponents > 0 drops that many leading path segments (GitHub tarballs
70
+ * wrap everything in a single {repo}-{sha}/ top-level dir → strip 1).
71
+ * Throws on non-zero exit.
72
+ */
73
+ function safeExtractTar(archivePath, destDir, { stripComponents = 0 } = {}) {
74
+ const flag = archivePath.endsWith('.tar.bz2') ? 'j' : 'z';
75
+ const args = ['-x' + flag, '-f', archivePath, '-C', destDir];
76
+ if (stripComponents > 0) args.push(`--strip-components=${stripComponents}`);
77
+ const result = spawnSync('tar', args, { stdio: 'pipe' });
78
+ if (result.error) {
79
+ throw new Error(`tar extraction failed for ${path.basename(archivePath)}: ${result.error.message}`);
80
+ }
81
+ if (result.status !== 0) {
82
+ const msg = (result.stderr || result.stdout || '').toString().trim();
83
+ throw new Error(`tar extraction failed for ${path.basename(archivePath)}: ${msg || 'unknown error'}`);
84
+ }
85
+ }
86
+
87
+ module.exports = { copyDirContents, countFiles, safeExtractTar };