amxx-builder 1.6.0 → 1.7.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 +13 -3
- package/README.md +276 -20
- package/action.yml +1 -1
- package/defaults/amxbuild.defaults.yml +7 -1
- package/mcp/handlers.js +342 -152
- package/mcp/registry.js +264 -25
- package/package.json +1 -1
- package/skills/amxb-migration/SKILL.md +126 -27
- package/skills/amxx-pawn-style/SKILL.md +422 -0
- package/src/agent-assets.js +206 -0
- package/src/asset-fetcher.js +33 -48
- package/src/build-plan.js +22 -5
- package/src/build-service.js +54 -11
- package/src/cli.js +38 -4
- package/src/collector.js +9 -1
- package/src/commands/build.js +5 -4
- package/src/commands/dry-run.js +28 -1
- package/src/commands/init.js +125 -11
- package/src/commands/opencode-skills.js +47 -0
- package/src/commands/serve.js +309 -111
- package/src/commands/skills-dir.js +21 -0
- package/src/commands/watch.js +66 -23
- package/src/compile-utils.js +119 -4
- package/src/compiler-fetcher.js +196 -41
- package/src/compiler.js +68 -29
- package/src/dep-graph.js +27 -0
- package/src/deployer.js +39 -15
- package/src/deps-resolver.js +151 -11
- package/src/deps-tree.js +20 -9
- package/src/download.js +132 -0
- package/src/fs-utils.js +35 -1
- package/src/fungun-fetcher.js +28 -8
- package/src/github-api.js +32 -0
- package/src/include-tree.js +30 -61
- package/src/ini-builder.js +1 -1
- package/src/jsonrpc-transport.js +53 -5
- package/src/local-sources.js +363 -0
- package/src/manifest-path.js +18 -1
- package/src/manifest.js +398 -40
- package/src/opencode-skills.js +262 -0
- package/src/release-fetcher.js +26 -33
- package/src/repo-fetcher.js +388 -37
- package/src/retry.js +3 -1
- package/src/schema.js +1 -1
- package/templates/init-deploy.env +9 -0
- package/templates/init-gitignore +22 -0
- package/src/dep-docs.js +0 -115
package/src/asset-fetcher.js
CHANGED
|
@@ -3,16 +3,13 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const crypto = require('crypto');
|
|
6
|
-
const axios = require('axios');
|
|
7
|
-
// Default for API calls; download sites pass their own longer timeout.
|
|
8
|
-
axios.defaults.timeout = 30000;
|
|
9
6
|
const AdmZip = require('adm-zip');
|
|
10
7
|
|
|
11
8
|
const chalk = require('chalk');
|
|
12
|
-
const { safeExtractTar } = require('./fs-utils');
|
|
9
|
+
const { safeExtractTar, makeSiblingTmpDir, publishDir } = require('./fs-utils');
|
|
10
|
+
const { downloadToFile } = require('./download');
|
|
13
11
|
const logger = require('./logger');
|
|
14
12
|
const { getCacheDir } = require('./cache-dir');
|
|
15
|
-
const { withRetry } = require('./retry');
|
|
16
13
|
const { getAmxmodxFullDir, getHostPlatform } = require('./compiler-fetcher');
|
|
17
14
|
const { getReleaseCacheDir } = require('./release-fetcher');
|
|
18
15
|
const { resolveGithubToken } = require('./manifest');
|
|
@@ -91,47 +88,34 @@ async function resolveUrlSource(source, manifestDir, buildDir, noFetch) {
|
|
|
91
88
|
|
|
92
89
|
const filename = getFilenameFromUrl(source.url);
|
|
93
90
|
logger.step(`Assets: downloading ${filename}...`);
|
|
94
|
-
fs.mkdirSync(cacheDir, { recursive: true });
|
|
95
|
-
|
|
96
|
-
const bar = require('./progress').createBar(100, ` ${chalk.cyan('Downloading')} ${(filename || 'file').padEnd(30)}`);
|
|
97
91
|
|
|
92
|
+
// Download + extract into a unique sibling temp dir, then rename into place:
|
|
93
|
+
// an interrupted run leaves only the temp dir (never a sentinel-marked cache)
|
|
94
|
+
// and concurrent fetches of the same URL cannot interleave writes.
|
|
95
|
+
const tmpDir = makeSiblingTmpDir(cacheDir);
|
|
98
96
|
try {
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
onDownloadProgress: (e) => {
|
|
105
|
-
if (bar && e.total) {
|
|
106
|
-
bar.update(Math.round(e.loaded / e.total * 100));
|
|
107
|
-
}
|
|
108
|
-
},
|
|
109
|
-
}),
|
|
110
|
-
{ label: filename }
|
|
111
|
-
);
|
|
112
|
-
if (bar) bar.stop();
|
|
113
|
-
const contentType = response.headers['content-type'] || '';
|
|
114
|
-
const data = Buffer.from(response.data);
|
|
97
|
+
const downloadPath = path.join(tmpDir, filename);
|
|
98
|
+
const { headers } = await downloadToFile(source.url, downloadPath, {
|
|
99
|
+
progressLabel: ` ${chalk.cyan('Downloading')} ${(filename || 'file').padEnd(30)}`,
|
|
100
|
+
});
|
|
101
|
+
const contentType = headers['content-type'] || '';
|
|
115
102
|
|
|
116
103
|
if (isArchive(filename, contentType)) {
|
|
117
|
-
|
|
118
|
-
} else {
|
|
119
|
-
const filePath = path.join(cacheDir, filename);
|
|
120
|
-
const part = filePath + '.part';
|
|
121
|
-
fs.writeFileSync(part, data);
|
|
122
|
-
fs.renameSync(part, filePath);
|
|
104
|
+
extractArchiveFile(downloadPath, tmpDir);
|
|
123
105
|
}
|
|
124
106
|
|
|
125
|
-
|
|
107
|
+
// Sentinel written into tmpDir first: only complete dirs become the cache.
|
|
108
|
+
const sentinelTmp = path.join(tmpDir, '.cached.tmp');
|
|
126
109
|
fs.writeFileSync(sentinelTmp, JSON.stringify({ url: source.url, cached_at: new Date().toISOString() }));
|
|
127
|
-
fs.renameSync(sentinelTmp,
|
|
110
|
+
fs.renameSync(sentinelTmp, path.join(tmpDir, '.cached'));
|
|
111
|
+
publishDir(tmpDir, cacheDir, () => fs.existsSync(sentinel));
|
|
128
112
|
logger.info(`Assets: ${filename} ready`);
|
|
129
113
|
return cacheDir;
|
|
130
114
|
} catch (err) {
|
|
115
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {}
|
|
131
116
|
// Never rmSync the whole cache dir: it may be a shared 'global' entry used
|
|
132
|
-
// by parallel sources or other projects.
|
|
133
|
-
//
|
|
134
|
-
try { fs.rmSync(sentinel, { force: true }); } catch (_) {}
|
|
117
|
+
// by parallel sources or other projects. Only the unique temp dir above is
|
|
118
|
+
// ours to remove; a stale marker-less cache dir is replaced on next fetch.
|
|
135
119
|
throw new Error(`Failed to fetch asset ${source.url}: ${err.message}`);
|
|
136
120
|
}
|
|
137
121
|
}
|
|
@@ -157,25 +141,26 @@ function isArchive(filename, contentType) {
|
|
|
157
141
|
return /zip|tar|gzip|x-compressed/.test(contentType);
|
|
158
142
|
}
|
|
159
143
|
|
|
160
|
-
function
|
|
161
|
-
const isZip = /\.zip$/i.test(
|
|
144
|
+
function extractArchiveFile(archivePath, destDir) {
|
|
145
|
+
const isZip = /\.zip$/i.test(archivePath) || isZipMagicFile(archivePath);
|
|
162
146
|
if (isZip) {
|
|
163
|
-
new AdmZip(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const tmpFile = path.join(destDir, filename);
|
|
167
|
-
fs.writeFileSync(tmpFile, data);
|
|
168
|
-
try {
|
|
169
|
-
safeExtractTar(tmpFile, destDir);
|
|
170
|
-
} finally {
|
|
171
|
-
fs.rmSync(tmpFile, { force: true });
|
|
147
|
+
new AdmZip(archivePath).extractAllTo(destDir, true);
|
|
148
|
+
} else {
|
|
149
|
+
safeExtractTar(archivePath, destDir);
|
|
172
150
|
}
|
|
151
|
+
fs.rmSync(archivePath, { force: true });
|
|
173
152
|
}
|
|
174
153
|
|
|
175
154
|
// ZIP archives start with "PK" — filename extensions are unreliable for
|
|
176
155
|
// CDN/redirect URLs, so sniff the actual bytes when the name is inconclusive.
|
|
177
|
-
function
|
|
178
|
-
|
|
156
|
+
function isZipMagicFile(filePath) {
|
|
157
|
+
try {
|
|
158
|
+
const fd = fs.openSync(filePath, 'r');
|
|
159
|
+
const buf = Buffer.alloc(2);
|
|
160
|
+
fs.readSync(fd, buf, 0, 2, 0);
|
|
161
|
+
fs.closeSync(fd);
|
|
162
|
+
return buf[0] === 0x50 && buf[1] === 0x4b;
|
|
163
|
+
} catch { return false; }
|
|
179
164
|
}
|
|
180
165
|
|
|
181
166
|
// ─── map application ──────────────────────────────────────────────────────────
|
package/src/build-plan.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const glob = require('fast-glob');
|
|
6
|
+
const { isLocal } = require('./local-sources');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Structured build plan — mirrors what printDryRun shows, but as data.
|
|
@@ -35,18 +36,23 @@ function buildPlanData(manifest, options = {}) {
|
|
|
35
36
|
},
|
|
36
37
|
repos: manifest.repos.map((r) => ({
|
|
37
38
|
repo: r.repo,
|
|
38
|
-
|
|
39
|
+
source: isLocal(r) ? 'local' : (r.source || 'git'),
|
|
40
|
+
ref: isLocal(r) ? null : (r.ref || 'default branch'),
|
|
39
41
|
amxmodx_dir: r.amxmodx_dir,
|
|
40
42
|
deps_override: r.deps_override || null,
|
|
43
|
+
local_dir: r._localDir || null,
|
|
44
|
+
...(r.ref_ttl !== undefined ? { ref_ttl: r.ref_ttl } : {}),
|
|
41
45
|
})),
|
|
42
46
|
globalDeps: manifest.globalDeps.map((d) => ({
|
|
43
|
-
source: d.source,
|
|
44
|
-
repo: d.source === 'fungun' ? null : d.repo,
|
|
45
|
-
ref: d.ref,
|
|
47
|
+
source: isLocal(d) ? 'local' : d.source,
|
|
48
|
+
repo: isLocal(d) ? d.repo : (d.source === 'fungun' ? null : d.repo),
|
|
49
|
+
ref: isLocal(d) ? null : d.ref,
|
|
46
50
|
id: d.id ?? null,
|
|
47
51
|
url: d.url ?? null,
|
|
48
52
|
include_path: d.include_path || null,
|
|
49
53
|
asset: d.asset ?? null,
|
|
54
|
+
local_dir: d._localDir || null,
|
|
55
|
+
...(d.ref_ttl !== undefined ? { ref_ttl: d.ref_ttl } : {}),
|
|
50
56
|
})),
|
|
51
57
|
assets: manifest.assets.sources.map((s) => {
|
|
52
58
|
if (detailed) {
|
|
@@ -94,7 +100,18 @@ function buildPlanData(manifest, options = {}) {
|
|
|
94
100
|
: path.join(path.resolve(out.dir), expand(out.archive_name)),
|
|
95
101
|
amxmodx_path: expand(out.amxmodx_path) + '/',
|
|
96
102
|
assets_path: out.assets_path ? expand(out.assets_path) + '/' : null,
|
|
97
|
-
|
|
103
|
+
// `pluginIni` is the resolved source of truth; hand-built fixtures
|
|
104
|
+
// (tests, external callers) may not carry it → fall back to the legacy
|
|
105
|
+
// raw output flag.
|
|
106
|
+
generate_ini: manifest.pluginIni ? manifest.pluginIni.enabled : Boolean(out.generate_ini),
|
|
107
|
+
plugins_ini: manifest.pluginIni
|
|
108
|
+
? {
|
|
109
|
+
enabled: manifest.pluginIni.enabled,
|
|
110
|
+
default: manifest.pluginIni.defaultIni,
|
|
111
|
+
debug: manifest.pluginIni.defaultDebug,
|
|
112
|
+
force_debug: manifest.pluginIni.forceDebug,
|
|
113
|
+
}
|
|
114
|
+
: null,
|
|
98
115
|
on_conflict: out.on_conflict,
|
|
99
116
|
},
|
|
100
117
|
};
|
package/src/build-service.js
CHANGED
|
@@ -37,14 +37,40 @@ const logger = require('./logger');
|
|
|
37
37
|
const { emit, EVENTS } = require('./events');
|
|
38
38
|
const { resolveGithubToken } = require('./manifest');
|
|
39
39
|
const { fetchCompiler } = require('./compiler-fetcher');
|
|
40
|
-
const {
|
|
41
|
-
const {
|
|
40
|
+
const { resolveRepoRefs } = require('./repo-fetcher');
|
|
41
|
+
const { ensureRepoDir } = require('./local-sources');
|
|
42
|
+
const { resolveDeps, repoKey, normalizeRepo } = require('./deps-resolver');
|
|
42
43
|
const { compilePlugins } = require('./compiler');
|
|
43
44
|
const { collectAll } = require('./collector');
|
|
44
45
|
const { fetchAssets } = require('./asset-fetcher');
|
|
45
46
|
const { buildIniFiles } = require('./ini-builder');
|
|
46
47
|
const { createArchive, copyOutput } = require('./archiver');
|
|
47
48
|
|
|
49
|
+
// Legacy manifest knobs (output.generate_ini, *.plugins_ini_postfix) still work
|
|
50
|
+
// but warn. Watch/serve call runBuild repeatedly with the same manifest, so the
|
|
51
|
+
// same warning is emitted only once per process to avoid rebuild-time noise.
|
|
52
|
+
const loggedDeprecations = new Set();
|
|
53
|
+
|
|
54
|
+
function logDeprecations(manifest) {
|
|
55
|
+
for (const message of manifest._deprecations || []) {
|
|
56
|
+
if (loggedDeprecations.has(message)) continue;
|
|
57
|
+
loggedDeprecations.add(message);
|
|
58
|
+
logger.warn(message);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Same once-per-process dedupe as logDeprecations: watch/serve rebuild often.
|
|
63
|
+
const loggedForcedDebug = new Set();
|
|
64
|
+
|
|
65
|
+
function logForcedDebug(manifest) {
|
|
66
|
+
const forceDebug = manifest.pluginIni ? manifest.pluginIni.forceDebug : null;
|
|
67
|
+
if (forceDebug == null) return;
|
|
68
|
+
const message = `AMXB_PLUGINS_DEBUG — plugin debug forced ${forceDebug ? 'on' : 'off'} for this build`;
|
|
69
|
+
if (loggedForcedDebug.has(message)) return;
|
|
70
|
+
loggedForcedDebug.add(message);
|
|
71
|
+
logger.dim(message);
|
|
72
|
+
}
|
|
73
|
+
|
|
48
74
|
/**
|
|
49
75
|
* Run the full build pipeline for an already-resolved manifest.
|
|
50
76
|
*
|
|
@@ -86,6 +112,9 @@ async function runBuild(manifest, options = {}) {
|
|
|
86
112
|
} catch (_) { /* no subscriber — keep the original error surface */ }
|
|
87
113
|
};
|
|
88
114
|
|
|
115
|
+
logDeprecations(manifest);
|
|
116
|
+
logForcedDebug(manifest);
|
|
117
|
+
|
|
89
118
|
// Cancellation is checked between stages. Compilation itself is atomic per
|
|
90
119
|
// plugin and runs to completion — cancellation only takes effect at the next
|
|
91
120
|
// stage boundary (documented in the module docstring).
|
|
@@ -118,19 +147,32 @@ async function runBuild(manifest, options = {}) {
|
|
|
118
147
|
await resolveRepoRefs(manifest.repos, (repo) => resolveGithubToken(manifest, repo));
|
|
119
148
|
checkCancelled();
|
|
120
149
|
|
|
121
|
-
const cloneJobs = new Map();
|
|
150
|
+
const cloneJobs = new Map(); // normalized repo identity → fetch promise
|
|
122
151
|
for (const repoConfig of manifest.repos) {
|
|
123
|
-
const
|
|
124
|
-
if (!cloneJobs.has(
|
|
125
|
-
|
|
126
|
-
|
|
152
|
+
const identity = normalizeRepo(repoConfig);
|
|
153
|
+
if (!cloneJobs.has(identity)) {
|
|
154
|
+
// ensureRepoDir returns `_localDir` for local entries (never fetches).
|
|
155
|
+
cloneJobs.set(identity,
|
|
156
|
+
ensureRepoDir(repoConfig, {
|
|
157
|
+
token: resolveGithubToken(manifest, repoConfig.repo),
|
|
158
|
+
noFetch,
|
|
159
|
+
ssh: manifest.github.ssh,
|
|
160
|
+
})
|
|
127
161
|
);
|
|
128
162
|
}
|
|
129
163
|
}
|
|
130
|
-
const cloned =
|
|
131
|
-
|
|
164
|
+
const cloned = new Map();
|
|
165
|
+
await Promise.all(
|
|
166
|
+
[...cloneJobs.entries()].map(async ([identity, p]) => {
|
|
167
|
+
const dir = await p;
|
|
168
|
+
cloned.set(identity, dir);
|
|
169
|
+
})
|
|
132
170
|
);
|
|
133
|
-
|
|
171
|
+
// Map every manifest entry (raw spelling) to its clone dir; duplicate
|
|
172
|
+
// spellings of the same repo share the clone but each repoKey stays valid.
|
|
173
|
+
for (const repoConfig of manifest.repos) {
|
|
174
|
+
repoLocalDirs[repoKey(repoConfig)] = cloned.get(normalizeRepo(repoConfig));
|
|
175
|
+
}
|
|
134
176
|
checkCancelled();
|
|
135
177
|
}
|
|
136
178
|
|
|
@@ -156,7 +198,7 @@ async function runBuild(manifest, options = {}) {
|
|
|
156
198
|
);
|
|
157
199
|
|
|
158
200
|
// ── 7. Generate plugins-*.ini ───────────────────────────────────────────
|
|
159
|
-
if (manifest.
|
|
201
|
+
if (manifest.pluginIni.enabled) {
|
|
160
202
|
emitEvent(EVENTS.STAGE, { stage: 'ini', message: 'Generating plugins ini files' });
|
|
161
203
|
buildIniFiles(compiledPlugins, buildDir);
|
|
162
204
|
}
|
|
@@ -166,6 +208,7 @@ async function runBuild(manifest, options = {}) {
|
|
|
166
208
|
if (noArchive) {
|
|
167
209
|
logger.info('--no-archive: skipping zip creation');
|
|
168
210
|
const elapsed = ((Date.now() - buildStart) / 1000).toFixed(1);
|
|
211
|
+
emitEvent(EVENTS.DONE, { ok: true, elapsed, noArchive: true, message: `Done in ${elapsed}s` });
|
|
169
212
|
return { ok: true, elapsed, noArchive: true };
|
|
170
213
|
}
|
|
171
214
|
|
package/src/cli.js
CHANGED
|
@@ -19,6 +19,8 @@ const { runReleases } = require('./commands/releases');
|
|
|
19
19
|
const { runInit, runInitInteractive } = require('./commands/init');
|
|
20
20
|
const { runMcp } = require('./commands/mcp');
|
|
21
21
|
const { runServe } = require('./commands/serve');
|
|
22
|
+
const { runSkillsDir } = require('./commands/skills-dir');
|
|
23
|
+
const { runOpencodeSkills } = require('./commands/opencode-skills');
|
|
22
24
|
|
|
23
25
|
program
|
|
24
26
|
.name('amxx-builder')
|
|
@@ -28,9 +30,13 @@ program
|
|
|
28
30
|
// ─── Update check ──────────────────────────────────────────────────────────────
|
|
29
31
|
|
|
30
32
|
program.hook('preAction', async () => {
|
|
31
|
-
// The MCP server
|
|
32
|
-
// would corrupt the protocol stream.
|
|
33
|
-
|
|
33
|
+
// The MCP server, the serve JSON-RPC server, skills-dir and opencode-skills
|
|
34
|
+
// own stdout; an update notice would corrupt the protocol/parseable stream.
|
|
35
|
+
// The GitHub Action already pins a version and shouldn't make an extra
|
|
36
|
+
// network call.
|
|
37
|
+
if (program.args[0] === 'mcp' || program.args[0] === 'serve'
|
|
38
|
+
|| program.args[0] === 'skills-dir' || program.args[0] === 'opencode-skills') return;
|
|
39
|
+
if (process.env.GITHUB_ACTIONS === 'true') return;
|
|
34
40
|
try {
|
|
35
41
|
const latest = await checkForUpdate();
|
|
36
42
|
if (latest) {
|
|
@@ -245,10 +251,13 @@ program
|
|
|
245
251
|
.option('--ci', 'Alias for --workflow')
|
|
246
252
|
.option('--plugin <name>', 'Create amxmodx/scripting/<name>.sma')
|
|
247
253
|
.option('--gitignore', 'Create .gitignore')
|
|
248
|
-
.option('--opencode', 'Create .opencode/opencode.json
|
|
254
|
+
.option('--opencode', 'Create .opencode/opencode.json (MCP config) + skills bridge plugin (amxb mcp)')
|
|
249
255
|
.option('--deploy', 'Create .env with deploy stubs (AMXB_DEPLOY_*)')
|
|
250
256
|
.option('--script', 'Create build.bat and build.sh quick-build scripts')
|
|
257
|
+
.option('--vscode', 'Create .vscode/extensions.json with recommended extensions')
|
|
258
|
+
.option('--vsc', 'Alias for --vscode')
|
|
251
259
|
.option('-f, --force', 'Overwrite existing files instead of skipping them')
|
|
260
|
+
.option('--with-manifest', 'With --force, also overwrite an existing amxbuild.yml')
|
|
252
261
|
.option('-i, --interactive', 'Interactive mode with prompts')
|
|
253
262
|
.action(async (options) => {
|
|
254
263
|
try {
|
|
@@ -291,6 +300,31 @@ program
|
|
|
291
300
|
}
|
|
292
301
|
});
|
|
293
302
|
|
|
303
|
+
// ─── skills-dir ────────────────────────────────────────────────────────────────
|
|
304
|
+
|
|
305
|
+
program
|
|
306
|
+
.command('skills-dir')
|
|
307
|
+
.description('Print the absolute path to the bundled skills directory (used by the opencode skills bridge plugin)')
|
|
308
|
+
.action(() => {
|
|
309
|
+
runSkillsDir();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// ─── opencode-skills ─────────────────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
program
|
|
315
|
+
.command('opencode-skills')
|
|
316
|
+
.description('Materialize project + dependency skills for the opencode bridge (stdout: container paths)')
|
|
317
|
+
.option('--manifest <path>', 'Path to manifest file')
|
|
318
|
+
.option('--no-fetch', 'Use cached repos without re-cloning')
|
|
319
|
+
.action(async (options) => {
|
|
320
|
+
try {
|
|
321
|
+
await runOpencodeSkills(options);
|
|
322
|
+
} catch (err) {
|
|
323
|
+
logger.error(err.message);
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
294
328
|
// ─── version ───────────────────────────────────────────────────────────────────
|
|
295
329
|
|
|
296
330
|
program
|
package/src/collector.js
CHANGED
|
@@ -2,7 +2,7 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const glob = require('fast-glob');
|
|
4
4
|
const logger = require('./logger');
|
|
5
|
-
const { repoKey } = require('./deps-resolver');
|
|
5
|
+
const { repoKey, normalizeRepo } = require('./deps-resolver');
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Copies everything from each repo's amxmodx_dir into build/amxmodx/,
|
|
@@ -25,9 +25,17 @@ async function collectAll(manifest, repoLocalDirs, buildDir) {
|
|
|
25
25
|
fs.mkdirSync(amxmodxBuildDir, { recursive: true });
|
|
26
26
|
|
|
27
27
|
const origins = new Map(); // rel path → repo label (conflict tracking)
|
|
28
|
+
const seenRepos = new Set(); // case-insensitive repo identity (dedupe)
|
|
28
29
|
|
|
29
30
|
// Copy from each remote repo
|
|
30
31
|
for (const repoConfig of manifest.repos) {
|
|
32
|
+
const repoIdentity = normalizeRepo(repoConfig);
|
|
33
|
+
if (seenRepos.has(repoIdentity)) {
|
|
34
|
+
logger.dim(` ${repoConfig.repo}: duplicate entry, skipping`);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
seenRepos.add(repoIdentity);
|
|
38
|
+
|
|
31
39
|
const repoDir = repoLocalDirs[repoKey(repoConfig)];
|
|
32
40
|
const srcDir = path.join(repoDir, repoConfig.amxmodx_dir);
|
|
33
41
|
|
package/src/commands/build.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const logger = require('../logger');
|
|
4
|
-
const {
|
|
4
|
+
const { resolveManifest } = require('../manifest');
|
|
5
5
|
const { resolveManifestPath, loadEnv } = require('./shared');
|
|
6
6
|
const { printDryRun } = require('./dry-run');
|
|
7
7
|
const { subscribeCompiledRendering } = require('./compile-renderer');
|
|
@@ -29,9 +29,10 @@ async function runBuildCLI(options) {
|
|
|
29
29
|
const manifestPath = resolveManifestPath(options.manifest);
|
|
30
30
|
loadEnv(manifestPath);
|
|
31
31
|
|
|
32
|
-
const manifest =
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
const manifest = resolveManifest(manifestPath, {
|
|
33
|
+
set: options.set,
|
|
34
|
+
define: options.define,
|
|
35
|
+
});
|
|
35
36
|
logger.info(`Manifest: ${manifest.name} v${manifest.version}`);
|
|
36
37
|
|
|
37
38
|
if (options.dryRun) {
|
package/src/commands/dry-run.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const logger = require('../logger');
|
|
5
5
|
const { buildPlanData } = require('../build-plan');
|
|
6
|
+
const { isLocal } = require('../local-sources');
|
|
6
7
|
|
|
7
8
|
function printDryRun(manifest) {
|
|
8
9
|
const out = manifest.output;
|
|
@@ -20,6 +21,10 @@ function printDryRun(manifest) {
|
|
|
20
21
|
if (manifest.repos.length) {
|
|
21
22
|
logger.info(`\nRepos (${manifest.repos.length}):`);
|
|
22
23
|
for (const r of manifest.repos) {
|
|
24
|
+
if (isLocal(r)) {
|
|
25
|
+
logger.dim(` [local] ${r.repo} -> ${r._localDir} [dir: ${r.amxmodx_dir}]`);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
23
28
|
const ref = r.ref || 'default branch';
|
|
24
29
|
logger.dim(` ${r.repo} @ ${ref} [dir: ${r.amxmodx_dir}]`);
|
|
25
30
|
}
|
|
@@ -28,6 +33,10 @@ function printDryRun(manifest) {
|
|
|
28
33
|
if (manifest.globalDeps.length) {
|
|
29
34
|
logger.info(`\nGlobal deps (${manifest.globalDeps.length}):`);
|
|
30
35
|
for (const d of manifest.globalDeps) {
|
|
36
|
+
if (isLocal(d)) {
|
|
37
|
+
logger.dim(` [local] ${d.repo} -> ${d._localDir}${d.include_path ? ':' + d.include_path : ''}`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
31
40
|
if (d.source === 'fungun') {
|
|
32
41
|
logger.dim(` [fungun] plugin #${d.id} (${d.url})`);
|
|
33
42
|
continue;
|
|
@@ -60,7 +69,25 @@ function printDryRun(manifest) {
|
|
|
60
69
|
logger.dim(` amxmodx path in archive: ${expand(out.amxmodx_path)}/`);
|
|
61
70
|
}
|
|
62
71
|
if (out.assets_path) logger.dim(` assets path: ${expand(out.assets_path)}/`);
|
|
63
|
-
|
|
72
|
+
|
|
73
|
+
if (manifest.pluginIni) {
|
|
74
|
+
const { enabled, defaultIni, defaultDebug, forceDebug } = manifest.pluginIni;
|
|
75
|
+
const base = defaultIni === false
|
|
76
|
+
? 'none'
|
|
77
|
+
: (defaultIni === '' ? 'plugins.ini' : `plugins-${defaultIni}.ini`);
|
|
78
|
+
const debug = forceDebug != null
|
|
79
|
+
? `${forceDebug} (forced by AMXB_PLUGINS_DEBUG)`
|
|
80
|
+
: defaultDebug;
|
|
81
|
+
logger.dim(` plugins ini: ${enabled ? 'enabled' : 'disabled'} | default: ${base} | debug: ${debug}`);
|
|
82
|
+
logger.dim(` on_conflict: ${out.on_conflict}`);
|
|
83
|
+
} else {
|
|
84
|
+
logger.dim(` generate_ini: ${out.generate_ini} | on_conflict: ${out.on_conflict}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (manifest._deprecations && manifest._deprecations.length) {
|
|
88
|
+
logger.info(`\nDeprecations (${manifest._deprecations.length}):`);
|
|
89
|
+
for (const d of manifest._deprecations) logger.warn(` ${d}`);
|
|
90
|
+
}
|
|
64
91
|
|
|
65
92
|
logger.info(`\n=== END DRY RUN ===`);
|
|
66
93
|
}
|