amxx-builder 1.5.2 → 1.6.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 +6 -0
- package/README.md +102 -2
- package/mcp/handlers.js +163 -26
- package/mcp/registry.js +125 -0
- package/package.json +2 -1
- package/skills/amxb-migration/SKILL.md +554 -0
- package/src/build-plan.js +3 -1
- package/src/cli.js +1 -0
- package/src/commands/deps-tree.js +4 -1
- package/src/commands/dry-run.js +4 -0
- package/src/commands/init.js +19 -18
- package/src/commands/serve.js +338 -25
- package/src/compiler-fetcher.js +34 -1
- package/src/dep-docs.js +115 -0
- package/src/dep-graph.js +23 -0
- package/src/deployer.js +11 -6
- package/src/deps-resolver.js +54 -3
- package/src/deps-tree.js +18 -0
- package/src/fungun-fetcher.js +347 -0
- package/src/github-api.js +309 -0
- package/src/include-tree.js +7 -2
- package/src/jsonrpc-transport.js +2 -1
- package/src/manifest.js +86 -5
- package/templates/init-workflow.yml +1 -1
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GitHub REST API helpers for repo-scope metadata (core, interface-agnostic).
|
|
5
|
+
*
|
|
6
|
+
* Used by the serve interface (`repos.info` / `repos.branches` /
|
|
7
|
+
* `repos.structure`) and potentially other interfaces (MCP) — per AGENTS.md
|
|
8
|
+
* the HTTP calls live here, not in the interface layer.
|
|
9
|
+
*
|
|
10
|
+
* Conventions:
|
|
11
|
+
* - HTTP errors are normalized to `GithubError` with a numeric `status`
|
|
12
|
+
* (HTTP status when known, `null` for network/timeout errors).
|
|
13
|
+
* - 404 from GitHub is deliberately NOT an error for the repo-scope methods:
|
|
14
|
+
* GitHub returns 404 both for nonexistent and for private/no-access repos,
|
|
15
|
+
* and the two cannot be distinguished even with a token. Callers get
|
|
16
|
+
* `{ exists: false, reason: 'not_found_or_no_access' }` instead.
|
|
17
|
+
* - Tokens are never resolved here — callers pass the resolved token
|
|
18
|
+
* (see `resolveGithubToken` in src/manifest.js).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const axios = require('axios');
|
|
22
|
+
// Same global default as release-lister; re-setting is idempotent and keeps
|
|
23
|
+
// this module's calls bounded even when loaded without release-lister.
|
|
24
|
+
axios.defaults.timeout = 30000;
|
|
25
|
+
|
|
26
|
+
/** "owner/repo" — no slashes inside the parts, no whitespace. */
|
|
27
|
+
const REPO_RE = /^[^/\s]+\/[^/\s]+$/;
|
|
28
|
+
|
|
29
|
+
class GithubError extends Error {
|
|
30
|
+
/**
|
|
31
|
+
* @param {string} message - error text (GitHub's message when available)
|
|
32
|
+
* @param {number|null} status - HTTP status, or null for network/timeout errors
|
|
33
|
+
*/
|
|
34
|
+
constructor(message, status) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = 'GithubError';
|
|
37
|
+
this.status = status == null ? null : status;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {unknown} repo
|
|
43
|
+
* @returns {boolean} true if `repo` looks like "owner/repo"
|
|
44
|
+
*/
|
|
45
|
+
function isValidRepo(repo) {
|
|
46
|
+
return typeof repo === 'string' && REPO_RE.test(repo);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Validate the filter/limit options of a repo-structure request.
|
|
51
|
+
*
|
|
52
|
+
* @param {{ depth?: unknown, dirsOnly?: unknown, ext?: unknown, maxEntries?: unknown }} options
|
|
53
|
+
* @returns {string|null} error message, or null when the options are valid
|
|
54
|
+
*/
|
|
55
|
+
function validateRepoStructureOptions({ depth, dirsOnly, ext, maxEntries } = {}) {
|
|
56
|
+
if (depth != null && (!Number.isInteger(depth) || depth < 1)) {
|
|
57
|
+
return 'Invalid "depth": expected a positive integer';
|
|
58
|
+
}
|
|
59
|
+
if (maxEntries != null && (!Number.isInteger(maxEntries) || maxEntries < 1 || maxEntries > 2000)) {
|
|
60
|
+
return 'Invalid "maxEntries": expected an integer in 1..2000';
|
|
61
|
+
}
|
|
62
|
+
if (ext != null && (!Array.isArray(ext) || ext.some((e) => typeof e !== 'string' || e.length === 0))) {
|
|
63
|
+
return 'Invalid "ext": expected an array of non-empty strings';
|
|
64
|
+
}
|
|
65
|
+
if (dirsOnly != null && typeof dirsOnly !== 'boolean') {
|
|
66
|
+
return 'Invalid "dirsOnly": expected a boolean';
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* GET a GitHub API URL with the same headers as release-lister
|
|
73
|
+
* (`Accept: application/vnd.github+json`, Bearer token when present).
|
|
74
|
+
* Non-2xx responses and network errors are normalized to `GithubError`.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} url
|
|
77
|
+
* @param {{ token?: string|null }} [options]
|
|
78
|
+
* @returns {Promise<any>} parsed JSON body
|
|
79
|
+
*/
|
|
80
|
+
async function githubGet(url, { token } = {}) {
|
|
81
|
+
const headers = { Accept: 'application/vnd.github+json' };
|
|
82
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const { data } = await axios.get(url, { headers });
|
|
86
|
+
return data;
|
|
87
|
+
} catch (err) {
|
|
88
|
+
if (err.response) {
|
|
89
|
+
const status = err.response.status;
|
|
90
|
+
const message = (err.response.data && err.response.data.message) || err.message;
|
|
91
|
+
throw new GithubError(message, status);
|
|
92
|
+
}
|
|
93
|
+
// No response: network error, DNS, timeout, ...
|
|
94
|
+
throw new GithubError(err && err.message ? err.message : String(err), null);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function notFoundResult(repo) {
|
|
99
|
+
return { repo, exists: false, reason: 'not_found_or_no_access' };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Repository existence + metadata.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} repo - "owner/repo"
|
|
106
|
+
* @param {{ token?: string|null }} [options]
|
|
107
|
+
* @returns {Promise<object>} `{ exists: true, ...metadata }` or
|
|
108
|
+
* `{ exists: false, reason: 'not_found_or_no_access' }` on 404.
|
|
109
|
+
* Throws `GithubError` for non-404 errors (403/429/5xx/network).
|
|
110
|
+
*/
|
|
111
|
+
async function getRepoInfo(repo, { token } = {}) {
|
|
112
|
+
let data;
|
|
113
|
+
try {
|
|
114
|
+
data = await githubGet(`https://api.github.com/repos/${repo}`, { token });
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (err instanceof GithubError && err.status === 404) return notFoundResult(repo);
|
|
117
|
+
throw err;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
repo,
|
|
122
|
+
exists: true,
|
|
123
|
+
private: data.private === true,
|
|
124
|
+
archived: data.archived === true,
|
|
125
|
+
disabled: data.disabled === true,
|
|
126
|
+
defaultBranch: data.default_branch || null,
|
|
127
|
+
description: data.description ?? null,
|
|
128
|
+
pushedAt: data.pushed_at || null,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Branch list with the commit SHA of each branch head.
|
|
134
|
+
*
|
|
135
|
+
* @param {string} repo - "owner/repo"
|
|
136
|
+
* @param {{ token?: string|null, limit?: number, page?: number }} [options]
|
|
137
|
+
* @returns {Promise<object>} `{ repo, branches: [{ name, commitSha }] }`,
|
|
138
|
+
* `{ repo, branches: [] }` for an empty repo (409), or
|
|
139
|
+
* `{ exists: false, reason: 'not_found_or_no_access' }` on 404.
|
|
140
|
+
* Throws `GithubError` for non-404/409 errors.
|
|
141
|
+
*/
|
|
142
|
+
async function listBranches(repo, { token, limit = 10, page = 1 } = {}) {
|
|
143
|
+
const perPage = Math.min(100, Math.max(1, limit));
|
|
144
|
+
const pageNum = Math.max(1, page);
|
|
145
|
+
|
|
146
|
+
let data;
|
|
147
|
+
try {
|
|
148
|
+
data = await githubGet(
|
|
149
|
+
`https://api.github.com/repos/${repo}/branches?per_page=${perPage}&page=${pageNum}`,
|
|
150
|
+
{ token }
|
|
151
|
+
);
|
|
152
|
+
} catch (err) {
|
|
153
|
+
if (err instanceof GithubError && err.status === 404) return notFoundResult(repo);
|
|
154
|
+
if (err instanceof GithubError && err.status === 409) {
|
|
155
|
+
// Empty repository (no commits yet) — no branches, not an error.
|
|
156
|
+
return { repo, branches: [] };
|
|
157
|
+
}
|
|
158
|
+
throw err;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
repo,
|
|
163
|
+
branches: data.map((b) => ({
|
|
164
|
+
name: b.name,
|
|
165
|
+
commitSha: (b.commit && b.commit.sha) || null,
|
|
166
|
+
})),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Filter a GitHub tree (from the Git Trees API) into the structure response.
|
|
172
|
+
* Pure function — exported for tests.
|
|
173
|
+
*
|
|
174
|
+
* @param {Array<{ path: string, type: string, mode?: string }>} tree
|
|
175
|
+
* @param {{ depth?: number, dirsOnly?: boolean, ext?: string[], maxEntries?: number }} [options]
|
|
176
|
+
* @returns {{ entries: Array<{ path: string, type: 'dir'|'file' }>, truncated: boolean }}
|
|
177
|
+
*/
|
|
178
|
+
function filterTreeEntries(tree, { depth, dirsOnly = false, ext, maxEntries = 500 } = {}) {
|
|
179
|
+
const cap = Math.max(1, Math.min(2000, maxEntries));
|
|
180
|
+
const suffixes = Array.isArray(ext) && ext.length > 0
|
|
181
|
+
? ext.map((e) => String(e).replace(/^\./, '').toLowerCase())
|
|
182
|
+
: null;
|
|
183
|
+
|
|
184
|
+
const entries = [];
|
|
185
|
+
let truncated = false;
|
|
186
|
+
|
|
187
|
+
for (const item of tree) {
|
|
188
|
+
if (entries.length >= cap) {
|
|
189
|
+
truncated = true;
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
// GitHub types: "blob" (file), "tree" (dir), "commit" (submodule) — submodules
|
|
193
|
+
// are skipped: they have no path content to copy.
|
|
194
|
+
const type = item.type === 'tree' ? 'dir' : item.type === 'blob' ? 'file' : null;
|
|
195
|
+
if (!type) continue;
|
|
196
|
+
if (dirsOnly && type !== 'dir') continue;
|
|
197
|
+
// "ext" restricts to matching files only — directories are filtered out too.
|
|
198
|
+
if (suffixes) {
|
|
199
|
+
if (type !== 'file') continue;
|
|
200
|
+
const lower = item.path.toLowerCase();
|
|
201
|
+
if (!suffixes.some((s) => lower.endsWith('.' + s))) continue;
|
|
202
|
+
}
|
|
203
|
+
if (depth != null && item.path.split('/').length > depth) continue;
|
|
204
|
+
|
|
205
|
+
entries.push({ path: item.path, type });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return { entries, truncated };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Repository file/folder structure via the Git Trees API
|
|
213
|
+
* (`GET /repos/{owner}/{repo}/git/trees/{tree_sha}?recursive=1` — one call for
|
|
214
|
+
* the whole tree; GitHub resolves branch/tag refs itself).
|
|
215
|
+
*
|
|
216
|
+
* When `ref` is omitted the default branch is resolved first via the repo
|
|
217
|
+
* endpoint — the same call also verifies repo existence, so no extra API call
|
|
218
|
+
* is spent on a separate existence check.
|
|
219
|
+
*
|
|
220
|
+
* @param {string} repo - "owner/repo"
|
|
221
|
+
* @param {{ token?: string|null, ref?: string|null, depth?: number, dirsOnly?: boolean,
|
|
222
|
+
* ext?: string[], maxEntries?: number }} [options]
|
|
223
|
+
* @returns {Promise<object>} `{ repo, ref, truncated, entries }`,
|
|
224
|
+
* `{ repo, ref, truncated: false, entries: [] }` for an empty repo (409), or
|
|
225
|
+
* `{ exists: false, reason: 'not_found_or_no_access' }` when the repo is
|
|
226
|
+
* gone/private. Throws `GithubError` for other errors — including a
|
|
227
|
+
* nonexistent `ref` on an existing repo (trees API answers 404, ambiguous
|
|
228
|
+
* with a missing repo; the repo endpoint is re-checked to disambiguate →
|
|
229
|
+
* `GithubError` 404 "Ref not found: <ref>").
|
|
230
|
+
*/
|
|
231
|
+
async function getRepoStructure(repo, { token, ref = null, depth, dirsOnly = false, ext, maxEntries = 500 } = {}) {
|
|
232
|
+
let treeSha = ref;
|
|
233
|
+
let usedRef = ref;
|
|
234
|
+
let repoVerified = false;
|
|
235
|
+
|
|
236
|
+
// Default depth: top level only — unlimited when ext is given (the
|
|
237
|
+
// exclude-files use case needs the whole tree). Guards against fetching
|
|
238
|
+
// the full tree of a huge repo when only a shallow listing is needed.
|
|
239
|
+
const effectiveDepth = depth != null ? depth : (Array.isArray(ext) && ext.length > 0 ? undefined : 1);
|
|
240
|
+
|
|
241
|
+
if (!treeSha) {
|
|
242
|
+
let info;
|
|
243
|
+
try {
|
|
244
|
+
info = await getRepoInfo(repo, { token });
|
|
245
|
+
} catch (err) {
|
|
246
|
+
throw err; // non-404 GithubError → caller decides how to surface it
|
|
247
|
+
}
|
|
248
|
+
if (!info.exists) return notFoundResult(repo);
|
|
249
|
+
if (!info.defaultBranch) {
|
|
250
|
+
// Repo without a default branch configured — nothing to list.
|
|
251
|
+
return { repo, ref: null, truncated: false, entries: [] };
|
|
252
|
+
}
|
|
253
|
+
treeSha = info.defaultBranch;
|
|
254
|
+
usedRef = info.defaultBranch;
|
|
255
|
+
repoVerified = true;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
let data;
|
|
259
|
+
try {
|
|
260
|
+
data = await githubGet(
|
|
261
|
+
`https://api.github.com/repos/${repo}/git/trees/${encodeURIComponent(treeSha)}?recursive=1`,
|
|
262
|
+
{ token }
|
|
263
|
+
);
|
|
264
|
+
} catch (err) {
|
|
265
|
+
if (err instanceof GithubError && err.status === 404) {
|
|
266
|
+
// The trees API answers 404 both for a missing/private repo AND for a
|
|
267
|
+
// nonexistent branch/tag (422 is only for malformed 40-hex SHAs). When
|
|
268
|
+
// the repo was not just verified, re-check its existence to tell the
|
|
269
|
+
// two apart.
|
|
270
|
+
if (!repoVerified) {
|
|
271
|
+
let info;
|
|
272
|
+
try {
|
|
273
|
+
info = await getRepoInfo(repo, { token });
|
|
274
|
+
} catch (checkErr) {
|
|
275
|
+
throw checkErr; // non-404 GithubError from the repo re-check
|
|
276
|
+
}
|
|
277
|
+
if (!info.exists) return notFoundResult(repo);
|
|
278
|
+
}
|
|
279
|
+
// Repo is alive (just verified, or verified while resolving the default
|
|
280
|
+
// branch) → the ref itself is the problem.
|
|
281
|
+
throw new GithubError(`Ref not found: ${treeSha}`, 404);
|
|
282
|
+
}
|
|
283
|
+
if (err instanceof GithubError && err.status === 409) {
|
|
284
|
+
// Empty repository (no commits yet) — no tree.
|
|
285
|
+
return { repo, ref: usedRef, truncated: false, entries: [] };
|
|
286
|
+
}
|
|
287
|
+
throw err;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const filtered = filterTreeEntries(data.tree || [], { depth: effectiveDepth, dirsOnly, ext, maxEntries });
|
|
291
|
+
return {
|
|
292
|
+
repo,
|
|
293
|
+
ref: usedRef,
|
|
294
|
+
// GitHub-side truncation (100k entries) also makes the answer incomplete.
|
|
295
|
+
truncated: data.truncated === true || filtered.truncated,
|
|
296
|
+
entries: filtered.entries,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
module.exports = {
|
|
301
|
+
GithubError,
|
|
302
|
+
isValidRepo,
|
|
303
|
+
validateRepoStructureOptions,
|
|
304
|
+
githubGet,
|
|
305
|
+
getRepoInfo,
|
|
306
|
+
listBranches,
|
|
307
|
+
getRepoStructure,
|
|
308
|
+
filterTreeEntries,
|
|
309
|
+
};
|
package/src/include-tree.js
CHANGED
|
@@ -30,7 +30,8 @@ const { parseManifest, parseDepsLines, resolveGithubToken } = require('./manifes
|
|
|
30
30
|
const { fetchCompiler, fetchLatestVersion } = require('./compiler-fetcher');
|
|
31
31
|
const { fetchRepo, resolveRefIfLatest, resolveRepoRefs } = require('./repo-fetcher');
|
|
32
32
|
const { fetchReleaseDep } = require('./release-fetcher');
|
|
33
|
-
const {
|
|
33
|
+
const { fetchFungunDep } = require('./fungun-fetcher');
|
|
34
|
+
const { normalize, depLabel } = require('./deps-resolver');
|
|
34
35
|
const { loadEnv } = require('./env');
|
|
35
36
|
const { resolveManifestPath } = require('./manifest-path');
|
|
36
37
|
|
|
@@ -614,7 +615,7 @@ async function buildIncludeTree(manifestPath, targetPath, options = {}) {
|
|
|
614
615
|
seenDeps.add(key);
|
|
615
616
|
try {
|
|
616
617
|
const depDir = await fetchDepIncludeDir(dep, tokenFor(dep.repo), noFetch, manifest.github.ssh);
|
|
617
|
-
graph.includeDirs.push({ path: depDir, label: `dep: ${dep
|
|
618
|
+
graph.includeDirs.push({ path: depDir, label: `dep: ${depLabel(dep)}` });
|
|
618
619
|
} catch (_) { /* skip unresolvable */ }
|
|
619
620
|
}
|
|
620
621
|
|
|
@@ -818,6 +819,10 @@ async function fetchDepIncludeDir(dep, token, noFetch, ssh = false) {
|
|
|
818
819
|
);
|
|
819
820
|
}
|
|
820
821
|
|
|
822
|
+
if (dep.source === 'fungun') {
|
|
823
|
+
return fetchFungunDep(dep, noFetch);
|
|
824
|
+
}
|
|
825
|
+
|
|
821
826
|
const resolvedRef = await resolveRefIfLatest(dep.ref, dep.repo, token);
|
|
822
827
|
const repoDir = await fetchRepo(dep.repo, resolvedRef, token, noFetch, ssh);
|
|
823
828
|
const candidates = dep.include_path
|
package/src/jsonrpc-transport.js
CHANGED
|
@@ -167,7 +167,8 @@ class JsonRpcServer {
|
|
|
167
167
|
result = await handler(params);
|
|
168
168
|
} catch (err) {
|
|
169
169
|
if (typeof err.code === 'number') {
|
|
170
|
-
|
|
170
|
+
// Handlers may attach structured details via err.data (e.g. GitHub status).
|
|
171
|
+
this.sendError(id, err.code, err.message || 'Error', err.data);
|
|
171
172
|
} else {
|
|
172
173
|
this.sendError(
|
|
173
174
|
id,
|
package/src/manifest.js
CHANGED
|
@@ -3,6 +3,7 @@ const yaml = require('js-yaml');
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
|
|
5
5
|
const { validateManifest: validateSchema } = require('./schema');
|
|
6
|
+
const { parsePluginRef } = require('./fungun-fetcher');
|
|
6
7
|
|
|
7
8
|
const DEFAULTS_PATH = path.join(__dirname, '..', 'defaults', 'amxbuild.defaults.yml');
|
|
8
9
|
|
|
@@ -171,25 +172,105 @@ function makeRepo(r, globalPostfix, globalAmxDir) {
|
|
|
171
172
|
*/
|
|
172
173
|
const DEP_STRING_RE = /^([^@\s]+)@([^:\s]+)(?::(.+))?$/;
|
|
173
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Normalize a `docs` value (long-form dep entries only) to an array of
|
|
177
|
+
* trimmed, non-empty doc paths, or null when nothing is specified.
|
|
178
|
+
*
|
|
179
|
+
* @param {*} val
|
|
180
|
+
* @returns {string[]|null}
|
|
181
|
+
*/
|
|
182
|
+
function normalizeDocs(val) {
|
|
183
|
+
if (val == null) return null;
|
|
184
|
+
const list = Array.isArray(val) ? val.map(String) : [String(val)];
|
|
185
|
+
const trimmed = list.map((s) => s.trim()).filter(Boolean);
|
|
186
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
187
|
+
}
|
|
188
|
+
|
|
174
189
|
/**
|
|
175
190
|
* Parse a long-form dep object (manifest `deps` entries).
|
|
176
191
|
*
|
|
192
|
+
* git / release entries: `{ repo, ref, source?, include_path?, asset?, docs? }`
|
|
193
|
+
* fungun entries: `{ source: 'fungun', id: <index> }` or
|
|
194
|
+
* `{ source: 'fungun', url: <page link> }`
|
|
195
|
+
*
|
|
177
196
|
* @param {object} line
|
|
178
|
-
* @returns {{ repo: string, ref: string, include_path: string|null, source: string, asset:
|
|
197
|
+
* @returns {{ repo: string, ref: string|null, include_path: string|null, source: string, asset: *, docs: string[]|null }}
|
|
179
198
|
*/
|
|
180
199
|
function parseDepObject(line) {
|
|
181
|
-
if (!line.repo) throw new Error(`Dep entry missing "repo": ${JSON.stringify(line)}`);
|
|
182
|
-
if (!line.ref) throw new Error(`Dep entry missing "ref": ${JSON.stringify(line)}`);
|
|
183
200
|
const source = line.source || 'git';
|
|
201
|
+
|
|
202
|
+
if (source === 'fungun') return parseFungunDepObject(line);
|
|
203
|
+
|
|
184
204
|
if (!['git', 'release'].includes(source)) {
|
|
185
|
-
throw new Error(`Dep entry "source" must be "git" or "
|
|
205
|
+
throw new Error(`Dep entry "source" must be "git", "release" or "fungun": ${JSON.stringify(line)}`);
|
|
206
|
+
}
|
|
207
|
+
if (line.id != null || line.url != null) {
|
|
208
|
+
throw new Error(`Dep entry "id"/"url" are only valid with "source: fungun": ${JSON.stringify(line)}`);
|
|
186
209
|
}
|
|
210
|
+
if (!line.repo) throw new Error(`Dep entry missing "repo": ${JSON.stringify(line)}`);
|
|
211
|
+
if (!line.ref) throw new Error(`Dep entry missing "ref": ${JSON.stringify(line)}`);
|
|
187
212
|
return {
|
|
188
213
|
repo: String(line.repo).trim(),
|
|
189
214
|
ref: String(line.ref).trim(),
|
|
190
215
|
include_path: line.include_path ? String(line.include_path).trim() : null,
|
|
191
216
|
source,
|
|
192
217
|
asset: line.asset != null ? line.asset : null,
|
|
218
|
+
docs: normalizeDocs(line.docs),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Parse a long-form fungun dep. fungun.net plugins are closed-source and are
|
|
224
|
+
* addressed by their shop page index (`id`) or page URL (`url`) — there is no
|
|
225
|
+
* GitHub repo/ref. The parsed object carries a synthetic stable `repo`
|
|
226
|
+
* (`fungun.net/<id>`) so shared dep-dedup / dest-dir / cache-key logic in
|
|
227
|
+
* deps-resolver and include-tree keeps working unchanged.
|
|
228
|
+
*/
|
|
229
|
+
function parseFungunDepObject(line) {
|
|
230
|
+
const rawId = line.id != null ? String(line.id).trim() : '';
|
|
231
|
+
const rawUrl = line.url != null ? String(line.url).trim() : '';
|
|
232
|
+
const hasId = rawId !== '';
|
|
233
|
+
const hasUrl = rawUrl !== '';
|
|
234
|
+
|
|
235
|
+
if (hasId && hasUrl) {
|
|
236
|
+
throw new Error(
|
|
237
|
+
`Dep entry source "fungun" — give either "id" or "url", not both: ${JSON.stringify(line)}`
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
if (!hasId && !hasUrl) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
`Dep entry source "fungun" requires "id" (plugin index) or "url" (page link): ${JSON.stringify(line)}`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
if (line.include_path != null || line.asset != null) {
|
|
246
|
+
throw new Error(
|
|
247
|
+
`Dep entry source "fungun" does not support include_path/asset — ` +
|
|
248
|
+
`the .inc files come from the plugin page: ${JSON.stringify(line)}`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
if (line.repo != null || line.ref != null) {
|
|
252
|
+
throw new Error(
|
|
253
|
+
`Dep entry source "fungun" does not support repo/ref — ` +
|
|
254
|
+
`address the plugin by "id" or "url": ${JSON.stringify(line)}`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
let ref;
|
|
259
|
+
try {
|
|
260
|
+
ref = parsePluginRef(hasId ? rawId : rawUrl);
|
|
261
|
+
} catch (err) {
|
|
262
|
+
throw new Error(`Dep entry source "fungun": ${err.message}`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
repo: `fungun.net/${ref.id}`,
|
|
267
|
+
ref: null,
|
|
268
|
+
source: 'fungun',
|
|
269
|
+
id: ref.id,
|
|
270
|
+
url: ref.url,
|
|
271
|
+
include_path: null,
|
|
272
|
+
asset: null,
|
|
273
|
+
docs: normalizeDocs(line.docs),
|
|
193
274
|
};
|
|
194
275
|
}
|
|
195
276
|
|
|
@@ -370,4 +451,4 @@ function resolveManifest(manifestPath, options = {}) {
|
|
|
370
451
|
return manifest;
|
|
371
452
|
}
|
|
372
453
|
|
|
373
|
-
module.exports = { parseManifest, parseDepsLines, parseDepString, parseDepObject, applyOverrides, parseOverrideValue, resolveManifest, resolveGithubToken, loadDefaultsRaw, deepMerge };
|
|
454
|
+
module.exports = { parseManifest, parseDepsLines, parseDepString, parseDepObject, normalizeDocs, applyOverrides, parseOverrideValue, resolveManifest, resolveGithubToken, loadDefaultsRaw, deepMerge };
|