amxx-builder 1.6.1 → 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 +7 -1
- package/README.md +219 -17
- package/action.yml +1 -1
- package/defaults/amxbuild.defaults.yml +0 -1
- package/mcp/handlers.js +51 -10
- package/mcp/registry.js +68 -2
- package/package.json +1 -1
- package/skills/amxb-migration/SKILL.md +108 -23
- package/skills/amxx-pawn-style/SKILL.md +422 -0
- package/src/build-plan.js +22 -5
- package/src/build-service.js +37 -3
- package/src/cli.js +2 -0
- package/src/commands/build.js +5 -4
- package/src/commands/dry-run.js +28 -1
- package/src/commands/init.js +69 -6
- package/src/commands/serve.js +10 -3
- package/src/commands/watch.js +18 -4
- package/src/compile-utils.js +108 -2
- package/src/compiler.js +43 -16
- package/src/deps-resolver.js +46 -8
- package/src/deps-tree.js +18 -8
- package/src/github-api.js +32 -0
- package/src/include-tree.js +3 -1
- package/src/ini-builder.js +1 -1
- package/src/local-sources.js +363 -0
- package/src/manifest.js +323 -23
- package/src/opencode-skills.js +1 -1
- package/src/repo-fetcher.js +119 -11
- package/src/schema.js +1 -1
- package/templates/init-deploy.env +9 -0
- package/templates/init-gitignore +22 -0
package/src/commands/init.js
CHANGED
|
@@ -8,6 +8,12 @@ const logger = require('../logger');
|
|
|
8
8
|
const TEMPLATES_DIR = path.join(__dirname, '..', '..', 'templates');
|
|
9
9
|
const SCHEMA_URL = 'https://raw.githubusercontent.com/AmxxModularEcosystem/amxx-builder/master/schema/amxbuild.schema.json';
|
|
10
10
|
|
|
11
|
+
// Written to .vscode/extensions.json by `amxb init --vscode`.
|
|
12
|
+
const VSCODE_EXTENSIONS = [
|
|
13
|
+
'Faktor.amxx-pawn-all-in',
|
|
14
|
+
'amxx-modular-ecosystem.amxb-vscode',
|
|
15
|
+
];
|
|
16
|
+
|
|
11
17
|
// Auto-discovered by opencode (.opencode/plugin/*.js). Registers skills from all
|
|
12
18
|
// three sources in the config hook, with no machine-specific paths in opencode.json.
|
|
13
19
|
const OPENCODE_BRIDGE_FILE = path.join('.opencode', 'plugin', 'amxb-skills.js');
|
|
@@ -98,6 +104,12 @@ async function runInitInteractive(options) {
|
|
|
98
104
|
initial: true,
|
|
99
105
|
}).run();
|
|
100
106
|
|
|
107
|
+
const doVscode = await new Confirm({
|
|
108
|
+
name: 'vscode',
|
|
109
|
+
message: 'Create .vscode/extensions.json with recommended extensions?',
|
|
110
|
+
initial: true,
|
|
111
|
+
}).run();
|
|
112
|
+
|
|
101
113
|
const actions = [];
|
|
102
114
|
actions.push('amxbuild.yml');
|
|
103
115
|
if (doWorkflow) actions.push('.github/workflows/ci.yml');
|
|
@@ -106,6 +118,7 @@ async function runInitInteractive(options) {
|
|
|
106
118
|
if (doDeploy) actions.push('.env');
|
|
107
119
|
if (doOpencode) actions.push('.opencode/opencode.json');
|
|
108
120
|
if (doScript) actions.push('build.bat', 'build.sh');
|
|
121
|
+
if (doVscode) actions.push('.vscode/extensions.json');
|
|
109
122
|
|
|
110
123
|
logger.info('Creating:');
|
|
111
124
|
for (const a of actions) logger.dim(` ${a}`);
|
|
@@ -128,9 +141,7 @@ async function runInitInteractive(options) {
|
|
|
128
141
|
}
|
|
129
142
|
|
|
130
143
|
if (doGitignore) {
|
|
131
|
-
|
|
132
|
-
'*.amxx', '*.zip', '.env', '.amxb-cache', '.claude', 'build', 'dist', '',
|
|
133
|
-
].join('\n'), options.force);
|
|
144
|
+
writeGitignore(options.force);
|
|
134
145
|
}
|
|
135
146
|
|
|
136
147
|
if (doDeploy) {
|
|
@@ -145,6 +156,10 @@ async function runInitInteractive(options) {
|
|
|
145
156
|
if (doScript) {
|
|
146
157
|
writeBuildScripts(options.force);
|
|
147
158
|
}
|
|
159
|
+
|
|
160
|
+
if (doVscode) {
|
|
161
|
+
writeVscodeExtensions();
|
|
162
|
+
}
|
|
148
163
|
}
|
|
149
164
|
|
|
150
165
|
function runInit(options) {
|
|
@@ -167,9 +182,7 @@ function runInit(options) {
|
|
|
167
182
|
}
|
|
168
183
|
|
|
169
184
|
if (options.gitignore) {
|
|
170
|
-
|
|
171
|
-
'*.amxx', '*.zip', '.env', '.amxb-cache', '.claude', 'build', 'dist', '',
|
|
172
|
-
].join('\n'), options.force);
|
|
185
|
+
writeGitignore(options.force);
|
|
173
186
|
}
|
|
174
187
|
|
|
175
188
|
if (options.deploy) {
|
|
@@ -184,6 +197,16 @@ function runInit(options) {
|
|
|
184
197
|
if (options.script) {
|
|
185
198
|
writeBuildScripts(options.force);
|
|
186
199
|
}
|
|
200
|
+
|
|
201
|
+
if (options.vscode || options.vsc) {
|
|
202
|
+
writeVscodeExtensions();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Shared by both init paths; also the .gitignore basis the amxb-migration skill
|
|
207
|
+
// reuses, so keep templates/init-gitignore in sync with its step 6.
|
|
208
|
+
function writeGitignore(force) {
|
|
209
|
+
return writeIfAbsent('.gitignore', renderTemplate('init-gitignore'), force);
|
|
187
210
|
}
|
|
188
211
|
|
|
189
212
|
function writeBuildScripts(force) {
|
|
@@ -269,6 +292,46 @@ function writeOpencodeBridge(force) {
|
|
|
269
292
|
writeIfAbsent(OPENCODE_BRIDGE_FILE, OPENCODE_BRIDGE_PLUGIN, force);
|
|
270
293
|
}
|
|
271
294
|
|
|
295
|
+
// The extensions file is shared with the user's own editor setup, so it is
|
|
296
|
+
// merged rather than overwritten: existing recommendations are preserved and
|
|
297
|
+
// only the missing amxb entries are appended.
|
|
298
|
+
function writeVscodeExtensions() {
|
|
299
|
+
const dir = '.vscode';
|
|
300
|
+
const file = path.join(dir, 'extensions.json');
|
|
301
|
+
|
|
302
|
+
if (!fs.existsSync(file)) {
|
|
303
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
304
|
+
fs.writeFileSync(file, JSON.stringify({ recommendations: VSCODE_EXTENSIONS }, null, 2) + '\n');
|
|
305
|
+
logger.success(`Created ${file}`);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
let cfg;
|
|
310
|
+
try {
|
|
311
|
+
cfg = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
312
|
+
} catch (err) {
|
|
313
|
+
logger.warn(`${file} exists but is invalid JSON, skipping`);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (cfg === null || typeof cfg !== 'object' || Array.isArray(cfg)) {
|
|
318
|
+
logger.warn(`${file} is not a JSON object, skipping`);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const current = Array.isArray(cfg.recommendations) ? cfg.recommendations : [];
|
|
323
|
+
const missing = VSCODE_EXTENSIONS.filter((id) => !current.includes(id));
|
|
324
|
+
|
|
325
|
+
if (missing.length === 0) {
|
|
326
|
+
logger.warn(`${file} already recommends the amxb extensions, skipping`);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
cfg.recommendations = current.concat(missing);
|
|
331
|
+
fs.writeFileSync(file, JSON.stringify(cfg, null, 2) + '\n');
|
|
332
|
+
logger.success(`Updated ${file} with recommended extensions`);
|
|
333
|
+
}
|
|
334
|
+
|
|
272
335
|
function renderTemplate(name, vars = {}) {
|
|
273
336
|
let content = fs.readFileSync(path.join(TEMPLATES_DIR, name), 'utf8');
|
|
274
337
|
for (const [key, value] of Object.entries(vars)) {
|
package/src/commands/serve.js
CHANGED
|
@@ -54,7 +54,8 @@ const { on, off, EVENTS } = require('../events');
|
|
|
54
54
|
|
|
55
55
|
const { loadEnv } = require('../env');
|
|
56
56
|
const { resolveManifestPath } = require('../manifest-path');
|
|
57
|
-
const { resolveManifest, parseManifest, resolveGithubToken, parseDepString, parseDocEntries, parseSkillEntries } = require('../manifest');
|
|
57
|
+
const { resolveManifest, parseManifest, resolveGithubToken, parseDepString, parseDepObject, parseRefTtl, parseDocEntries, parseSkillEntries } = require('../manifest');
|
|
58
|
+
const { resolveLocalEntry } = require('../local-sources');
|
|
58
59
|
const { readDepManifest, collectDepAssets, collectLocalAssets } = require('../agent-assets');
|
|
59
60
|
const { validateManifestFile } = require('../validate');
|
|
60
61
|
const { collectIncFiles, parseIncludeDirective, searchIncludeFile } = require('../include-tree');
|
|
@@ -184,7 +185,9 @@ function resolveGithubTokenFor(params, repo) {
|
|
|
184
185
|
function depFromParams(params) {
|
|
185
186
|
let dep;
|
|
186
187
|
if (params?.dep) {
|
|
187
|
-
|
|
188
|
+
if (typeof params.dep === 'string') dep = parseDepString(params.dep);
|
|
189
|
+
else if (params.dep.source === 'local') dep = parseDepObject(params.dep);
|
|
190
|
+
else dep = { ...params.dep };
|
|
188
191
|
} else {
|
|
189
192
|
if (!params?.repo) throw new Error('Provide either "dep" or "repo"');
|
|
190
193
|
const source = params.source || 'git';
|
|
@@ -194,7 +197,10 @@ function depFromParams(params) {
|
|
|
194
197
|
if (params?.source) dep.source = params.source;
|
|
195
198
|
if (params?.include_path) dep.include_path = params.include_path;
|
|
196
199
|
if (params?.asset != null) dep.asset = params.asset;
|
|
197
|
-
return
|
|
200
|
+
return resolveLocalEntry(
|
|
201
|
+
dep,
|
|
202
|
+
params?.manifest ? path.dirname(path.resolve(params.manifest)) : process.cwd()
|
|
203
|
+
);
|
|
198
204
|
}
|
|
199
205
|
|
|
200
206
|
// Agent docs/skills: dep mode when dep/repo is present, otherwise the local
|
|
@@ -540,6 +546,7 @@ function createServeServer() {
|
|
|
540
546
|
source: entry.source || 'git',
|
|
541
547
|
include_path: entry.include_path || null,
|
|
542
548
|
asset: entry.asset != null ? entry.asset : null,
|
|
549
|
+
...(entry.ref_ttl != null ? { ref_ttl: parseRefTtl(entry.ref_ttl, `deps entry ${entry.repo}`) } : {}),
|
|
543
550
|
};
|
|
544
551
|
});
|
|
545
552
|
return buildDepTree(rootDeps, { token: params?.token, noFetch, depth, from: 'user' });
|
package/src/commands/watch.js
CHANGED
|
@@ -11,7 +11,8 @@ const { deployBuild, deployPlugin, deployFile, removeDeployedFile } = require('.
|
|
|
11
11
|
const { sendRconForPlugins } = require('../rcon');
|
|
12
12
|
const { DepGraph } = require('../dep-graph');
|
|
13
13
|
const { startWatch } = require('../watcher');
|
|
14
|
-
const {
|
|
14
|
+
const { resolveRepoRefs } = require('../repo-fetcher');
|
|
15
|
+
const { ensureRepoDir } = require('../local-sources');
|
|
15
16
|
const { resolveDeps, repoKey } = require('../deps-resolver');
|
|
16
17
|
const { resolveManifestPath, loadEnv } = require('./shared');
|
|
17
18
|
const { runBuild } = require('./build');
|
|
@@ -20,6 +21,13 @@ const { subscribeCompiledRendering } = require('./compile-renderer');
|
|
|
20
21
|
// Render compiler 'compiled' events (previously direct stdout/stderr writes).
|
|
21
22
|
subscribeCompiledRendering();
|
|
22
23
|
|
|
24
|
+
// Watch never regenerates INI files, so applyPluginRule is only used as a skip
|
|
25
|
+
// check here; the base only has to be a valid defaults layer.
|
|
26
|
+
function pluginRuleBase(manifest) {
|
|
27
|
+
const { defaultIni, defaultDebug } = manifest.pluginIni;
|
|
28
|
+
return { ini: defaultIni, debug: defaultDebug === true };
|
|
29
|
+
}
|
|
30
|
+
|
|
23
31
|
async function runWatch(options) {
|
|
24
32
|
const manifestPath = resolveManifestPath(options.manifest);
|
|
25
33
|
const buildDir = path.resolve(options.buildDir || './build');
|
|
@@ -52,8 +60,14 @@ async function runWatch(options) {
|
|
|
52
60
|
for (const repoConfig of manifest.repos) {
|
|
53
61
|
const key = repoKey(repoConfig);
|
|
54
62
|
if (!cloneJobs.has(key)) {
|
|
63
|
+
// Cache-only, like before; ensureRepoDir returns `_localDir` for
|
|
64
|
+
// local entries (no network, no noFetch check).
|
|
55
65
|
cloneJobs.set(key,
|
|
56
|
-
|
|
66
|
+
ensureRepoDir(repoConfig, {
|
|
67
|
+
token: resolveGithubToken(manifest, repoConfig.repo),
|
|
68
|
+
noFetch: true,
|
|
69
|
+
ssh: manifest.github.ssh,
|
|
70
|
+
})
|
|
57
71
|
);
|
|
58
72
|
}
|
|
59
73
|
}
|
|
@@ -144,7 +158,7 @@ async function runWatch(options) {
|
|
|
144
158
|
return enqueue(async () => {
|
|
145
159
|
state.depGraph.update(smaPath);
|
|
146
160
|
const smaRel = path.relative(state.scriptingRootDir, smaPath).split(path.sep).join('/');
|
|
147
|
-
const pluginRule = applyPluginRule(smaRel, state.manifest.
|
|
161
|
+
const pluginRule = applyPluginRule(smaRel, state.manifest.plugins.rules, pluginRuleBase(state.manifest));
|
|
148
162
|
if (!pluginRule) {
|
|
149
163
|
logger.dim(` Skipped by plugin rule: ${smaRel}`);
|
|
150
164
|
return;
|
|
@@ -178,7 +192,7 @@ async function runWatch(options) {
|
|
|
178
192
|
const compiled = [];
|
|
179
193
|
for (const smaPath of affected) {
|
|
180
194
|
const smaRel = path.relative(state.scriptingRootDir, smaPath).split(path.sep).join('/');
|
|
181
|
-
const pluginRule = applyPluginRule(smaRel, state.manifest.
|
|
195
|
+
const pluginRule = applyPluginRule(smaRel, state.manifest.plugins.rules, pluginRuleBase(state.manifest));
|
|
182
196
|
if (!pluginRule) {
|
|
183
197
|
logger.dim(` Skipped by plugin rule: ${smaRel}`);
|
|
184
198
|
continue;
|
package/src/compile-utils.js
CHANGED
|
@@ -4,11 +4,113 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { execFile } = require('child_process');
|
|
6
6
|
|
|
7
|
+
// ─── WSL DrvFs/9p input guard ────────────────────────────────────────────────
|
|
8
|
+
//
|
|
9
|
+
// amxxpc is a 32-bit Linux binary that cannot READ files on WSL DrvFs/9p mounts
|
|
10
|
+
// (/mnt/c, /mnt/d, /mnt/j, …). A source or include dir there makes it fail with
|
|
11
|
+
// "fatal error 100: cannot read from file" (a directory used as -i aborts it
|
|
12
|
+
// with std::bad_alloc), even though the file exists and Node reads it fine.
|
|
13
|
+
// Writing output to such a mount works. Catch this up-front and hand back an
|
|
14
|
+
// actionable message instead of the cryptic compiler output.
|
|
15
|
+
|
|
16
|
+
let _isWsl;
|
|
17
|
+
let _mountpoints = null;
|
|
18
|
+
|
|
19
|
+
function isWsl() {
|
|
20
|
+
if (_isWsl !== undefined) return _isWsl;
|
|
21
|
+
_isWsl = false;
|
|
22
|
+
if (process.platform === 'linux') {
|
|
23
|
+
if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) {
|
|
24
|
+
_isWsl = true;
|
|
25
|
+
} else {
|
|
26
|
+
try {
|
|
27
|
+
_isWsl = /microsoft|wsl/i.test(fs.readFileSync('/proc/version', 'utf8'));
|
|
28
|
+
} catch (_) { /* no /proc/version — assume not WSL */ }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return _isWsl;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// "/proc/mounts" text → mountpoints whose fstype is 9p (WSL DrvFs).
|
|
35
|
+
// Mountpoints escape spaces/tabs/backslashes as \040 / \011 / \134.
|
|
36
|
+
function parse9pMountpoints(mountsText) {
|
|
37
|
+
const out = [];
|
|
38
|
+
for (const line of String(mountsText).split('\n')) {
|
|
39
|
+
const parts = line.split(/\s+/);
|
|
40
|
+
if (parts.length >= 4 && parts[2] === '9p') {
|
|
41
|
+
out.push(parts[1].replace(/\\([0-7]{3})/g, (_, oct) => String.fromCharCode(parseInt(oct, 8))));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function get9pMountpoints() {
|
|
48
|
+
if (_mountpoints) return _mountpoints;
|
|
49
|
+
_mountpoints = [];
|
|
50
|
+
if (process.platform === 'linux') {
|
|
51
|
+
try { _mountpoints = parse9pMountpoints(fs.readFileSync('/proc/mounts', 'utf8')); }
|
|
52
|
+
catch (_) { /* no /proc/mounts — nothing to guard */ }
|
|
53
|
+
}
|
|
54
|
+
return _mountpoints;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Compiler argv → the paths amxxpc must READ (source + `-i` include dirs).
|
|
58
|
+
// `-o` output paths are ignored — writing to a 9p mount works.
|
|
59
|
+
function collectCompilerInputs(args) {
|
|
60
|
+
const inputs = [];
|
|
61
|
+
for (let i = 0; i < args.length; i++) {
|
|
62
|
+
const arg = args[i];
|
|
63
|
+
if (arg === '-i') { if (args[i + 1]) inputs.push(args[++i]); continue; }
|
|
64
|
+
if (arg.startsWith('-i')) { inputs.push(arg.slice(2)); continue; }
|
|
65
|
+
if (arg === '-o') { i++; continue; }
|
|
66
|
+
if (arg.startsWith('-o')) continue;
|
|
67
|
+
if (i === 0 && !arg.startsWith('-')) inputs.push(arg); // source is argv[0]
|
|
68
|
+
}
|
|
69
|
+
return inputs.filter(Boolean);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isUnder(p, dir) {
|
|
73
|
+
const abs = path.resolve(p);
|
|
74
|
+
if (abs === dir) return true;
|
|
75
|
+
return abs.startsWith(dir.endsWith('/') ? dir : dir + '/');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function findNonNativeInputs(args, mountpoints) {
|
|
79
|
+
return collectCompilerInputs(args).filter((p) => mountpoints.some((mp) => isUnder(p, mp)));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function nonNativeInputError(inputs) {
|
|
83
|
+
return [
|
|
84
|
+
'amxxpc cannot read files on WSL DrvFs/9p mounts (/mnt/*).',
|
|
85
|
+
'Unreadable input(s):',
|
|
86
|
+
...inputs.map((p) => ` ${p}`),
|
|
87
|
+
'The compiler is a 32-bit Linux binary; on these mounts it fails with',
|
|
88
|
+
'"fatal error 100: cannot read from file" (an include directory aborts it with',
|
|
89
|
+
'std::bad_alloc), although the file exists and Node can read it.',
|
|
90
|
+
'Run the build from the Linux-native filesystem instead — copy the project to',
|
|
91
|
+
'e.g. ~/ (the ~/.cache/amxx-builder cache is shared, nothing re-downloads), or',
|
|
92
|
+
'run the build on Windows.',
|
|
93
|
+
].join('\n');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Preflight for a compiler invocation. On WSL, when the source or any `-i`
|
|
98
|
+
* include dir sits on a DrvFs/9p mount, `ok` is false and `error` is a
|
|
99
|
+
* ready-to-print explanation. `wsl`/`mountpoints` are injectable for tests.
|
|
100
|
+
*/
|
|
101
|
+
function checkCompilerInputs(args, { wsl = isWsl(), mountpoints = get9pMountpoints() } = {}) {
|
|
102
|
+
if (!wsl || !mountpoints.length) return { ok: true, inputs: [], error: null };
|
|
103
|
+
const inputs = findNonNativeInputs(args, mountpoints);
|
|
104
|
+
if (!inputs.length) return { ok: true, inputs: [], error: null };
|
|
105
|
+
return { ok: false, inputs, error: nonNativeInputError(inputs) };
|
|
106
|
+
}
|
|
107
|
+
|
|
7
108
|
/**
|
|
8
109
|
* Unified compiler spawn. Interface-agnostic core helper (used by the CLI build,
|
|
9
110
|
* watch mode and the MCP compile tool).
|
|
10
111
|
*
|
|
11
112
|
* ALWAYS resolves — never rejects:
|
|
113
|
+
* - WSL + input on a DrvFs/9p mount (/mnt/*): { status: 1, output: <explanation> }
|
|
12
114
|
* - on spawn error (ENOENT etc.): { status: 1, output: String(err.message) }
|
|
13
115
|
* - on close (including non-zero exit): { status: code, output: stdout+stderr merged }
|
|
14
116
|
*
|
|
@@ -16,7 +118,11 @@ const { execFile } = require('child_process');
|
|
|
16
118
|
* amxxpc needs its bundled libs). windowsHide: true keeps a console from
|
|
17
119
|
* flashing on Windows.
|
|
18
120
|
*/
|
|
19
|
-
function spawnCompiler(cmd, args, { maxBuffer = 10 * 1024 * 1024 } = {}) {
|
|
121
|
+
function spawnCompiler(cmd, args, { maxBuffer = 10 * 1024 * 1024, ...guardOptions } = {}) {
|
|
122
|
+
const guard = checkCompilerInputs(args, guardOptions);
|
|
123
|
+
if (!guard.ok) {
|
|
124
|
+
return Promise.resolve({ status: 1, output: guard.error });
|
|
125
|
+
}
|
|
20
126
|
return new Promise((resolve) => {
|
|
21
127
|
const env = { ...process.env };
|
|
22
128
|
if (process.platform === 'linux') {
|
|
@@ -71,4 +177,4 @@ function buildDefineArgs(defines) {
|
|
|
71
177
|
return (defines || []).map((d) => (d.includes('=') ? d : `${d}=1`));
|
|
72
178
|
}
|
|
73
179
|
|
|
74
|
-
module.exports = { spawnCompiler, buildIncludeArgs, buildDefineArgs };
|
|
180
|
+
module.exports = { spawnCompiler, buildIncludeArgs, buildDefineArgs, checkCompilerInputs, parse9pMountpoints };
|
package/src/compiler.js
CHANGED
|
@@ -9,19 +9,31 @@ const { repoKey, normalizeRepo } = require('./deps-resolver');
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Applies plugin rules to a local .sma file path (relative to scripting/).
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
*
|
|
13
|
+
* @param {string} smaRelPath - .sma path relative to scripting/
|
|
14
|
+
* @param {Array} rules - manifest.plugins.rules (first matching rule wins)
|
|
15
|
+
* @param {{ ini: false|string, debug: boolean }} base - plugins.defaults layer:
|
|
16
|
+
* ini '' → plugins.ini, ini false → excluded from every INI
|
|
17
|
+
* @returns {null|{ postfix: string, skipIni: boolean, debug: boolean }} null
|
|
18
|
+
* when the matching rule has enabled:false (skip the plugin entirely);
|
|
19
|
+
* otherwise the effective INI postfix, whether the plugin is excluded from
|
|
20
|
+
* every INI (ini:false), and the effective debug flag. `ini` unset/null and
|
|
21
|
+
* `debug` unset/null inherit `base`.
|
|
14
22
|
*/
|
|
15
|
-
function applyPluginRule(smaRelPath, rules,
|
|
23
|
+
function applyPluginRule(smaRelPath, rules, base) {
|
|
16
24
|
const normalized = smaRelPath.split(path.sep).join('/');
|
|
17
25
|
for (const rule of rules) {
|
|
18
26
|
if (micromatch.isMatch(normalized, rule.match, { dot: true })) {
|
|
19
|
-
if (
|
|
20
|
-
const
|
|
21
|
-
return {
|
|
27
|
+
if (rule.enabled === false) return null;
|
|
28
|
+
const ini = rule.ini != null ? rule.ini : base.ini;
|
|
29
|
+
return {
|
|
30
|
+
postfix: ini === false ? '' : ini,
|
|
31
|
+
skipIni: ini === false,
|
|
32
|
+
debug: rule.debug != null ? rule.debug : base.debug,
|
|
33
|
+
};
|
|
22
34
|
}
|
|
23
35
|
}
|
|
24
|
-
return { postfix:
|
|
36
|
+
return { postfix: base.ini === false ? '' : base.ini, skipIni: base.ini === false, debug: base.debug };
|
|
25
37
|
}
|
|
26
38
|
|
|
27
39
|
async function compilePlugins(manifest, repoLocalDirs, compilerPath, includeDirs, buildDir) {
|
|
@@ -30,6 +42,12 @@ async function compilePlugins(manifest, repoLocalDirs, compilerPath, includeDirs
|
|
|
30
42
|
|
|
31
43
|
const collectedIncDir = path.join(buildDir, 'amxmodx', 'scripting', 'include');
|
|
32
44
|
|
|
45
|
+
// plugins.defaults is the base layer for every plugin (local + repo).
|
|
46
|
+
const pluginIni = manifest.pluginIni || { defaultIni: false, defaultDebug: false };
|
|
47
|
+
const base = { ini: pluginIni.defaultIni, debug: pluginIni.defaultDebug === true };
|
|
48
|
+
// AMXB_PLUGINS_DEBUG outranks rule/repo debug when set (true or false).
|
|
49
|
+
const forceDebug = pluginIni.forceDebug != null ? pluginIni.forceDebug : null;
|
|
50
|
+
|
|
33
51
|
// ── Build unified source list ──────────────────────────────────────────────
|
|
34
52
|
const seenSources = new Set(); // case-insensitive repo identity (dedupe)
|
|
35
53
|
const sources = [];
|
|
@@ -46,7 +64,7 @@ async function compilePlugins(manifest, repoLocalDirs, compilerPath, includeDirs
|
|
|
46
64
|
'scripting'
|
|
47
65
|
),
|
|
48
66
|
exclude: repoConfig.exclude,
|
|
49
|
-
|
|
67
|
+
settings: repoConfig._pluginSettings || { ini: null, debug: null },
|
|
50
68
|
});
|
|
51
69
|
}
|
|
52
70
|
|
|
@@ -54,7 +72,7 @@ async function compilePlugins(manifest, repoLocalDirs, compilerPath, includeDirs
|
|
|
54
72
|
if (fs.existsSync(localScriptingDir)) {
|
|
55
73
|
sources.push({
|
|
56
74
|
label: '(local)', ref: 'local', isLocal: true, scriptingDir: localScriptingDir,
|
|
57
|
-
exclude: [],
|
|
75
|
+
exclude: [], settings: null,
|
|
58
76
|
});
|
|
59
77
|
}
|
|
60
78
|
|
|
@@ -62,7 +80,7 @@ async function compilePlugins(manifest, repoLocalDirs, compilerPath, includeDirs
|
|
|
62
80
|
const onConflict = manifest.output.on_conflict || 'last_wins';
|
|
63
81
|
const tasksByOut = new Map(); // outPath → task (dedupe cross-source collisions)
|
|
64
82
|
for (const src of sources) {
|
|
65
|
-
const { scriptingDir, exclude,
|
|
83
|
+
const { scriptingDir, exclude, settings, label, ref, isLocal = false } = src;
|
|
66
84
|
|
|
67
85
|
if (!fs.existsSync(scriptingDir)) {
|
|
68
86
|
logger.dim(` ${label}: no scripting/ dir`);
|
|
@@ -86,23 +104,32 @@ async function compilePlugins(manifest, repoLocalDirs, compilerPath, includeDirs
|
|
|
86
104
|
}
|
|
87
105
|
|
|
88
106
|
for (const smaRel of smaFiles) {
|
|
89
|
-
let taskPostfix
|
|
90
|
-
let skipIni
|
|
107
|
+
let taskPostfix;
|
|
108
|
+
let skipIni = false;
|
|
109
|
+
let taskDebug = base.debug;
|
|
91
110
|
|
|
92
111
|
if (isLocal) {
|
|
93
|
-
const ruleResult = applyPluginRule(smaRel, manifest.
|
|
112
|
+
const ruleResult = applyPluginRule(smaRel, manifest.plugins.rules, base);
|
|
94
113
|
if (!ruleResult) {
|
|
95
114
|
logger.skip(`Skipped (plugin rule): ${smaRel}`);
|
|
96
115
|
continue;
|
|
97
116
|
}
|
|
98
117
|
taskPostfix = ruleResult.postfix;
|
|
99
118
|
skipIni = ruleResult.skipIni;
|
|
119
|
+
taskDebug = forceDebug != null ? forceDebug : ruleResult.debug;
|
|
120
|
+
} else {
|
|
121
|
+
const ini = settings.ini != null ? settings.ini : base.ini;
|
|
122
|
+
taskPostfix = ini === false ? '' : ini;
|
|
123
|
+
skipIni = ini === false;
|
|
124
|
+
taskDebug = forceDebug != null
|
|
125
|
+
? forceDebug
|
|
126
|
+
: (settings.debug != null ? settings.debug : base.debug);
|
|
100
127
|
}
|
|
101
128
|
|
|
102
129
|
const baseName = path.basename(smaRel);
|
|
103
130
|
const outName = smaRel.replace(/\.sma$/, '.amxx').split(path.sep).join('/');
|
|
104
131
|
const task = {
|
|
105
|
-
label, ref, postfix: taskPostfix, skipIni, baseName,
|
|
132
|
+
label, ref, postfix: taskPostfix, skipIni, debug: taskDebug, baseName,
|
|
106
133
|
srcPath: path.join(scriptingDir, smaRel),
|
|
107
134
|
outName,
|
|
108
135
|
outPath: path.join(pluginsDir, ...outName.split('/')),
|
|
@@ -172,7 +199,7 @@ async function mapLimit(items, limit, fn) {
|
|
|
172
199
|
}
|
|
173
200
|
|
|
174
201
|
async function runCompile(compilerPath, task) {
|
|
175
|
-
const { srcPath, outPath, outName, includes, defines, baseName, postfix, skipIni, label, ref } = task;
|
|
202
|
+
const { srcPath, outPath, outName, includes, defines, baseName, postfix, skipIni, debug, label, ref } = task;
|
|
176
203
|
|
|
177
204
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
178
205
|
const args = [srcPath, `-o${outPath}`, ...includes, ...defines];
|
|
@@ -189,7 +216,7 @@ async function runCompile(compilerPath, task) {
|
|
|
189
216
|
|
|
190
217
|
emit(EVENTS.COMPILED, { baseName, ok: true, output, amxxName: outName, repo: label, ref, outName });
|
|
191
218
|
|
|
192
|
-
return { amxxName: outName, plugins_ini_postfix: postfix, skipIni: skipIni || false, repo: label, ref };
|
|
219
|
+
return { amxxName: outName, plugins_ini_postfix: postfix, skipIni: skipIni || false, debug: debug === true, repo: label, ref };
|
|
193
220
|
}
|
|
194
221
|
|
|
195
222
|
async function findExcluded(dir, patterns) {
|
package/src/deps-resolver.js
CHANGED
|
@@ -3,7 +3,8 @@ const path = require('path');
|
|
|
3
3
|
const glob = require('fast-glob');
|
|
4
4
|
const logger = require('./logger');
|
|
5
5
|
const { parseDepsLines, resolveGithubToken } = require('./manifest');
|
|
6
|
-
const {
|
|
6
|
+
const { isLocal } = require('./local-sources');
|
|
7
|
+
const { fetchRepo, resolveRefIfLatest, applicableRefTtl } = require('./repo-fetcher');
|
|
7
8
|
const { fetchReleaseDep } = require('./release-fetcher');
|
|
8
9
|
const { fetchFungunDep } = require('./fungun-fetcher');
|
|
9
10
|
|
|
@@ -11,6 +12,9 @@ const { fetchFungunDep } = require('./fungun-fetcher');
|
|
|
11
12
|
* Resolves all deps, clones them, copies .inc files to build/_includes/,
|
|
12
13
|
* and returns an array of include-dir paths to pass to the compiler (-i flags).
|
|
13
14
|
*
|
|
15
|
+
* Local deps (from ./local-sources) short-circuit first: their include dir is
|
|
16
|
+
* resolved from disk — no ref resolution, fetch or token lookup.
|
|
17
|
+
*
|
|
14
18
|
* Priority: manifest.globalDeps > repo.deps_override > DEPS_LIST file in repo root
|
|
15
19
|
*/
|
|
16
20
|
async function resolveDeps(manifest, repoLocalDirs, noFetch, buildDir) {
|
|
@@ -54,7 +58,9 @@ async function resolveDeps(manifest, repoLocalDirs, noFetch, buildDir) {
|
|
|
54
58
|
|
|
55
59
|
for (const [k, dep] of merged) {
|
|
56
60
|
let srcDir;
|
|
57
|
-
if (dep
|
|
61
|
+
if (isLocal(dep)) {
|
|
62
|
+
srcDir = resolveIncludePath(dep._localDir, dep.include_path, dep.repo);
|
|
63
|
+
} else if (dep.source === 'release') {
|
|
58
64
|
const token = resolveGithubToken(manifest, dep.repo);
|
|
59
65
|
srcDir = await fetchReleaseDep(dep, token, noFetch);
|
|
60
66
|
} else if (dep.source === 'fungun') {
|
|
@@ -62,7 +68,10 @@ async function resolveDeps(manifest, repoLocalDirs, noFetch, buildDir) {
|
|
|
62
68
|
} else {
|
|
63
69
|
const token = resolveGithubToken(manifest, dep.repo);
|
|
64
70
|
const resolvedDepRef = await resolveRefIfLatest(dep.ref, dep.repo, token);
|
|
65
|
-
const depDir = await fetchRepo(
|
|
71
|
+
const depDir = await fetchRepo(
|
|
72
|
+
dep.repo, resolvedDepRef, token, noFetch, manifest.github.ssh,
|
|
73
|
+
applicableRefTtl(dep.ref, dep.ref_ttl)
|
|
74
|
+
);
|
|
66
75
|
srcDir = resolveIncludePath(depDir, dep.include_path, dep.repo);
|
|
67
76
|
}
|
|
68
77
|
|
|
@@ -138,6 +147,9 @@ function resolveIncludePath(repoDir, explicitPath, repoName) {
|
|
|
138
147
|
* Single source of truth shared by the build pipeline, the MCP server and
|
|
139
148
|
* agent-assets resolution.
|
|
140
149
|
*
|
|
150
|
+
* Local deps short-circuit on disk before any release/fungun/git branch — they
|
|
151
|
+
* need no token, ref, fetch or `--no-fetch` handling.
|
|
152
|
+
*
|
|
141
153
|
* `dep` is a parsed dep OBJECT ({ repo, ref, source, include_path, asset }).
|
|
142
154
|
* GitHub token resolution (per-owner fallbacks) is an interface-layer concern —
|
|
143
155
|
* callers pass the already-resolved token, this function never calls fallbackToken.
|
|
@@ -150,6 +162,17 @@ function resolveIncludePath(repoDir, explicitPath, repoName) {
|
|
|
150
162
|
* @returns {Promise<{ rootDir: string, label: string }>}
|
|
151
163
|
*/
|
|
152
164
|
async function fetchDepRoot(dep, { token, noFetch, ssh = false } = {}) {
|
|
165
|
+
if (isLocal(dep)) {
|
|
166
|
+
if (dep.include_path) {
|
|
167
|
+
const sub = path.join(dep._localDir, dep.include_path);
|
|
168
|
+
if (!fs.existsSync(sub)) {
|
|
169
|
+
throw new Error(`include_path "${dep.include_path}" not found in ${dep.repo}`);
|
|
170
|
+
}
|
|
171
|
+
return { rootDir: sub, label: `${dep.repo} (local)` };
|
|
172
|
+
}
|
|
173
|
+
return { rootDir: dep._localDir, label: `${dep.repo} (local)` };
|
|
174
|
+
}
|
|
175
|
+
|
|
153
176
|
if (dep.source === 'release') {
|
|
154
177
|
const dir = await fetchReleaseDep(dep, token, noFetch);
|
|
155
178
|
return { rootDir: dir, label: `${dep.repo}@${dep.ref} (release)` };
|
|
@@ -161,7 +184,9 @@ async function fetchDepRoot(dep, { token, noFetch, ssh = false } = {}) {
|
|
|
161
184
|
}
|
|
162
185
|
|
|
163
186
|
const resolvedRef = await resolveRefIfLatest(dep.ref, dep.repo, token);
|
|
164
|
-
const repoDir = await fetchRepo(
|
|
187
|
+
const repoDir = await fetchRepo(
|
|
188
|
+
dep.repo, resolvedRef, token, noFetch, ssh, applicableRefTtl(dep.ref, dep.ref_ttl)
|
|
189
|
+
);
|
|
165
190
|
if (dep.include_path) {
|
|
166
191
|
const sub = path.join(repoDir, dep.include_path);
|
|
167
192
|
if (!fs.existsSync(sub)) {
|
|
@@ -173,11 +198,14 @@ async function fetchDepRoot(dep, { token, noFetch, ssh = false } = {}) {
|
|
|
173
198
|
}
|
|
174
199
|
|
|
175
200
|
/**
|
|
176
|
-
* Fetch a dependency's include directory (release/fungun/git), using the
|
|
201
|
+
* Fetch a dependency's include directory (local/release/fungun/git), using the
|
|
177
202
|
* fetch cache where possible.
|
|
178
203
|
* Canonical single-source-of-truth shared by the build pipeline (include-tree),
|
|
179
204
|
* the CLI, the serve interface and the MCP layer.
|
|
180
205
|
*
|
|
206
|
+
* Local deps are always on disk — short-circuit to their `_localDir` before any
|
|
207
|
+
* token/ref/fetch logic (`--no-fetch` is irrelevant for them).
|
|
208
|
+
*
|
|
181
209
|
* Explicit-include_path semantics: silently falls back to the repo root when
|
|
182
210
|
* the given path does not exist (the interface callers rely on this).
|
|
183
211
|
*
|
|
@@ -188,6 +216,8 @@ async function fetchDepRoot(dep, { token, noFetch, ssh = false } = {}) {
|
|
|
188
216
|
* @returns {Promise<string>} directory to use as the include dir
|
|
189
217
|
*/
|
|
190
218
|
async function fetchDepIncludeDir(dep, token, noFetch, ssh = false) {
|
|
219
|
+
if (isLocal(dep)) return findDepIncludeDir(dep._localDir, dep.include_path);
|
|
220
|
+
|
|
191
221
|
if (dep.source === 'release') {
|
|
192
222
|
return fetchReleaseDep(
|
|
193
223
|
{ repo: dep.repo, ref: dep.ref, include_path: dep.include_path, asset: dep.asset },
|
|
@@ -201,7 +231,9 @@ async function fetchDepIncludeDir(dep, token, noFetch, ssh = false) {
|
|
|
201
231
|
}
|
|
202
232
|
|
|
203
233
|
const resolvedRef = await resolveRefIfLatest(dep.ref, dep.repo, token);
|
|
204
|
-
const repoDir = await fetchRepo(
|
|
234
|
+
const repoDir = await fetchRepo(
|
|
235
|
+
dep.repo, resolvedRef, token, noFetch, ssh, applicableRefTtl(dep.ref, dep.ref_ttl)
|
|
236
|
+
);
|
|
205
237
|
return findDepIncludeDir(repoDir, dep.include_path);
|
|
206
238
|
}
|
|
207
239
|
|
|
@@ -218,6 +250,7 @@ async function fetchDepIncludeDir(dep, token, noFetch, ssh = false) {
|
|
|
218
250
|
* interface's existing formatting.
|
|
219
251
|
*
|
|
220
252
|
* GitHub tokens are resolved per-owner via resolveGithubToken(manifest, repo);
|
|
253
|
+
* local deps skip the lookup entirely (null token — they never leave disk).
|
|
221
254
|
* ssh must be passed explicitly by the caller (manifest.github.ssh).
|
|
222
255
|
*
|
|
223
256
|
* @param {object|null} manifest - parsed manifest (null → no deps)
|
|
@@ -233,7 +266,8 @@ async function collectDepIncludeDirs(manifest, { noFetch = false, ssh = false }
|
|
|
233
266
|
for (let i = 0; i < deps.length; i++) {
|
|
234
267
|
const dep = deps[i];
|
|
235
268
|
try {
|
|
236
|
-
|
|
269
|
+
const token = isLocal(dep) ? null : resolveGithubToken(manifest, dep.repo);
|
|
270
|
+
dirs[i] = await fetchDepIncludeDir(dep, token, noFetch, ssh);
|
|
237
271
|
} catch (err) {
|
|
238
272
|
errors[i] = err && err.message ? err.message : String(err);
|
|
239
273
|
}
|
|
@@ -245,11 +279,15 @@ async function collectDepIncludeDirs(manifest, { noFetch = false, ssh = false }
|
|
|
245
279
|
// and dedup by core modules that previously inlined repo.toLowerCase()).
|
|
246
280
|
function normalize(repo) { return repo.toLowerCase(); }
|
|
247
281
|
|
|
248
|
-
// Human-readable dep label (fungun deps: no repo@ref — addressed by shop page
|
|
282
|
+
// Human-readable dep label (fungun deps: no repo@ref — addressed by shop page
|
|
283
|
+
// id; local deps: synthetic local/<name> id, versioned by the working tree).
|
|
249
284
|
function depLabel(dep) {
|
|
250
285
|
if (dep && dep.source === 'fungun') {
|
|
251
286
|
return `fungun.net plugin #${dep.id}`;
|
|
252
287
|
}
|
|
288
|
+
if (isLocal(dep)) {
|
|
289
|
+
return `${dep.repo} (local)`;
|
|
290
|
+
}
|
|
253
291
|
return `${dep.repo}@${dep.ref || 'default branch'}`;
|
|
254
292
|
}
|
|
255
293
|
|