akm-cli 0.0.0 → 0.0.16
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/LICENSE +385 -0
- package/README.md +298 -6
- package/dist/asset-spec.js +70 -0
- package/dist/cli.js +912 -0
- package/dist/common.js +199 -0
- package/dist/config-cli.js +195 -0
- package/dist/config.js +324 -0
- package/dist/db.js +371 -0
- package/dist/embedder.js +150 -0
- package/dist/errors.js +28 -0
- package/dist/file-context.js +187 -0
- package/dist/frontmatter.js +86 -0
- package/dist/github.js +17 -0
- package/dist/indexer.js +341 -0
- package/dist/init.js +43 -0
- package/dist/llm.js +87 -0
- package/dist/lockfile.js +60 -0
- package/dist/markdown.js +77 -0
- package/dist/matchers.js +170 -0
- package/dist/metadata.js +408 -0
- package/dist/origin-resolve.js +54 -0
- package/dist/paths.js +92 -0
- package/dist/registry-install.js +459 -0
- package/dist/registry-resolve.js +486 -0
- package/dist/registry-search.js +255 -0
- package/dist/registry-types.js +1 -0
- package/dist/renderers.js +386 -0
- package/dist/ripgrep-install.js +155 -0
- package/dist/ripgrep-resolve.js +78 -0
- package/dist/ripgrep.js +2 -0
- package/dist/self-update.js +226 -0
- package/dist/stash-add.js +71 -0
- package/dist/stash-clone.js +115 -0
- package/dist/stash-ref.js +75 -0
- package/dist/stash-registry.js +206 -0
- package/dist/stash-resolve.js +92 -0
- package/dist/stash-search.js +494 -0
- package/dist/stash-show.js +59 -0
- package/dist/stash-source.js +131 -0
- package/dist/stash-types.js +1 -0
- package/dist/walker.js +163 -0
- package/dist/warn.js +20 -0
- package/package.json +53 -7
- package/index.js +0 -4
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { fetchWithRetry } from "./common";
|
|
7
|
+
import { asRecord, asString, GITHUB_API_BASE, githubHeaders } from "./github";
|
|
8
|
+
export function parseRegistryRef(rawRef) {
|
|
9
|
+
const ref = rawRef.trim();
|
|
10
|
+
if (!ref)
|
|
11
|
+
throw new Error("Registry ref is required.");
|
|
12
|
+
if (ref.startsWith("npm:")) {
|
|
13
|
+
return parseNpmRef(ref.slice(4), ref);
|
|
14
|
+
}
|
|
15
|
+
if (ref.startsWith("github:")) {
|
|
16
|
+
return parseGithubShorthand(ref.slice(7), ref);
|
|
17
|
+
}
|
|
18
|
+
if (ref.startsWith("git+")) {
|
|
19
|
+
return parseGitUrl(stripGitTransport(ref), ref);
|
|
20
|
+
}
|
|
21
|
+
if (ref.startsWith("file:")) {
|
|
22
|
+
return tryParseLocalRef(fileUriToPath(ref), true);
|
|
23
|
+
}
|
|
24
|
+
if (ref.startsWith("http://") || ref.startsWith("https://")) {
|
|
25
|
+
return parseRemoteUrl(ref);
|
|
26
|
+
}
|
|
27
|
+
const localRef = tryParseLocalRef(ref, isPathLikeRef(ref));
|
|
28
|
+
if (localRef) {
|
|
29
|
+
return localRef;
|
|
30
|
+
}
|
|
31
|
+
if (ref.startsWith("@") || !looksLikeGithubOwnerRepo(ref)) {
|
|
32
|
+
return parseNpmRef(ref, ref);
|
|
33
|
+
}
|
|
34
|
+
return parseGithubShorthand(ref, ref);
|
|
35
|
+
}
|
|
36
|
+
export async function resolveRegistryArtifact(parsed) {
|
|
37
|
+
if (parsed.source === "npm") {
|
|
38
|
+
return resolveNpmArtifact(parsed);
|
|
39
|
+
}
|
|
40
|
+
if (parsed.source === "local") {
|
|
41
|
+
return resolveLocalArtifact(parsed);
|
|
42
|
+
}
|
|
43
|
+
if (parsed.source === "git") {
|
|
44
|
+
return resolveGitArtifact(parsed);
|
|
45
|
+
}
|
|
46
|
+
return resolveGithubArtifact(parsed);
|
|
47
|
+
}
|
|
48
|
+
function parseNpmRef(input, originalRef) {
|
|
49
|
+
const trimmed = input.trim();
|
|
50
|
+
if (!trimmed)
|
|
51
|
+
throw new Error("Invalid npm ref.");
|
|
52
|
+
const parsed = splitNpmNameAndVersion(trimmed);
|
|
53
|
+
validateNpmPackageName(parsed.packageName);
|
|
54
|
+
return {
|
|
55
|
+
source: "npm",
|
|
56
|
+
ref: originalRef,
|
|
57
|
+
id: `npm:${parsed.packageName}`,
|
|
58
|
+
packageName: parsed.packageName,
|
|
59
|
+
requestedVersionOrTag: parsed.requestedVersionOrTag,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function parseGithubShorthand(input, originalRef) {
|
|
63
|
+
const [repoPart, requestedRef] = splitRefSuffix(input.trim());
|
|
64
|
+
const segments = repoPart.split("/").filter(Boolean);
|
|
65
|
+
if (segments.length !== 2) {
|
|
66
|
+
throw new Error("Invalid GitHub ref. Expected owner/repo or owner/repo#ref.");
|
|
67
|
+
}
|
|
68
|
+
const owner = segments[0];
|
|
69
|
+
const repo = segments[1].replace(/\.git$/i, "");
|
|
70
|
+
if (!owner || !repo) {
|
|
71
|
+
throw new Error("Invalid GitHub ref. Expected owner/repo.");
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
source: "github",
|
|
75
|
+
ref: originalRef,
|
|
76
|
+
id: `github:${owner}/${repo}`,
|
|
77
|
+
owner,
|
|
78
|
+
repo,
|
|
79
|
+
requestedRef,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function parseRemoteUrl(rawUrl) {
|
|
83
|
+
let url;
|
|
84
|
+
try {
|
|
85
|
+
url = new URL(rawUrl);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
throw new Error("Invalid registry URL.");
|
|
89
|
+
}
|
|
90
|
+
if (url.hostname === "github.com") {
|
|
91
|
+
return parseGithubUrl(url, rawUrl);
|
|
92
|
+
}
|
|
93
|
+
return parseGitUrl(rawUrl, rawUrl);
|
|
94
|
+
}
|
|
95
|
+
function parseGithubUrl(url, rawUrl) {
|
|
96
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
97
|
+
if (segments.length < 2) {
|
|
98
|
+
throw new Error("Invalid GitHub URL. Expected https://github.com/owner/repo.");
|
|
99
|
+
}
|
|
100
|
+
const owner = segments[0];
|
|
101
|
+
const repo = segments[1].replace(/\.git$/i, "");
|
|
102
|
+
const requestedRef = url.hash ? decodeURIComponent(url.hash.slice(1)) : undefined;
|
|
103
|
+
return {
|
|
104
|
+
source: "github",
|
|
105
|
+
ref: rawUrl,
|
|
106
|
+
id: `github:${owner}/${repo}`,
|
|
107
|
+
owner,
|
|
108
|
+
repo,
|
|
109
|
+
requestedRef,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function parseGitUrl(input, originalRef) {
|
|
113
|
+
const [urlPart, requestedRef] = splitRefSuffix(input.trim());
|
|
114
|
+
if (!urlPart)
|
|
115
|
+
throw new Error("Invalid git ref. A URL is required.");
|
|
116
|
+
// Normalize the URL for the id (strip .git suffix, fragment)
|
|
117
|
+
const normalized = urlPart.replace(/\.git$/i, "");
|
|
118
|
+
return {
|
|
119
|
+
source: "git",
|
|
120
|
+
ref: originalRef,
|
|
121
|
+
id: `git:${normalized}`,
|
|
122
|
+
url: urlPart,
|
|
123
|
+
requestedRef,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function tryParseLocalRef(rawRef, explicitPath) {
|
|
127
|
+
const resolvedPath = path.resolve(rawRef);
|
|
128
|
+
let stat;
|
|
129
|
+
try {
|
|
130
|
+
stat = fs.statSync(resolvedPath);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Explicit paths (./foo, ../bar, /abs) should throw on missing
|
|
134
|
+
if (explicitPath) {
|
|
135
|
+
throw new Error(`Local path not found: ${resolvedPath}`);
|
|
136
|
+
}
|
|
137
|
+
// Bare names that don't exist on disk — let caller fall through to npm/github
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
if (!stat.isDirectory()) {
|
|
141
|
+
if (explicitPath) {
|
|
142
|
+
throw new Error("Local add path must be a directory, but the provided path is not one.");
|
|
143
|
+
}
|
|
144
|
+
// Bare name exists but isn't a directory — not a local ref
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
const repoRoot = findGitRepoRoot(resolvedPath);
|
|
148
|
+
return {
|
|
149
|
+
source: "local",
|
|
150
|
+
ref: rawRef,
|
|
151
|
+
id: `local:${toReadableLocalId(resolvedPath)}`,
|
|
152
|
+
repoRoot,
|
|
153
|
+
sourcePath: resolvedPath,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function isPathLikeRef(ref) {
|
|
157
|
+
if (ref === "." || ref === "..")
|
|
158
|
+
return true;
|
|
159
|
+
if (path.isAbsolute(ref))
|
|
160
|
+
return true;
|
|
161
|
+
if (ref.startsWith("./") || ref.startsWith("../") || ref.startsWith(".\\") || ref.startsWith("..\\")) {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
return ref.includes("/") || ref.includes("\\");
|
|
165
|
+
}
|
|
166
|
+
async function resolveNpmArtifact(parsed) {
|
|
167
|
+
const encodedName = encodeURIComponent(parsed.packageName);
|
|
168
|
+
const metadata = await fetchJson(`https://registry.npmjs.org/${encodedName}`);
|
|
169
|
+
const versions = asRecord(metadata.versions);
|
|
170
|
+
const distTags = asRecord(metadata["dist-tags"]);
|
|
171
|
+
const requested = parsed.requestedVersionOrTag;
|
|
172
|
+
let resolvedVersion;
|
|
173
|
+
if (!requested) {
|
|
174
|
+
resolvedVersion = asString(distTags.latest);
|
|
175
|
+
}
|
|
176
|
+
else if (requested in versions) {
|
|
177
|
+
resolvedVersion = requested;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
// Try dist-tag first
|
|
181
|
+
resolvedVersion = asString(distTags[requested]);
|
|
182
|
+
// If not a dist-tag, try semver range resolution
|
|
183
|
+
if (!resolvedVersion && isSemverRange(requested)) {
|
|
184
|
+
const versionKeys = Object.keys(versions).filter(isExactSemver);
|
|
185
|
+
resolvedVersion = maxSatisfying(versionKeys, requested);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (!resolvedVersion || !(resolvedVersion in versions)) {
|
|
189
|
+
throw new Error(`Unable to resolve npm ref "${parsed.ref}".`);
|
|
190
|
+
}
|
|
191
|
+
const versionMeta = asRecord(versions[resolvedVersion]);
|
|
192
|
+
const dist = asRecord(versionMeta.dist);
|
|
193
|
+
const tarballUrl = asString(dist.tarball);
|
|
194
|
+
if (!tarballUrl) {
|
|
195
|
+
throw new Error(`npm package ${parsed.packageName}@${resolvedVersion} does not expose a tarball URL.`);
|
|
196
|
+
}
|
|
197
|
+
const resolvedRevision = asString(dist.shasum) ?? asString(dist.integrity);
|
|
198
|
+
return {
|
|
199
|
+
id: parsed.id,
|
|
200
|
+
source: parsed.source,
|
|
201
|
+
ref: parsed.ref,
|
|
202
|
+
artifactUrl: tarballUrl,
|
|
203
|
+
resolvedVersion,
|
|
204
|
+
resolvedRevision,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
async function resolveGithubArtifact(parsed) {
|
|
208
|
+
const headers = githubHeaders();
|
|
209
|
+
if (parsed.requestedRef) {
|
|
210
|
+
const commit = await tryFetchJson(`${GITHUB_API_BASE}/repos/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}/commits/${encodeURIComponent(parsed.requestedRef)}`, headers);
|
|
211
|
+
const resolvedRevision = asString(commit?.sha) ?? parsed.requestedRef;
|
|
212
|
+
return {
|
|
213
|
+
id: parsed.id,
|
|
214
|
+
source: parsed.source,
|
|
215
|
+
ref: parsed.ref,
|
|
216
|
+
artifactUrl: `${GITHUB_API_BASE}/repos/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}/tarball/${encodeURIComponent(parsed.requestedRef)}`,
|
|
217
|
+
resolvedRevision,
|
|
218
|
+
resolvedVersion: parsed.requestedRef,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const latestRelease = await tryFetchJson(`${GITHUB_API_BASE}/repos/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}/releases/latest`, headers);
|
|
222
|
+
if (latestRelease) {
|
|
223
|
+
const tarballUrl = asString(latestRelease.tarball_url);
|
|
224
|
+
if (tarballUrl) {
|
|
225
|
+
return {
|
|
226
|
+
id: parsed.id,
|
|
227
|
+
source: parsed.source,
|
|
228
|
+
ref: parsed.ref,
|
|
229
|
+
artifactUrl: tarballUrl,
|
|
230
|
+
resolvedVersion: asString(latestRelease.tag_name),
|
|
231
|
+
resolvedRevision: asString(latestRelease.target_commitish),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const repoMeta = await fetchJson(`${GITHUB_API_BASE}/repos/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}`, headers);
|
|
236
|
+
const defaultBranch = asString(repoMeta.default_branch);
|
|
237
|
+
if (!defaultBranch) {
|
|
238
|
+
throw new Error(`Unable to resolve default branch for ${parsed.owner}/${parsed.repo}.`);
|
|
239
|
+
}
|
|
240
|
+
const commit = await tryFetchJson(`${GITHUB_API_BASE}/repos/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}/commits/${encodeURIComponent(defaultBranch)}`, headers);
|
|
241
|
+
return {
|
|
242
|
+
id: parsed.id,
|
|
243
|
+
source: parsed.source,
|
|
244
|
+
ref: parsed.ref,
|
|
245
|
+
artifactUrl: `${GITHUB_API_BASE}/repos/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}/tarball/${encodeURIComponent(defaultBranch)}`,
|
|
246
|
+
resolvedVersion: defaultBranch,
|
|
247
|
+
resolvedRevision: asString(commit?.sha) ?? defaultBranch,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
async function resolveGitArtifact(parsed) {
|
|
251
|
+
const ref = parsed.requestedRef ?? "HEAD";
|
|
252
|
+
const result = spawnSync("git", ["ls-remote", parsed.url, ref], { encoding: "utf8", timeout: 30_000 });
|
|
253
|
+
let resolvedRevision;
|
|
254
|
+
if (result.status === 0) {
|
|
255
|
+
const firstLine = result.stdout.trim().split(/\r?\n/)[0];
|
|
256
|
+
resolvedRevision = firstLine?.split(/\s/)[0] || undefined;
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
id: parsed.id,
|
|
260
|
+
source: parsed.source,
|
|
261
|
+
ref: parsed.ref,
|
|
262
|
+
artifactUrl: parsed.url,
|
|
263
|
+
resolvedVersion: parsed.requestedRef,
|
|
264
|
+
resolvedRevision,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
async function resolveLocalArtifact(parsed) {
|
|
268
|
+
return {
|
|
269
|
+
id: parsed.id,
|
|
270
|
+
source: parsed.source,
|
|
271
|
+
ref: parsed.ref,
|
|
272
|
+
artifactUrl: pathToFileURL(parsed.sourcePath).toString(),
|
|
273
|
+
resolvedRevision: parsed.repoRoot ? readGitValue(parsed.repoRoot, "rev-parse", "HEAD") : undefined,
|
|
274
|
+
resolvedVersion: parsed.repoRoot ? readGitValue(parsed.repoRoot, "rev-parse", "--abbrev-ref", "HEAD") : undefined,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function splitNpmNameAndVersion(input) {
|
|
278
|
+
if (input.startsWith("@")) {
|
|
279
|
+
const secondAt = input.indexOf("@", 1);
|
|
280
|
+
if (secondAt > 0) {
|
|
281
|
+
return {
|
|
282
|
+
packageName: input.slice(0, secondAt),
|
|
283
|
+
requestedVersionOrTag: input.slice(secondAt + 1) || undefined,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
return { packageName: input };
|
|
287
|
+
}
|
|
288
|
+
const at = input.lastIndexOf("@");
|
|
289
|
+
if (at > 0) {
|
|
290
|
+
return {
|
|
291
|
+
packageName: input.slice(0, at),
|
|
292
|
+
requestedVersionOrTag: input.slice(at + 1) || undefined,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
return { packageName: input };
|
|
296
|
+
}
|
|
297
|
+
function validateNpmPackageName(name) {
|
|
298
|
+
if (!name)
|
|
299
|
+
throw new Error("Invalid npm package name: name is required.");
|
|
300
|
+
if (name.length > 214)
|
|
301
|
+
throw new Error(`Invalid npm package name: "${name}" exceeds 214 characters.`);
|
|
302
|
+
if (name !== name.toLowerCase() && !name.startsWith("@")) {
|
|
303
|
+
throw new Error(`Invalid npm package name: "${name}" must be lowercase.`);
|
|
304
|
+
}
|
|
305
|
+
if (name.startsWith(".") || name.startsWith("_")) {
|
|
306
|
+
throw new Error(`Invalid npm package name: "${name}" cannot start with . or _.`);
|
|
307
|
+
}
|
|
308
|
+
if (/[~'!()*]/.test(name) ||
|
|
309
|
+
name.includes(" ") ||
|
|
310
|
+
encodeURIComponent(name)
|
|
311
|
+
.replace(/%40/g, "@")
|
|
312
|
+
.replace(/%2[Ff]/g, "/") !== name) {
|
|
313
|
+
throw new Error(`Invalid npm package name: "${name}" contains invalid characters.`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function looksLikeGithubOwnerRepo(ref) {
|
|
317
|
+
const [repoPart] = splitRefSuffix(ref);
|
|
318
|
+
const parts = repoPart.split("/").filter(Boolean);
|
|
319
|
+
return parts.length === 2;
|
|
320
|
+
}
|
|
321
|
+
function splitRefSuffix(value) {
|
|
322
|
+
const hash = value.indexOf("#");
|
|
323
|
+
if (hash < 0)
|
|
324
|
+
return [value, undefined];
|
|
325
|
+
return [value.slice(0, hash), value.slice(hash + 1) || undefined];
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Strip the `git+` transport prefix from a ref, returning the inner URL.
|
|
329
|
+
* Handles `git+https://...`, `git+ssh://...`, `git+http://...`, etc.
|
|
330
|
+
*/
|
|
331
|
+
function stripGitTransport(ref) {
|
|
332
|
+
return ref.slice(4); // strip "git+"
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Convert a `file:` URI to a local filesystem path.
|
|
336
|
+
* Supports `file:./relative`, `file:../relative`, and `file:///absolute`.
|
|
337
|
+
*/
|
|
338
|
+
function fileUriToPath(ref) {
|
|
339
|
+
const after = ref.slice(5); // strip "file:"
|
|
340
|
+
// file:///absolute/path or file:///C:/path
|
|
341
|
+
if (after.startsWith("///")) {
|
|
342
|
+
return after.slice(2); // keep one leading /
|
|
343
|
+
}
|
|
344
|
+
// file://hostname/path (rare, treat hostname/path as absolute)
|
|
345
|
+
if (after.startsWith("//")) {
|
|
346
|
+
return after.slice(1);
|
|
347
|
+
}
|
|
348
|
+
// file:./relative or file:../relative or file:/absolute
|
|
349
|
+
return after;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Build a human-readable local ID from an absolute path.
|
|
353
|
+
* /home/user/.hyphn/skills → ~/.hyphn/skills
|
|
354
|
+
* /tmp/my-kit → /tmp/my-kit
|
|
355
|
+
*/
|
|
356
|
+
function toReadableLocalId(absolutePath) {
|
|
357
|
+
const home = os.homedir();
|
|
358
|
+
if (absolutePath === home)
|
|
359
|
+
return "~";
|
|
360
|
+
if (absolutePath.startsWith(home + path.sep)) {
|
|
361
|
+
return `~/${absolutePath.slice(home.length + 1)}`;
|
|
362
|
+
}
|
|
363
|
+
return absolutePath;
|
|
364
|
+
}
|
|
365
|
+
function findGitRepoRoot(startDir) {
|
|
366
|
+
let current = path.resolve(startDir);
|
|
367
|
+
while (true) {
|
|
368
|
+
if (fs.existsSync(path.join(current, ".git"))) {
|
|
369
|
+
return current;
|
|
370
|
+
}
|
|
371
|
+
const parent = path.dirname(current);
|
|
372
|
+
if (parent === current)
|
|
373
|
+
return undefined;
|
|
374
|
+
current = parent;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function readGitValue(repoRoot, ...args) {
|
|
378
|
+
const result = spawnSync("git", ["-C", repoRoot, ...args], { encoding: "utf8" });
|
|
379
|
+
if (result.status !== 0)
|
|
380
|
+
return undefined;
|
|
381
|
+
const value = result.stdout.trim();
|
|
382
|
+
return value || undefined;
|
|
383
|
+
}
|
|
384
|
+
function parseSemver(version) {
|
|
385
|
+
const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/);
|
|
386
|
+
if (!match)
|
|
387
|
+
return undefined;
|
|
388
|
+
return {
|
|
389
|
+
major: parseInt(match[1], 10),
|
|
390
|
+
minor: parseInt(match[2], 10),
|
|
391
|
+
patch: parseInt(match[3], 10),
|
|
392
|
+
prerelease: match[4],
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function isExactSemver(version) {
|
|
396
|
+
return /^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.+-]+)?$/.test(version);
|
|
397
|
+
}
|
|
398
|
+
function isSemverRange(input) {
|
|
399
|
+
return /^[~^>=<*]/.test(input) || /^\d+\.(\d+|\*)/.test(input);
|
|
400
|
+
}
|
|
401
|
+
function compareSemver(a, b) {
|
|
402
|
+
if (a.major !== b.major)
|
|
403
|
+
return a.major - b.major;
|
|
404
|
+
if (a.minor !== b.minor)
|
|
405
|
+
return a.minor - b.minor;
|
|
406
|
+
if (a.patch !== b.patch)
|
|
407
|
+
return a.patch - b.patch;
|
|
408
|
+
// Versions with prerelease are lower than release
|
|
409
|
+
if (a.prerelease && !b.prerelease)
|
|
410
|
+
return -1;
|
|
411
|
+
if (!a.prerelease && b.prerelease)
|
|
412
|
+
return 1;
|
|
413
|
+
return 0;
|
|
414
|
+
}
|
|
415
|
+
function semverGte(a, b) {
|
|
416
|
+
return compareSemver(a, b) >= 0;
|
|
417
|
+
}
|
|
418
|
+
function satisfiesRange(version, range) {
|
|
419
|
+
// Skip pre-release versions unless range specifically mentions one
|
|
420
|
+
if (version.prerelease && !range.includes("-"))
|
|
421
|
+
return false;
|
|
422
|
+
// ^1.2.3 — compatible with version: same major, >= minor.patch
|
|
423
|
+
const caretMatch = range.match(/^\^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/);
|
|
424
|
+
if (caretMatch) {
|
|
425
|
+
const rMajor = parseInt(caretMatch[1], 10);
|
|
426
|
+
const rMinor = parseInt(caretMatch[2], 10);
|
|
427
|
+
const rPatch = parseInt(caretMatch[3], 10);
|
|
428
|
+
if (version.major !== rMajor)
|
|
429
|
+
return false;
|
|
430
|
+
// ^0.x has special behavior: ^0.2.3 means >=0.2.3 <0.3.0
|
|
431
|
+
if (rMajor === 0) {
|
|
432
|
+
if (version.minor !== rMinor)
|
|
433
|
+
return false;
|
|
434
|
+
return version.patch >= rPatch;
|
|
435
|
+
}
|
|
436
|
+
return semverGte(version, { major: rMajor, minor: rMinor, patch: rPatch });
|
|
437
|
+
}
|
|
438
|
+
// ~1.2.3 — same major.minor, patch >= specified
|
|
439
|
+
const tildeMatch = range.match(/^~(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/);
|
|
440
|
+
if (tildeMatch) {
|
|
441
|
+
const rMajor = parseInt(tildeMatch[1], 10);
|
|
442
|
+
const rMinor = parseInt(tildeMatch[2], 10);
|
|
443
|
+
const rPatch = parseInt(tildeMatch[3], 10);
|
|
444
|
+
return version.major === rMajor && version.minor === rMinor && version.patch >= rPatch;
|
|
445
|
+
}
|
|
446
|
+
// >=1.2.3
|
|
447
|
+
const gteMatch = range.match(/^>=(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/);
|
|
448
|
+
if (gteMatch) {
|
|
449
|
+
const rMajor = parseInt(gteMatch[1], 10);
|
|
450
|
+
const rMinor = parseInt(gteMatch[2], 10);
|
|
451
|
+
const rPatch = parseInt(gteMatch[3], 10);
|
|
452
|
+
return semverGte(version, { major: rMajor, minor: rMinor, patch: rPatch });
|
|
453
|
+
}
|
|
454
|
+
// * or latest
|
|
455
|
+
if (range === "*" || range === "latest")
|
|
456
|
+
return true;
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
export function maxSatisfying(versions, range) {
|
|
460
|
+
const candidates = [];
|
|
461
|
+
for (const v of versions) {
|
|
462
|
+
const parsed = parseSemver(v);
|
|
463
|
+
if (!parsed)
|
|
464
|
+
continue;
|
|
465
|
+
if (satisfiesRange(parsed, range)) {
|
|
466
|
+
candidates.push({ version: v, parsed });
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (candidates.length === 0)
|
|
470
|
+
return undefined;
|
|
471
|
+
candidates.sort((a, b) => compareSemver(b.parsed, a.parsed));
|
|
472
|
+
return candidates[0].version;
|
|
473
|
+
}
|
|
474
|
+
async function fetchJson(url, headers) {
|
|
475
|
+
const response = await fetchWithRetry(url, { headers });
|
|
476
|
+
if (!response.ok) {
|
|
477
|
+
throw new Error(`Request failed (${response.status}) for ${url}`);
|
|
478
|
+
}
|
|
479
|
+
return (await response.json());
|
|
480
|
+
}
|
|
481
|
+
async function tryFetchJson(url, headers) {
|
|
482
|
+
const response = await fetchWithRetry(url, { headers });
|
|
483
|
+
if (!response.ok)
|
|
484
|
+
return null;
|
|
485
|
+
return (await response.json());
|
|
486
|
+
}
|