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/deps-tree.js
CHANGED
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
const fs = require('fs');
|
|
17
17
|
const path = require('path');
|
|
18
18
|
|
|
19
|
-
const { fetchRepo, resolveRef } = require('./repo-fetcher');
|
|
19
|
+
const { fetchRepo, resolveRef, applicableRefTtl } = require('./repo-fetcher');
|
|
20
20
|
const { parseDepsLines } = require('./manifest');
|
|
21
21
|
const { normalize } = require('./deps-resolver');
|
|
22
|
+
const { isLocal } = require('./local-sources');
|
|
22
23
|
|
|
23
24
|
// ─── Public API ────────────────────────────────────────────────────────────────
|
|
24
25
|
|
|
@@ -128,11 +129,17 @@ async function walkDep(dep, ctx) {
|
|
|
128
129
|
// ── Resolve ref (e.g. "latest" → concrete tag) ──────────────────────────
|
|
129
130
|
let resolvedRef;
|
|
130
131
|
let refError = null;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
resolvedRef =
|
|
135
|
-
|
|
132
|
+
if (isLocal(dep)) {
|
|
133
|
+
// Local sources have no remote ref to resolve — never touch the network.
|
|
134
|
+
// The 'local' sentinel is truthy, so a local repo's DEPS_LIST still expands.
|
|
135
|
+
resolvedRef = 'local';
|
|
136
|
+
} else {
|
|
137
|
+
try {
|
|
138
|
+
resolvedRef = await resolveRef(repo, dep.ref, token);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
resolvedRef = null;
|
|
141
|
+
refError = err.message;
|
|
142
|
+
}
|
|
136
143
|
}
|
|
137
144
|
|
|
138
145
|
// ── Cycle vs shared detection ───────────────────────────────────────────
|
|
@@ -181,7 +188,8 @@ async function walkDep(dep, ctx) {
|
|
|
181
188
|
repo,
|
|
182
189
|
ref: dep.ref || null,
|
|
183
190
|
resolvedRef,
|
|
184
|
-
source: dep.source || 'git',
|
|
191
|
+
source: isLocal(dep) ? 'local' : (dep.source || 'git'),
|
|
192
|
+
localDir: dep._localDir || null,
|
|
185
193
|
include_path: dep.include_path || null,
|
|
186
194
|
asset: dep.asset != null ? dep.asset : null,
|
|
187
195
|
from: ctx.from,
|
|
@@ -204,7 +212,9 @@ async function getSubDeps(dep, resolvedRef, token, noFetch, getDepsOverride) {
|
|
|
204
212
|
}
|
|
205
213
|
|
|
206
214
|
// 2. Clone repo (or use cache) and read DEPS_LIST
|
|
207
|
-
const repoDir =
|
|
215
|
+
const repoDir = isLocal(dep)
|
|
216
|
+
? dep._localDir
|
|
217
|
+
: await fetchRepo(dep.repo, resolvedRef, token, noFetch, false, applicableRefTtl(dep.ref, dep.ref_ttl));
|
|
208
218
|
const depsPath = path.join(repoDir, 'DEPS_LIST');
|
|
209
219
|
|
|
210
220
|
if (!fs.existsSync(depsPath)) {
|
package/src/github-api.js
CHANGED
|
@@ -95,6 +95,37 @@ async function githubGet(url, { token } = {}) {
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Determine the kind of a ref: `'tag'`, `'branch'`, or `null` when neither
|
|
100
|
+
* namespace has it.
|
|
101
|
+
*
|
|
102
|
+
* Tags are probed first: when a name exists as both a tag and a branch, the
|
|
103
|
+
* immutable tag wins (a source pinned to a tag must not follow a branch).
|
|
104
|
+
* 404 from both namespaces → `null` (no such ref); any other GitHub error
|
|
105
|
+
* propagates as `GithubError` so a network/rate-limit failure is never
|
|
106
|
+
* mistaken for a successful classification.
|
|
107
|
+
*
|
|
108
|
+
* @param {string} repo - "owner/repo"
|
|
109
|
+
* @param {string} ref - branch/tag name (slashes stay path separators)
|
|
110
|
+
* @param {{ token?: string|null }} [options]
|
|
111
|
+
* @returns {Promise<'tag'|'branch'|null>}
|
|
112
|
+
*/
|
|
113
|
+
async function getRefKind(repo, ref, { token } = {}) {
|
|
114
|
+
// Slashes in the ref (feature/foo) stay path separators; each segment is
|
|
115
|
+
// encoded so '#', spaces, '?' in a ref name cannot break the URL.
|
|
116
|
+
const enc = String(ref).split('/').map(encodeURIComponent).join('/');
|
|
117
|
+
for (const [kind, ns] of [['tag', 'tags'], ['branch', 'heads']]) {
|
|
118
|
+
try {
|
|
119
|
+
await githubGet(`https://api.github.com/repos/${repo}/git/ref/${ns}/${enc}`, { token });
|
|
120
|
+
return kind;
|
|
121
|
+
} catch (err) {
|
|
122
|
+
if (err instanceof GithubError && err.status === 404) continue;
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
98
129
|
function notFoundResult(repo) {
|
|
99
130
|
return { repo, exists: false, reason: 'not_found_or_no_access' };
|
|
100
131
|
}
|
|
@@ -302,6 +333,7 @@ module.exports = {
|
|
|
302
333
|
isValidRepo,
|
|
303
334
|
validateRepoStructureOptions,
|
|
304
335
|
githubGet,
|
|
336
|
+
getRefKind,
|
|
305
337
|
getRepoInfo,
|
|
306
338
|
listBranches,
|
|
307
339
|
getRepoStructure,
|
package/src/include-tree.js
CHANGED
|
@@ -29,6 +29,7 @@ const glob = require('fast-glob');
|
|
|
29
29
|
const { parseManifest, resolveGithubToken } = require('./manifest');
|
|
30
30
|
const { fetchCompiler, fetchLatestVersion } = require('./compiler-fetcher');
|
|
31
31
|
const repoFetcher = require('./repo-fetcher');
|
|
32
|
+
const { ensureRepoDir } = require('./local-sources');
|
|
32
33
|
const { normalize, normalizeRepo, depLabel, fetchDepIncludeDir, readDepsListFile } = require('./deps-resolver');
|
|
33
34
|
const { loadEnv } = require('./env');
|
|
34
35
|
const { resolveManifestPath } = require('./manifest-path');
|
|
@@ -593,7 +594,8 @@ async function buildIncludeTree(manifestPath, targetPath, options = {}) {
|
|
|
593
594
|
if (repoDirs.has(key)) return repoDirs.get(key);
|
|
594
595
|
let dir = null;
|
|
595
596
|
try {
|
|
596
|
-
|
|
597
|
+
// ensureRepoDir short-circuits to `_localDir` for local entries.
|
|
598
|
+
dir = await ensureRepoDir(repoConfig, { token: tokenFor(repoConfig.repo), noFetch, ssh: manifest.github.ssh });
|
|
597
599
|
} catch (_) { /* unresolvable — cached as null below */ }
|
|
598
600
|
repoDirs.set(key, dir);
|
|
599
601
|
return dir;
|
package/src/ini-builder.js
CHANGED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local sources: let `repos:` / `deps:` entries be satisfied by a local
|
|
3
|
+
* directory instead of GitHub, and let an env var redirect existing entries to
|
|
4
|
+
* local dirs without editing the manifest.
|
|
5
|
+
*
|
|
6
|
+
* Two ways to point an entry at a local dir:
|
|
7
|
+
*
|
|
8
|
+
* 1. Declared in the manifest — a repo/dep entry with `source: local` carries
|
|
9
|
+
* `_localPathRaw` after parsing. `applyLocalOverrides` resolves it to an
|
|
10
|
+
* absolute `_localDir`.
|
|
11
|
+
* 2. Redirected by env — `AMXB_LOCAL_SOURCES` maps an entry's logical id
|
|
12
|
+
* (`owner/repo` or `local/<name>`) to a path, overriding whatever the
|
|
13
|
+
* manifest declared. `AMXB_LOCAL_STRICT=1` turns an id with no matching
|
|
14
|
+
* entry from a warning into an error.
|
|
15
|
+
*
|
|
16
|
+
* Runtime discrimination is `entry._localDir` — consumers branch on it, not on
|
|
17
|
+
* the `source` field. `_resolvedRef` is set to the sentinel `'local'` so the
|
|
18
|
+
* shared dep-dedup / cache-key logic keeps working unchanged.
|
|
19
|
+
*
|
|
20
|
+
* Dependency-light on purpose: this module is imported by `./manifest`, so it
|
|
21
|
+
* must NOT top-level require `./manifest` or `./deps-resolver` (cycle).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
|
|
27
|
+
const logger = require('./logger');
|
|
28
|
+
|
|
29
|
+
const ENV_VAR = 'AMXB_LOCAL_SOURCES';
|
|
30
|
+
const STRICT_VAR = 'AMXB_LOCAL_STRICT';
|
|
31
|
+
const LOCAL_ID_PREFIX = 'local/';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Normalize a logical id for map lookups. Local copy of deps-resolver's
|
|
35
|
+
* `normalize` — a top-level require of that module would create a cycle.
|
|
36
|
+
*
|
|
37
|
+
* @param {*} id
|
|
38
|
+
* @returns {string}
|
|
39
|
+
*/
|
|
40
|
+
function normalizeId(id) {
|
|
41
|
+
return String(id).toLowerCase();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* True when an entry has been resolved to a local directory.
|
|
46
|
+
*
|
|
47
|
+
* @param {object} entry — a repo/dep config
|
|
48
|
+
* @returns {boolean}
|
|
49
|
+
*/
|
|
50
|
+
function isLocal(entry) {
|
|
51
|
+
return !!entry && typeof entry._localDir === 'string' && !!entry._localDir;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The absolute local directory of an entry, or null when it is not local.
|
|
56
|
+
*
|
|
57
|
+
* @param {object} entry — a repo/dep config
|
|
58
|
+
* @returns {string|null}
|
|
59
|
+
*/
|
|
60
|
+
function localDirOf(entry) {
|
|
61
|
+
return (entry && entry._localDir) || null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Build the synthetic logical id for a local repo/dep. Uses the explicit
|
|
66
|
+
* `name` when provided, otherwise the path's basename — mirroring how the
|
|
67
|
+
* fungun parser synthesizes `fungun.net/<id>` so shared dedup/cache-key logic
|
|
68
|
+
* keeps working.
|
|
69
|
+
*
|
|
70
|
+
* @param {*} name — explicit name (optional)
|
|
71
|
+
* @param {string} rawPath — the local path
|
|
72
|
+
* @returns {string}
|
|
73
|
+
*/
|
|
74
|
+
function synthesizeLocalId(name, rawPath) {
|
|
75
|
+
const trimmed = typeof name === 'string' ? name.trim() : '';
|
|
76
|
+
if (trimmed !== '') return LOCAL_ID_PREFIX + trimmed;
|
|
77
|
+
return LOCAL_ID_PREFIX + path.basename(String(rawPath));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Resolve a manifest-relative (or absolute) local path to an absolute,
|
|
82
|
+
* normalized path.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} manifestDir — directory containing the manifest
|
|
85
|
+
* @param {string} p — path from the manifest / env
|
|
86
|
+
* @returns {string}
|
|
87
|
+
*/
|
|
88
|
+
function resolveLocalPath(manifestDir, p) {
|
|
89
|
+
const str = String(p);
|
|
90
|
+
return path.isAbsolute(str) ? path.normalize(str) : path.resolve(manifestDir, str);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Parse `AMXB_LOCAL_SOURCES` into an id → path map.
|
|
95
|
+
*
|
|
96
|
+
* Accepted shapes:
|
|
97
|
+
* - JSON object: `{"owner/repo": "path", ...}` (only when the trimmed value
|
|
98
|
+
* starts with `{`)
|
|
99
|
+
* - `id=path` pairs separated by `;` or newlines; blank lines and `#`
|
|
100
|
+
* comments are skipped.
|
|
101
|
+
*
|
|
102
|
+
* Invalid input is collected in `errors` — this function never throws.
|
|
103
|
+
*
|
|
104
|
+
* @param {object} [env] — environment map (default: process.env)
|
|
105
|
+
* @returns {{ map: Map<string, string>, strict: boolean, errors: string[] }}
|
|
106
|
+
*/
|
|
107
|
+
function parseLocalSourcesEnv(env = process.env) {
|
|
108
|
+
const map = new Map();
|
|
109
|
+
const errors = [];
|
|
110
|
+
const value = env[ENV_VAR] == null ? '' : String(env[ENV_VAR]);
|
|
111
|
+
const trimmed = value.trim();
|
|
112
|
+
|
|
113
|
+
if (trimmed !== '') {
|
|
114
|
+
if (trimmed.startsWith('{')) {
|
|
115
|
+
let parsed;
|
|
116
|
+
try {
|
|
117
|
+
parsed = JSON.parse(trimmed);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
errors.push(`Invalid JSON in ${ENV_VAR}: ${err.message}`);
|
|
120
|
+
}
|
|
121
|
+
if (parsed !== undefined) {
|
|
122
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
123
|
+
errors.push(`${ENV_VAR}: JSON value must be an object of "id": "path" pairs`);
|
|
124
|
+
} else {
|
|
125
|
+
for (const [key, val] of Object.entries(parsed)) {
|
|
126
|
+
const id = normalizeId(key).trim();
|
|
127
|
+
if (id === '') {
|
|
128
|
+
errors.push(`${ENV_VAR}: entry has an empty id`);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (typeof val !== 'string' || val.trim() === '') {
|
|
132
|
+
errors.push(`${ENV_VAR}: value for "${key}" must be a non-empty string path`);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
map.set(id, val.trim());
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
for (const rawEntry of trimmed.split(/\n|;/)) {
|
|
141
|
+
const entry = rawEntry.trim();
|
|
142
|
+
if (entry === '' || entry.startsWith('#')) continue;
|
|
143
|
+
const eqIdx = entry.indexOf('=');
|
|
144
|
+
if (eqIdx === -1) {
|
|
145
|
+
errors.push(`${ENV_VAR}: invalid entry "${entry}" (expected id=path)`);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const id = normalizeId(entry.slice(0, eqIdx)).trim();
|
|
149
|
+
const p = entry.slice(eqIdx + 1).trim();
|
|
150
|
+
if (id === '' || p === '') {
|
|
151
|
+
errors.push(`${ENV_VAR}: invalid entry "${entry}" (id and path must be non-empty)`);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
map.set(id, p);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const strict = ['1', 'true'].includes(String(env[STRICT_VAR]).toLowerCase());
|
|
160
|
+
return { map, strict, errors };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Throw a uniform error when a resolved local path is missing or not a dir.
|
|
165
|
+
*
|
|
166
|
+
* @param {string} absPath
|
|
167
|
+
* @param {string} id
|
|
168
|
+
* @param {string} context — human-readable note appended to the message
|
|
169
|
+
*/
|
|
170
|
+
function assertLocalDir(absPath, id, context) {
|
|
171
|
+
let stat = null;
|
|
172
|
+
try { stat = fs.statSync(absPath); } catch { /* missing */ }
|
|
173
|
+
if (!stat || !stat.isDirectory()) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`Local source for "${id}" not found or not a directory: ${absPath} ${context}`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Collect every repo/dep entry that can carry a local path: manifest repos,
|
|
182
|
+
* manifest global deps, and each repo's `deps_override` entries. `makeRepo`
|
|
183
|
+
* parses `deps_override` through `parseDepsLines` → `parseLocalDepObject`, so a
|
|
184
|
+
* local dep there carries `_localPathRaw` exactly like a global one — it must be
|
|
185
|
+
* resolved too, or consumers fall through to the git branch.
|
|
186
|
+
*
|
|
187
|
+
* `DEPS_LIST` files are string-only and git-only — deliberately not included.
|
|
188
|
+
*
|
|
189
|
+
* @param {object} manifest
|
|
190
|
+
* @returns {object[]}
|
|
191
|
+
*/
|
|
192
|
+
function localCandidateEntries(manifest) {
|
|
193
|
+
const repos = Array.isArray(manifest.repos) ? manifest.repos : [];
|
|
194
|
+
const entries = [
|
|
195
|
+
...repos,
|
|
196
|
+
...(Array.isArray(manifest.globalDeps) ? manifest.globalDeps : []),
|
|
197
|
+
];
|
|
198
|
+
for (const repo of repos) {
|
|
199
|
+
if (repo && Array.isArray(repo.deps_override)) entries.push(...repo.deps_override);
|
|
200
|
+
}
|
|
201
|
+
return entries;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Resolve a parsed entry's declared `_localPathRaw` against `baseDir`, assert
|
|
206
|
+
* the directory exists, and mark it local. Idempotent: an entry already local
|
|
207
|
+
* (`_localDir` set) is returned untouched, and an entry with no declared path is
|
|
208
|
+
* a no-op.
|
|
209
|
+
*
|
|
210
|
+
* Single source of truth for interfaces (MCP, serve) that receive an ad-hoc dep
|
|
211
|
+
* object with `source: local` + `path` — they must not re-implement path handling.
|
|
212
|
+
*
|
|
213
|
+
* @param {object} entry — a parsed repo/dep config (may carry `_localPathRaw`)
|
|
214
|
+
* @param {string} baseDir — directory the raw path is relative to
|
|
215
|
+
* @returns {object} the same entry
|
|
216
|
+
*/
|
|
217
|
+
function resolveLocalEntry(entry, baseDir) {
|
|
218
|
+
if (!entry || entry._localPathRaw == null || isLocal(entry)) return entry;
|
|
219
|
+
const absPath = resolveLocalPath(baseDir, entry._localPathRaw);
|
|
220
|
+
assertLocalDir(absPath, entry.repo, '(local source)');
|
|
221
|
+
entry._localDir = absPath;
|
|
222
|
+
entry._resolvedRef = 'local';
|
|
223
|
+
return entry;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Throw when one logical id is claimed by two local entries pointing at
|
|
228
|
+
* DIFFERENT directories. Two entries resolving to the same directory are a
|
|
229
|
+
* harmless dedup (the build treats them as one source); different dirs mean one
|
|
230
|
+
* source would be silently dropped — the synthetic `local/<name>` id collided,
|
|
231
|
+
* so the user must give one a unique `name`.
|
|
232
|
+
*
|
|
233
|
+
* @param {object[]} entries
|
|
234
|
+
*/
|
|
235
|
+
function assertUniqueLocalIds(entries) {
|
|
236
|
+
const byId = new Map();
|
|
237
|
+
for (const entry of entries) {
|
|
238
|
+
if (!entry || !entry.repo || !isLocal(entry)) continue;
|
|
239
|
+
const id = normalizeId(entry.repo);
|
|
240
|
+
if (!byId.has(id)) byId.set(id, []);
|
|
241
|
+
byId.get(id).push(entry);
|
|
242
|
+
}
|
|
243
|
+
for (const [id, group] of byId) {
|
|
244
|
+
const dirs = [...new Set(group.map((e) => e._localDir))].sort();
|
|
245
|
+
if (dirs.length > 1) {
|
|
246
|
+
throw new Error(
|
|
247
|
+
`Duplicate local source id "${id}": ${dirs.map((d) => `"${d}"`).join(' and ')} ` +
|
|
248
|
+
`are different directories — set a unique "name" on one of them.`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Resolve declared local paths to absolute `_localDir`s and apply env
|
|
256
|
+
* redirects. Mutates `manifest` in place and returns it.
|
|
257
|
+
*
|
|
258
|
+
* Precedence (highest first):
|
|
259
|
+
* 1. `AMXB_LOCAL_SOURCES` redirect for the entry's logical id
|
|
260
|
+
* 2. the entry's own declared `_localPathRaw`
|
|
261
|
+
*
|
|
262
|
+
* Env is parsed before resolution so a redirect can rescue a stale/missing
|
|
263
|
+
* declared path. Unknown env ids are fatal under `AMXB_LOCAL_STRICT=1` (a
|
|
264
|
+
* warning otherwise), and duplicate synthetic ids pointing at different dirs
|
|
265
|
+
* are rejected.
|
|
266
|
+
*
|
|
267
|
+
* @param {object} manifest — parsed manifest (must carry `_path`)
|
|
268
|
+
* @param {object} [env] — environment map (default: process.env)
|
|
269
|
+
* @returns {object} the same manifest
|
|
270
|
+
*/
|
|
271
|
+
function applyLocalOverrides(manifest, env = process.env) {
|
|
272
|
+
const manifestDir = path.dirname(manifest._path);
|
|
273
|
+
const entries = localCandidateEntries(manifest);
|
|
274
|
+
|
|
275
|
+
// Parse env first: a redirect must win over (and thus avoid asserting) a
|
|
276
|
+
// declared path that no longer exists.
|
|
277
|
+
const { map, strict, errors } = parseLocalSourcesEnv(env);
|
|
278
|
+
for (const message of errors) logger.warn(message);
|
|
279
|
+
|
|
280
|
+
// Unknown env ids keep their strict/warn behaviour. Known ids are collected
|
|
281
|
+
// from every repo/dep entry, including deps_override.
|
|
282
|
+
const knownIds = new Set();
|
|
283
|
+
for (const entry of entries) {
|
|
284
|
+
if (entry && entry.repo) knownIds.add(normalizeId(entry.repo));
|
|
285
|
+
}
|
|
286
|
+
for (const id of map.keys()) {
|
|
287
|
+
if (knownIds.has(id)) continue;
|
|
288
|
+
if (strict) {
|
|
289
|
+
throw new Error(
|
|
290
|
+
`No repo/dep with id "${id}" in this manifest (${STRICT_VAR} is set)`
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
logger.warn(`no repo/dep with id ${id} in this manifest — ignored`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Resolve each entry: env redirect (highest precedence) over declared path.
|
|
297
|
+
for (const entry of entries) {
|
|
298
|
+
if (!entry || !entry.repo) continue;
|
|
299
|
+
const id = normalizeId(entry.repo);
|
|
300
|
+
|
|
301
|
+
const envPath = map.get(id);
|
|
302
|
+
if (envPath != null) {
|
|
303
|
+
const absPath = resolveLocalPath(manifestDir, envPath);
|
|
304
|
+
assertLocalDir(absPath, id, `(${ENV_VAR})`);
|
|
305
|
+
entry._localDir = absPath;
|
|
306
|
+
entry._resolvedRef = 'local';
|
|
307
|
+
logger.warn(`${ENV_VAR}: ${id} -> ${absPath} (local override)`);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (entry._localPathRaw == null) continue;
|
|
312
|
+
const absPath = resolveLocalPath(manifestDir, entry._localPathRaw);
|
|
313
|
+
assertLocalDir(absPath, entry.repo, '(declared in manifest)');
|
|
314
|
+
entry._localDir = absPath;
|
|
315
|
+
entry._resolvedRef = 'local';
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
assertUniqueLocalIds(entries);
|
|
319
|
+
|
|
320
|
+
return manifest;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Ensure an entry's directory exists locally and return its path.
|
|
325
|
+
*
|
|
326
|
+
* Local entries short-circuit to `_localDir` — no network, no `--no-fetch`
|
|
327
|
+
* check. Non-local entries delegate to the repo fetcher (lazy-required to
|
|
328
|
+
* avoid a cycle: repo-fetcher/deps-resolver import each other).
|
|
329
|
+
*
|
|
330
|
+
* @param {object} repoConfig — a repo/dep config
|
|
331
|
+
* @param {object} [opts]
|
|
332
|
+
* @param {string|null} [opts.token]
|
|
333
|
+
* @param {boolean} [opts.noFetch]
|
|
334
|
+
* @param {boolean} [opts.ssh]
|
|
335
|
+
* @returns {Promise<string>} absolute directory path
|
|
336
|
+
*/
|
|
337
|
+
async function ensureRepoDir(repoConfig, { token, noFetch, ssh } = {}) {
|
|
338
|
+
if (isLocal(repoConfig)) return repoConfig._localDir;
|
|
339
|
+
|
|
340
|
+
// Lazy require: keep this module dependency-light and cycle-free.
|
|
341
|
+
const { fetchRepo, applicableRefTtl } = require('./repo-fetcher');
|
|
342
|
+
const ref = repoConfig._resolvedRef ?? repoConfig.ref ?? null;
|
|
343
|
+
// Applicability is judged on the original manifest ref: for `ref: latest`
|
|
344
|
+
// `_resolvedRef` is already the tag name, yet a configured ref_ttl still
|
|
345
|
+
// does not apply to the meta-ref.
|
|
346
|
+
const refTtl = applicableRefTtl(repoConfig.ref ?? null, repoConfig.ref_ttl);
|
|
347
|
+
return fetchRepo(repoConfig.repo, ref, token, noFetch, ssh, refTtl);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
module.exports = {
|
|
351
|
+
ENV_VAR,
|
|
352
|
+
STRICT_VAR,
|
|
353
|
+
LOCAL_ID_PREFIX,
|
|
354
|
+
normalizeId,
|
|
355
|
+
isLocal,
|
|
356
|
+
localDirOf,
|
|
357
|
+
synthesizeLocalId,
|
|
358
|
+
resolveLocalPath,
|
|
359
|
+
resolveLocalEntry,
|
|
360
|
+
parseLocalSourcesEnv,
|
|
361
|
+
applyLocalOverrides,
|
|
362
|
+
ensureRepoDir,
|
|
363
|
+
};
|