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
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: amxx-pawn-style
|
|
3
|
+
description: >-
|
|
4
|
+
Coding conventions for AMXX Pawn (AMX Mod X / GoldSrc .sma and .inc plugins)
|
|
5
|
+
that eliminate Hungarian notation. Use when writing, editing, refactoring,
|
|
6
|
+
or reviewing Pawn code — variables, constants, enums, type tags, natives, or
|
|
7
|
+
plugin metadata. Enforces CamelCase globals, camelCase locals/parameters, no
|
|
8
|
+
type prefixes (g_, i, sz, b, Float:), enum role prefixes (S_, E_, T_),
|
|
9
|
+
namespaced tags with a named Invalid_* zero value, `new const NAME[]`
|
|
10
|
+
strings, `playerIndex` over `id`, `const` on non-mutated parameters,
|
|
11
|
+
`MAX_PLAYERS + 1` arrays, explicit `bool:`, `@NativeName` handlers
|
|
12
|
+
registered with style 0 (never style 1), and `PluginName`/`PluginVersion`
|
|
13
|
+
metadata. Triggers: AMXX, AMX Mod X, Pawn, Small, .sma, .inc, naming
|
|
14
|
+
convention, native, register_native, plugin metadata, PluginName, type tag,
|
|
15
|
+
Hungarian notation, венгерская нотация, стиль кода. Do NOT use for
|
|
16
|
+
building/packaging/deploying servers (use amxb-migration / the amxb CLI),
|
|
17
|
+
or for non-Pawn languages.
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# AMXX Pawn code style
|
|
21
|
+
|
|
22
|
+
Canonical naming and typing conventions for AMXX Pawn (AMX Mod X) code. The
|
|
23
|
+
governing goal is to **remove Hungarian notation**: never encode a value's type
|
|
24
|
+
or scope in its identifier. Pawn already has real type tags (`Float:`,
|
|
25
|
+
`bool:`, custom enum tags), so the type belongs in the declaration, not in the
|
|
26
|
+
name. It also fixes the plugin's public surface: how natives are named and
|
|
27
|
+
registered, and how metadata is declared.
|
|
28
|
+
|
|
29
|
+
Legacy plugins in the ecosystem are saturated with Hungarian prefixes
|
|
30
|
+
(`g_iCount`, `szName`, `bEnabled`, `id`); agents trained on that code default
|
|
31
|
+
to reproducing it. Apply these rules to every new declaration, parameter,
|
|
32
|
+
constant, and enum instead.
|
|
33
|
+
|
|
34
|
+
## When to use
|
|
35
|
+
|
|
36
|
+
- Writing a new `.sma` plugin or `.inc` include.
|
|
37
|
+
- Adding or editing declarations in existing Pawn code.
|
|
38
|
+
- Reviewing or refactoring Pawn for naming/typing consistency.
|
|
39
|
+
- Removing Hungarian notation from a legacy plugin.
|
|
40
|
+
|
|
41
|
+
## When not to use
|
|
42
|
+
|
|
43
|
+
- Building, packaging, or deploying an AMXX server/project — use the
|
|
44
|
+
`amxb-migration` skill or the `amxb` CLI.
|
|
45
|
+
- Non-Pawn languages.
|
|
46
|
+
|
|
47
|
+
## Core rules
|
|
48
|
+
|
|
49
|
+
### 1. No Hungarian notation (highest priority)
|
|
50
|
+
|
|
51
|
+
Never encode type or scope in an identifier. Drop every prefix whose only job
|
|
52
|
+
is to restate the type. This rule overrides "match the surrounding code" — a
|
|
53
|
+
legacy file's style must not leak into new lines.
|
|
54
|
+
|
|
55
|
+
| Legacy prefix | Encodes | Modern replacement |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `g_`, `g` | global scope | no prefix — globals are CamelCase (`PlayerScores`) |
|
|
58
|
+
| `i`, `j`, `k` | integer | a descriptive name (`count`, `playerIndex`) |
|
|
59
|
+
| `sz`, `s`, `str` | string | a descriptive name (`name`, `message`) |
|
|
60
|
+
| `b` | boolean | descriptive name + `bool:` tag (`bool:Enabled`) |
|
|
61
|
+
| `f`, `fl` | float | descriptive name + `Float:` tag (`Float:Delay`) |
|
|
62
|
+
| `p` | pointer / parameter | a descriptive name |
|
|
63
|
+
| `a`, `arr` | array | a descriptive name |
|
|
64
|
+
| `id` | player index | `playerIndex` (rule 4) |
|
|
65
|
+
|
|
66
|
+
`bool:` and `Float:` are **tags**, not prefixes: keep them, and keep them
|
|
67
|
+
required for boolean/float values. A bare `i`/`j` used only as a trivial
|
|
68
|
+
`0..n` loop counter is not type Hungarian and is acceptable; name the index
|
|
69
|
+
when it refers to a domain entity (`playerIndex`, `itemIndex`, `weaponId`).
|
|
70
|
+
|
|
71
|
+
### 2. Variable naming
|
|
72
|
+
|
|
73
|
+
- **Global variables → `CamelCase`**: `new PlayerScores[MAX_PLAYERS + 1];`
|
|
74
|
+
- **Local variables and parameters → `camelCase`**:
|
|
75
|
+
`new damageAmount;`, `dealDamage(playerIndex, Float:damageAmount)`.
|
|
76
|
+
- Do not reintroduce the scope in the name: a global is `RoundCounter`, never
|
|
77
|
+
`g_RoundCounter`.
|
|
78
|
+
|
|
79
|
+
### 3. Constants
|
|
80
|
+
|
|
81
|
+
Compile-time constants use `UPPER_SNAKE_CASE` in a `new const` declaration.
|
|
82
|
+
String constants follow exactly this form:
|
|
83
|
+
|
|
84
|
+
```pawn
|
|
85
|
+
new const PLUGIN_NAME[] = "My Plugin";
|
|
86
|
+
new const MAX_RETRIES = 3;
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Inside an include (`.inc`) whose constants may not all be used, use
|
|
90
|
+
`stock const` to avoid "symbol is never used" warnings:
|
|
91
|
+
|
|
92
|
+
```pawn
|
|
93
|
+
stock const API_VERSION[] = "1.2.0";
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Prefer `new const` over `#define` for typed constants; reserve `#define` for
|
|
97
|
+
conditional compilation and macros. The plugin version string is a deliberate
|
|
98
|
+
exception (see Metadata).
|
|
99
|
+
|
|
100
|
+
### 4. The player index is `playerIndex`
|
|
101
|
+
|
|
102
|
+
Name AMXX player indices `playerIndex`. Never `id`, `pid`, `playerid`, or
|
|
103
|
+
`iPlayer`.
|
|
104
|
+
|
|
105
|
+
```pawn
|
|
106
|
+
public client_command(playerIndex)
|
|
107
|
+
{
|
|
108
|
+
if (!is_user_alive(playerIndex))
|
|
109
|
+
return PLUGIN_HANDLED;
|
|
110
|
+
|
|
111
|
+
new userHealth = get_user_health(playerIndex);
|
|
112
|
+
server_print("HP: %d", userHealth);
|
|
113
|
+
return PLUGIN_HANDLED;
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Player indices are 1-based; index `0` is the server/world. Account for this
|
|
118
|
+
when sizing arrays (rule 6).
|
|
119
|
+
|
|
120
|
+
### 5. Mark effectively-constant parameters `const`
|
|
121
|
+
|
|
122
|
+
Mark every parameter the function does not modify as `const` — arrays and
|
|
123
|
+
strings **and scalars**. The AMXX compiler enforces this: reassigning a
|
|
124
|
+
`const` parameter fails with `error 022: must be lvalue (non-constant)`,
|
|
125
|
+
whether it is an array or a scalar. Legacy code marks only string parameters;
|
|
126
|
+
extend it to all non-mutated parameters.
|
|
127
|
+
|
|
128
|
+
```pawn
|
|
129
|
+
stock printName(const name[])
|
|
130
|
+
{
|
|
131
|
+
server_print("%s", name);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
getEffectiveDamage(const baseDamage, const Float:multiplier)
|
|
135
|
+
{
|
|
136
|
+
return floatround(baseDamage * multiplier);
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 6. Per-player arrays are sized `MAX_PLAYERS + 1`
|
|
141
|
+
|
|
142
|
+
`MAX_PLAYERS` is 32, and valid indices run from `0` (server) through 32
|
|
143
|
+
inclusive. An array of `MAX_PLAYERS` overflows at index 32. Size any array
|
|
144
|
+
indexed by player index as `MAX_PLAYERS + 1` (33):
|
|
145
|
+
|
|
146
|
+
```pawn
|
|
147
|
+
new PlayerDeaths[MAX_PLAYERS + 1];
|
|
148
|
+
new bool:PlayerConnected[MAX_PLAYERS + 1];
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### 7. Booleans use the `bool:` tag
|
|
152
|
+
|
|
153
|
+
Declare booleans with `bool:` and assign `true`/`false`. Do not keep `0`/`1`
|
|
154
|
+
in an untagged cell, and do not name the variable with a `b` prefix.
|
|
155
|
+
|
|
156
|
+
```pawn
|
|
157
|
+
new bool:RoundActive;
|
|
158
|
+
|
|
159
|
+
if (!RoundActive)
|
|
160
|
+
{
|
|
161
|
+
RoundActive = true;
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Untagged `0`/`1` works at runtime, but `bool:` states intent, enables compile
|
|
166
|
+
time checking, and removes the need for the legacy `b` prefix.
|
|
167
|
+
|
|
168
|
+
### 8. Enum roles: `S_`, `E_`, `T_`
|
|
169
|
+
|
|
170
|
+
An AMXX `enum` serves three roles; the prefix announces which.
|
|
171
|
+
|
|
172
|
+
**Struct layout → `S_`.** A record whose name is used as the array dimension.
|
|
173
|
+
Field names live in the global symbol namespace, so keep them distinct from
|
|
174
|
+
your global variable names.
|
|
175
|
+
|
|
176
|
+
```pawn
|
|
177
|
+
enum S_PlayerStats
|
|
178
|
+
{
|
|
179
|
+
PlayerKills,
|
|
180
|
+
PlayerDeaths,
|
|
181
|
+
Float:PlayerPlayTime
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
new PlayerStats[MAX_PLAYERS + 1][S_PlayerStats];
|
|
185
|
+
PlayerStats[playerIndex][PlayerKills] = 0;
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Enumeration → `E_`.** A list of symbolic integer values. Strip the tag the
|
|
189
|
+
enum creates with `enum _:` so members can be assigned to plain cells without
|
|
190
|
+
`warning 213: tag mismatch`:
|
|
191
|
+
|
|
192
|
+
```pawn
|
|
193
|
+
enum _:E_Team
|
|
194
|
+
{
|
|
195
|
+
TeamNone,
|
|
196
|
+
TeamTerrorist,
|
|
197
|
+
TeamCounterTerrorist
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
new playerTeam = TeamTerrorist;
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Type / tag → `T_`.** The enum name is used as a tag to give its values a
|
|
204
|
+
distinct type. Keep the tag and type the variables. Namespace the tag by
|
|
205
|
+
module/system — `T_<System>_<Name>` — so tags from different modules cannot
|
|
206
|
+
collide, and declare a named zero value instead of writing an implicit
|
|
207
|
+
`Tag:0`:
|
|
208
|
+
|
|
209
|
+
```pawn
|
|
210
|
+
enum T_Weapon_State
|
|
211
|
+
{
|
|
212
|
+
Invalid_Weapon_State = 0,
|
|
213
|
+
WeaponIdle,
|
|
214
|
+
WeaponFiring,
|
|
215
|
+
WeaponReloading
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
new T_Weapon_State:weaponState = Invalid_Weapon_State;
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
`Invalid_<System>_<Name>` is the first `enum` member: it is a tagged constant
|
|
222
|
+
equal to `0`, so it also works as a default parameter value. An implicit
|
|
223
|
+
`Tag:0` literal is untagged and bypasses compile-time checking — use the named
|
|
224
|
+
value instead.
|
|
225
|
+
|
|
226
|
+
## Hungarian → modern conversion
|
|
227
|
+
|
|
228
|
+
| Legacy | Modern |
|
|
229
|
+
|---|---|
|
|
230
|
+
| `g_iPlayerScore` | `PlayerScore` |
|
|
231
|
+
| `g_szPlayerName` | `PlayerName` |
|
|
232
|
+
| `g_bRoundActive` | `bool:RoundActive` |
|
|
233
|
+
| `g_fSpawnDelay` | `Float:SpawnDelay` |
|
|
234
|
+
| `new iCount` | `new count` |
|
|
235
|
+
| `szMessage` | `message` |
|
|
236
|
+
| `public client_command(id)` | `public client_command(playerIndex)` |
|
|
237
|
+
| `new g_iData[33]` | `new Data[MAX_PLAYERS + 1]` |
|
|
238
|
+
|
|
239
|
+
Before (legacy):
|
|
240
|
+
|
|
241
|
+
```pawn
|
|
242
|
+
new g_iScore[MAX_PLAYERS];
|
|
243
|
+
new g_szName[33][32];
|
|
244
|
+
new bool:g_bConnected[33];
|
|
245
|
+
|
|
246
|
+
public client_command(id)
|
|
247
|
+
{
|
|
248
|
+
new szMessage[128];
|
|
249
|
+
if (g_bConnected[id])
|
|
250
|
+
formatex(szMessage, charsmax(szMessage), "%s: %d", g_szName[id], g_iScore[id]);
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
After:
|
|
255
|
+
|
|
256
|
+
```pawn
|
|
257
|
+
new Score[MAX_PLAYERS + 1];
|
|
258
|
+
new PlayerName[MAX_PLAYERS + 1][32];
|
|
259
|
+
new bool:PlayerConnected[MAX_PLAYERS + 1];
|
|
260
|
+
|
|
261
|
+
public client_command(playerIndex)
|
|
262
|
+
{
|
|
263
|
+
new message[128];
|
|
264
|
+
if (PlayerConnected[playerIndex])
|
|
265
|
+
formatex(message, charsmax(message), "%s: %d",
|
|
266
|
+
PlayerName[playerIndex], Score[playerIndex]);
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Applying to an existing (legacy) file
|
|
271
|
+
|
|
272
|
+
- Apply these conventions to the lines you add or change.
|
|
273
|
+
- Do not mass-rename untouched code unless the task is explicitly a
|
|
274
|
+
Hungarian-notation cleanup.
|
|
275
|
+
- Refactor via the language server / editor rename, not a blind text replace —
|
|
276
|
+
a global `id` → `playerIndex` replacement can corrupt strings, comments, and
|
|
277
|
+
unrelated identifiers.
|
|
278
|
+
- Renaming, `const`, and `bool:` additions must not change runtime behavior;
|
|
279
|
+
compile and confirm zero new warnings/errors.
|
|
280
|
+
|
|
281
|
+
## Plugin surface: natives and metadata
|
|
282
|
+
|
|
283
|
+
The conventions extend to the two things a plugin exposes to the rest of the
|
|
284
|
+
server: its natives and its metadata.
|
|
285
|
+
|
|
286
|
+
### Natives
|
|
287
|
+
|
|
288
|
+
Name the native handler `@` + the native's full name, and register it through
|
|
289
|
+
`plugin_natives()`. Preferring a handler name identical to the registered name
|
|
290
|
+
means one grep finds both ends; treat this as a preference, not a hard rule.
|
|
291
|
+
|
|
292
|
+
```pawn
|
|
293
|
+
public plugin_natives()
|
|
294
|
+
{
|
|
295
|
+
register_native("Inventory_GiveItem", "@Inventory_GiveItem");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
@Inventory_GiveItem(pluginId, argc)
|
|
299
|
+
{
|
|
300
|
+
// ...
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
- `@` already marks the function public, so do not also write `public` on an
|
|
305
|
+
`@` handler.
|
|
306
|
+
- `pluginId` is the implicit first parameter of a native handler. AMXX has no
|
|
307
|
+
`GetPluginId()` native, so read the calling plugin's id from this parameter.
|
|
308
|
+
- Register with the default style (omit the third argument). Never pass the
|
|
309
|
+
legacy style 1: AMXX keeps it only for compatibility with very old plugins
|
|
310
|
+
and it has known technical problems.
|
|
311
|
+
|
|
312
|
+
### Metadata
|
|
313
|
+
|
|
314
|
+
AMXX reads plugin metadata from `public const` string variables whose names
|
|
315
|
+
are reserved and exact. Declare them at file scope; `register_plugin` in
|
|
316
|
+
`plugin_init` is then optional and kept only for backward compatibility.
|
|
317
|
+
|
|
318
|
+
```pawn
|
|
319
|
+
public stock const PluginName[] = "My Plugin";
|
|
320
|
+
public stock const PluginVersion[] = MyPlugin_VERSION;
|
|
321
|
+
public stock const PluginAuthor[] = "Author";
|
|
322
|
+
public stock const PluginDescription[] = "Short summary";
|
|
323
|
+
public stock const PluginURL[] = "https://example.com/my-plugin";
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
- `PluginName`, `PluginVersion`, and `PluginAuthor` are the baseline.
|
|
327
|
+
`PluginDescription` and `PluginURL` are optional — declare them only when
|
|
328
|
+
they carry real information.
|
|
329
|
+
- `PluginVersion` may reuse a `#define`d version from the plugin's public
|
|
330
|
+
include: `#define MyPlugin_VERSION "x.y.z"` there, consumed here. This is
|
|
331
|
+
optional and mostly useful for plugins that ship a public API (the include
|
|
332
|
+
then carries its own version); a self-contained plugin can use a `new const`
|
|
333
|
+
string instead.
|
|
334
|
+
|
|
335
|
+
## Gotchas
|
|
336
|
+
|
|
337
|
+
- **`enum Name` creates a tag.** Assigning a member to a plain variable warns
|
|
338
|
+
`warning 213: tag mismatch`. Use `enum _:E_Name` for enums consumed as plain
|
|
339
|
+
integers, or type the variable with the tag (`T_Name:x`).
|
|
340
|
+
- **Enum fields share the global namespace.** A field named `PlayerDeaths`
|
|
341
|
+
collides with a global `new PlayerDeaths[]` (`error 021: symbol already
|
|
342
|
+
defined`). Keep field names distinct from global names.
|
|
343
|
+
- **`const` is enforced, not decorative.** Reassigning any `const` parameter —
|
|
344
|
+
scalar included — fails with `error 022: must be lvalue (non-constant)`.
|
|
345
|
+
A `stock` function that is never called has its body dropped before `const`
|
|
346
|
+
checking, so a bug there can hide until the function is used — do not rely
|
|
347
|
+
on an uncalled function to prove a `const` body is legal.
|
|
348
|
+
- **`MAX_PLAYERS` is 32, indices include 32.** Size per-player arrays
|
|
349
|
+
`MAX_PLAYERS + 1`.
|
|
350
|
+
- **`id` is also the argument name in the AMXX includes.** Rename only your
|
|
351
|
+
own declarations to `playerIndex`; keep passing natives unchanged.
|
|
352
|
+
- **Tags are not prefixes.** Removing `Float:`/`bool:` to "de-Hungarianize" is
|
|
353
|
+
wrong: keep the tag, drop the `f`/`b` name prefix.
|
|
354
|
+
- **`@` is already public.** Writing `public` on an `@` native handler is a
|
|
355
|
+
syntax error — the `@` prefix *is* the public declaration form.
|
|
356
|
+
- **Native registration style 1 is legacy.** It survives only for very old
|
|
357
|
+
plugins and has known technical problems; register with the default style.
|
|
358
|
+
- **`pluginId` has no getter.** There is no `GetPluginId()` native; the calling
|
|
359
|
+
plugin id arrives as the handler's implicit first parameter.
|
|
360
|
+
- **Metadata names are reserved and exact.** `PLUGIN_NAME` / `PLUGIN_VERSION`
|
|
361
|
+
are ordinary constants — AMXX reads only `PluginName` / `PluginVersion` /
|
|
362
|
+
`PluginAuthor` / `PluginDescription` / `PluginURL`.
|
|
363
|
+
|
|
364
|
+
## Must do
|
|
365
|
+
|
|
366
|
+
- Drop all type/scope prefixes from new identifiers.
|
|
367
|
+
- Name globals `CamelCase`; locals and parameters `camelCase`.
|
|
368
|
+
- Use `UPPER_SNAKE_CASE` for `new const` / `stock const` constants.
|
|
369
|
+
- Name player indices `playerIndex`.
|
|
370
|
+
- Mark every non-mutated parameter `const` (scalars included).
|
|
371
|
+
- Size player-indexed arrays `MAX_PLAYERS + 1`.
|
|
372
|
+
- Tag booleans `bool:` and use `true`/`false`.
|
|
373
|
+
- Name enums by role: `S_` struct, `E_` enumeration (`enum _:`), `T_` tag.
|
|
374
|
+
- Namespace type tags `T_<System>_<Name>` and give each a named
|
|
375
|
+
`Invalid_<System>_<Name> = 0` member.
|
|
376
|
+
- Register natives with the default style (style 0); prefer `@` + the native's
|
|
377
|
+
full name for the handler.
|
|
378
|
+
- Declare metadata with the reserved `PluginName` / `PluginVersion` /
|
|
379
|
+
`PluginAuthor` symbols, adding `PluginDescription` / `PluginURL` only when
|
|
380
|
+
they carry real information.
|
|
381
|
+
- Compile after any refactor and confirm no new warnings or errors.
|
|
382
|
+
|
|
383
|
+
## Must not do
|
|
384
|
+
|
|
385
|
+
- Do not introduce `g_`, `i`, `sz`, `b`, `f`, `p`, or `a` type/scope prefixes.
|
|
386
|
+
- Do not name a player index `id`.
|
|
387
|
+
- Do not use untagged `0`/`1` for a boolean.
|
|
388
|
+
- Do not size player-indexed arrays `MAX_PLAYERS`.
|
|
389
|
+
- Do not encode type in the name — put it in the tag.
|
|
390
|
+
- Do not blindly text-replace identifiers when refactoring.
|
|
391
|
+
- Do not propagate a legacy file's style into newly added code.
|
|
392
|
+
- Do not use an implicit `Tag:0`; use the named `Invalid_*` value.
|
|
393
|
+
- Do not use legacy native registration style 1, or write `public` on an `@`
|
|
394
|
+
handler.
|
|
395
|
+
- Do not name metadata `PLUGIN_NAME` / `PLUGIN_VERSION`; AMXX reads the exact
|
|
396
|
+
reserved symbols.
|
|
397
|
+
|
|
398
|
+
## Verification checklist
|
|
399
|
+
|
|
400
|
+
- [ ] No identifier carries a type or scope prefix.
|
|
401
|
+
- [ ] Globals `CamelCase`; locals and parameters `camelCase`.
|
|
402
|
+
- [ ] Constants `UPPER_SNAKE_CASE` in `new const` (or `stock const` in `.inc`).
|
|
403
|
+
- [ ] Player index named `playerIndex`.
|
|
404
|
+
- [ ] Every non-mutated parameter is `const`.
|
|
405
|
+
- [ ] Player-indexed arrays use `MAX_PLAYERS + 1`.
|
|
406
|
+
- [ ] Booleans use `bool:` and `true`/`false`.
|
|
407
|
+
- [ ] Enums prefixed by role; `E_` uses `enum _:` when consumed as integers.
|
|
408
|
+
- [ ] Type tags namespaced; each has a named `Invalid_*` zero value, no
|
|
409
|
+
`Tag:0`.
|
|
410
|
+
- [ ] Native handlers use `@` + native name (preferred); register with style 0
|
|
411
|
+
(required).
|
|
412
|
+
- [ ] Metadata uses the reserved `PluginName` / `PluginVersion` /
|
|
413
|
+
`PluginAuthor` symbols.
|
|
414
|
+
- [ ] Compiles with no new warnings or errors.
|
|
415
|
+
|
|
416
|
+
## Not yet covered (extend here)
|
|
417
|
+
|
|
418
|
+
Function/method naming, indentation and brace style, `#include` ordering,
|
|
419
|
+
statement formatting, and module layout are not specified yet. Plugin
|
|
420
|
+
lifecycle (`plugin_precache` as the boot hook, custom-forward orchestration)
|
|
421
|
+
is intentionally out of scope: it is architecture, not style, and belongs in a
|
|
422
|
+
separate skill.
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const glob = require('fast-glob');
|
|
6
|
+
const yaml = require('js-yaml');
|
|
7
|
+
|
|
8
|
+
const { fetchDepRoot } = require('./deps-resolver');
|
|
9
|
+
const { parseDocEntries, parseSkillEntries } = require('./manifest');
|
|
10
|
+
const { findManifestInDir } = require('./manifest-path');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* UTF-8 read that never throws. Binary files (NUL byte) and read errors are
|
|
14
|
+
* returned as descriptive placeholder strings instead.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} abs - absolute file path
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
function safeRead(abs) {
|
|
20
|
+
try {
|
|
21
|
+
const buf = fs.readFileSync(abs);
|
|
22
|
+
const text = buf.toString('utf8');
|
|
23
|
+
if (text.includes('\u0000')) return `[binary file, ${buf.length} bytes]`;
|
|
24
|
+
return text;
|
|
25
|
+
} catch (err) {
|
|
26
|
+
return `[error reading file: ${err.message}]`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolve a repo-relative path against the repo root, rejecting traversal.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} rootDir - repo root (absolute)
|
|
34
|
+
* @param {string} rel - declared relative path
|
|
35
|
+
* @returns {string} absolute path
|
|
36
|
+
*/
|
|
37
|
+
function safeResolve(rootDir, rel) {
|
|
38
|
+
const abs = path.resolve(rootDir, rel);
|
|
39
|
+
if (abs !== rootDir && !abs.startsWith(rootDir + path.sep)) {
|
|
40
|
+
throw new Error(`docs path escapes the repo root: "${rel}"`);
|
|
41
|
+
}
|
|
42
|
+
return abs;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function sortBundleFiles(files) {
|
|
46
|
+
return files.sort((a, b) => {
|
|
47
|
+
if (a === 'SKILL.md') return b === 'SKILL.md' ? 0 : -1;
|
|
48
|
+
if (b === 'SKILL.md') return 1;
|
|
49
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolve declared docs/skills against a repo root without reading content.
|
|
55
|
+
*
|
|
56
|
+
* Docs and single-file skills that do not exist land in `missing` (no throw);
|
|
57
|
+
* directory skills are enumerated recursively (SKILL.md first). Paths that
|
|
58
|
+
* escape rootDir throw.
|
|
59
|
+
*
|
|
60
|
+
* @param {{ docs?: Array<object>, skills?: Array<object> }} assets
|
|
61
|
+
* @param {string} rootDir - absolute repo root
|
|
62
|
+
* @returns {{ docs: Array<object>, skills: Array<object>, missing: string[] }}
|
|
63
|
+
*/
|
|
64
|
+
function resolveAssets({ docs, skills }, rootDir) {
|
|
65
|
+
const docInputs = Array.isArray(docs) ? docs : [];
|
|
66
|
+
const skillInputs = Array.isArray(skills) ? skills : [];
|
|
67
|
+
const outDocs = [];
|
|
68
|
+
const outSkills = [];
|
|
69
|
+
const missing = [];
|
|
70
|
+
|
|
71
|
+
for (const d of docInputs) {
|
|
72
|
+
const abs = safeResolve(rootDir, d.file);
|
|
73
|
+
if (fs.existsSync(abs) && fs.statSync(abs).isFile()) {
|
|
74
|
+
outDocs.push({ name: d.name, description: d.description, file: d.file, abs });
|
|
75
|
+
} else {
|
|
76
|
+
missing.push(d.file);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const s of skillInputs) {
|
|
81
|
+
if (s.file) {
|
|
82
|
+
const abs = safeResolve(rootDir, s.file);
|
|
83
|
+
if (fs.existsSync(abs) && fs.statSync(abs).isFile()) {
|
|
84
|
+
outSkills.push({ name: s.name, description: s.description, kind: 'file', file: s.file, dir: null, abs });
|
|
85
|
+
} else {
|
|
86
|
+
missing.push(s.file);
|
|
87
|
+
}
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const abs = safeResolve(rootDir, s.dir);
|
|
92
|
+
if (fs.existsSync(abs) && fs.statSync(abs).isDirectory()) {
|
|
93
|
+
const files = sortBundleFiles(glob.sync('**/*', { cwd: abs, dot: false, onlyFiles: true }));
|
|
94
|
+
outSkills.push({
|
|
95
|
+
name: s.name,
|
|
96
|
+
description: s.description,
|
|
97
|
+
kind: 'dir',
|
|
98
|
+
file: null,
|
|
99
|
+
dir: s.dir,
|
|
100
|
+
abs,
|
|
101
|
+
files: files.map((rel) => ({ rel, abs: path.join(abs, rel) })),
|
|
102
|
+
});
|
|
103
|
+
} else {
|
|
104
|
+
missing.push(s.dir);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { docs: outDocs, skills: outSkills, missing };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Attach file contents to a resolved assets object.
|
|
113
|
+
*
|
|
114
|
+
* @param {{ docs: Array<object>, skills: Array<object>, missing: string[] }} resolved
|
|
115
|
+
* @returns {{ docs: Array<object>, skills: Array<object>, missing: string[] }}
|
|
116
|
+
*/
|
|
117
|
+
function readAssets(resolved) {
|
|
118
|
+
return {
|
|
119
|
+
docs: resolved.docs.map((d) => ({ ...d, content: safeRead(d.abs) })),
|
|
120
|
+
skills: resolved.skills.map((s) => {
|
|
121
|
+
if (s.kind === 'dir') {
|
|
122
|
+
return { ...s, files: s.files.map((f) => ({ rel: f.rel, content: safeRead(f.abs) })) };
|
|
123
|
+
}
|
|
124
|
+
return { ...s, content: safeRead(s.abs) };
|
|
125
|
+
}),
|
|
126
|
+
missing: resolved.missing,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Fetch a dependency and read its own manifest, if any.
|
|
132
|
+
*
|
|
133
|
+
* Lenient: absence or unparseable YAML yields `manifestPath: null, raw: null`
|
|
134
|
+
* rather than throwing. `fetchRoot` is a test seam.
|
|
135
|
+
*
|
|
136
|
+
* @param {object} dep - parsed dep object
|
|
137
|
+
* @param {object} [opts]
|
|
138
|
+
* @param {string} [opts.token]
|
|
139
|
+
* @param {boolean} [opts.noFetch]
|
|
140
|
+
* @param {boolean} [opts.ssh]
|
|
141
|
+
* @param {Function} [opts.fetchRoot]
|
|
142
|
+
* @returns {Promise<{ label: string, rootDir: string, manifestPath: string|null, raw: object|null }>}
|
|
143
|
+
*/
|
|
144
|
+
async function readDepManifest(dep, { token, noFetch, ssh, fetchRoot } = {}) {
|
|
145
|
+
const fetch = fetchRoot || fetchDepRoot;
|
|
146
|
+
const { rootDir, label } = await fetch(dep, { token, noFetch, ssh });
|
|
147
|
+
|
|
148
|
+
const manifestPath = findManifestInDir(rootDir);
|
|
149
|
+
let raw = null;
|
|
150
|
+
if (manifestPath) {
|
|
151
|
+
try {
|
|
152
|
+
raw = yaml.load(fs.readFileSync(manifestPath, 'utf8')) || null;
|
|
153
|
+
} catch (_) {
|
|
154
|
+
raw = null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return { label, rootDir, manifestPath, raw };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Fetch a dependency and collect its declared agent docs/skills with content.
|
|
162
|
+
*
|
|
163
|
+
* @param {object} dep - parsed dep object
|
|
164
|
+
* @param {object} [opts] - same options as readDepManifest
|
|
165
|
+
* @returns {Promise<{ label: string, manifestPath: string|null, manifestName: string|null, docs: Array<object>, skills: Array<object>, missing: string[] }>}
|
|
166
|
+
*/
|
|
167
|
+
async function collectDepAssets(dep, opts) {
|
|
168
|
+
const m = await readDepManifest(dep, opts);
|
|
169
|
+
if (!m.raw) {
|
|
170
|
+
return { label: m.label, manifestPath: null, manifestName: null, docs: [], skills: [], missing: [] };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let docs;
|
|
174
|
+
let skills;
|
|
175
|
+
try { docs = parseDocEntries(m.raw.docs || []); } catch (_) { docs = []; }
|
|
176
|
+
try { skills = parseSkillEntries(m.raw.skills || []); } catch (_) { skills = []; }
|
|
177
|
+
|
|
178
|
+
const read = readAssets(resolveAssets({ docs, skills }, m.rootDir));
|
|
179
|
+
return {
|
|
180
|
+
label: m.label,
|
|
181
|
+
manifestPath: m.manifestPath,
|
|
182
|
+
manifestName: m.raw.name || null,
|
|
183
|
+
docs: read.docs,
|
|
184
|
+
skills: read.skills,
|
|
185
|
+
missing: read.missing,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Collect the local project's declared agent docs/skills with content.
|
|
191
|
+
*
|
|
192
|
+
* @param {object} manifest - parsed manifest (uses `manifest._path` for the root)
|
|
193
|
+
* @returns {{ docs: Array<object>, skills: Array<object>, missing: string[] }}
|
|
194
|
+
*/
|
|
195
|
+
function collectLocalAssets(manifest) {
|
|
196
|
+
const rootDir = path.dirname(manifest._path);
|
|
197
|
+
return readAssets(resolveAssets({ docs: manifest.docs, skills: manifest.skills }, rootDir));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
module.exports = {
|
|
201
|
+
resolveAssets,
|
|
202
|
+
readAssets,
|
|
203
|
+
readDepManifest,
|
|
204
|
+
collectDepAssets,
|
|
205
|
+
collectLocalAssets,
|
|
206
|
+
};
|