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.
Files changed (47) hide show
  1. package/AGENTS.md +13 -3
  2. package/README.md +276 -20
  3. package/action.yml +1 -1
  4. package/defaults/amxbuild.defaults.yml +7 -1
  5. package/mcp/handlers.js +342 -152
  6. package/mcp/registry.js +264 -25
  7. package/package.json +1 -1
  8. package/skills/amxb-migration/SKILL.md +126 -27
  9. package/skills/amxx-pawn-style/SKILL.md +422 -0
  10. package/src/agent-assets.js +206 -0
  11. package/src/asset-fetcher.js +33 -48
  12. package/src/build-plan.js +22 -5
  13. package/src/build-service.js +54 -11
  14. package/src/cli.js +38 -4
  15. package/src/collector.js +9 -1
  16. package/src/commands/build.js +5 -4
  17. package/src/commands/dry-run.js +28 -1
  18. package/src/commands/init.js +125 -11
  19. package/src/commands/opencode-skills.js +47 -0
  20. package/src/commands/serve.js +309 -111
  21. package/src/commands/skills-dir.js +21 -0
  22. package/src/commands/watch.js +66 -23
  23. package/src/compile-utils.js +119 -4
  24. package/src/compiler-fetcher.js +196 -41
  25. package/src/compiler.js +68 -29
  26. package/src/dep-graph.js +27 -0
  27. package/src/deployer.js +39 -15
  28. package/src/deps-resolver.js +151 -11
  29. package/src/deps-tree.js +20 -9
  30. package/src/download.js +132 -0
  31. package/src/fs-utils.js +35 -1
  32. package/src/fungun-fetcher.js +28 -8
  33. package/src/github-api.js +32 -0
  34. package/src/include-tree.js +30 -61
  35. package/src/ini-builder.js +1 -1
  36. package/src/jsonrpc-transport.js +53 -5
  37. package/src/local-sources.js +363 -0
  38. package/src/manifest-path.js +18 -1
  39. package/src/manifest.js +398 -40
  40. package/src/opencode-skills.js +262 -0
  41. package/src/release-fetcher.js +26 -33
  42. package/src/repo-fetcher.js +388 -37
  43. package/src/retry.js +3 -1
  44. package/src/schema.js +1 -1
  45. package/templates/init-deploy.env +9 -0
  46. package/templates/init-gitignore +22 -0
  47. 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
+ };