dslinter 0.0.11 → 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/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.0.16
4
+
5
+ [compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.0.15...v0.0.16)
6
+
7
+ ## v0.0.15
8
+
9
+ [compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.0.12...v0.0.15)
10
+
11
+ ### 🚀 Enhancements
12
+
13
+ - Update release workflow and versioning ([5e28f64](https://github.com/jrmybtlr/DSLinter/commit/5e28f64))
14
+
15
+ ### ❤️ Contributors
16
+
17
+ - Jeremy Butler <jeremy.butler@laravel.com>
18
+
19
+ ## v0.0.12
20
+
21
+ [compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.0.11...v0.0.12)
22
+
23
+ ### 🚀 Enhancements
24
+
25
+ - Enhance GitHub release asset handling and logging ([4a62b9f](https://github.com/jrmybtlr/DSLinter/commit/4a62b9f))
26
+
27
+ ### ❤️ Contributors
28
+
29
+ - Jeremy Butler <jeremy.butler@laravel.com>
30
+
3
31
  ## v0.0.11
4
32
 
5
33
  [compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.0.10...v0.0.11)
package/README.md CHANGED
@@ -33,7 +33,9 @@ Environment variables:
33
33
  |----------|---------|
34
34
  | `DSLINT_SKIP_DOWNLOAD=1` | Skip postinstall download (air-gapped / you only use `PATH`). |
35
35
  | `DSLINT_RELEASE_TAG` | Override release tag (default `v` + `dslinter` version from `package.json`). |
36
- | `DSLINT_GITHUB_REPO` | Override `owner/repo` for downloads (default `jrmybtlr/DSLinter`). |
36
+ | `DSLINT_GITHUB_REPO` | Override `owner/repo` for downloads (default from `package.json` → `jrmybtlr/DSLinter`). |
37
+ | `DSLINT_VERBOSE=1` | Log which GitHub releases/assets were tried when downloading. |
38
+ | `GITHUB_TOKEN` / `GH_TOKEN` | **Required for private repos** (`jrmybtlr/DSLinter`). Token needs read access to releases. |
37
39
 
38
40
  ### How this differs from `oxlint`
39
41
 
package/bin/dslinter.mjs CHANGED
@@ -43,7 +43,7 @@ async function resolveCommand() {
43
43
 
44
44
  const vendored = vendorBinaryPath(packageRoot);
45
45
  if (!existsSync(vendored)) {
46
- await ensureDslintBinary(packageRoot, { quiet: true });
46
+ await ensureDslintBinary(packageRoot, { quiet: false });
47
47
  }
48
48
  if (existsSync(vendored)) {
49
49
  return vendored;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dslinter",
3
- "version": "0.0.11",
3
+ "version": "0.0.16",
4
4
  "description": "DSLinter dashboard UI: playground shell, token wall, and governance panels (consumes dslint-report.json).",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,14 +1,19 @@
1
1
  /**
2
2
  * Best-effort download of the prebuilt `dslinter` CLI for this platform.
3
- * Used by postinstall and on first `dslinter` / `npx dslinter` invocation.
4
3
  */
5
4
  import { readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
6
5
  import { chmod, mkdir, stat } from "node:fs/promises";
7
6
  import { dirname, join } from "node:path";
8
7
  import { fileURLToPath } from "node:url";
8
+ import {
9
+ fetchReleasesForVersion,
10
+ githubAuthToken,
11
+ pickReleaseAsset,
12
+ tryDirectAssetDownload,
13
+ } from "./github-release.mjs";
9
14
  import {
10
15
  githubRepoFromPackage,
11
- releaseAssetBaseName,
16
+ releaseAssetCandidateNames,
12
17
  vendorBinaryPath,
13
18
  } from "./resolve-dslint-binary.mjs";
14
19
 
@@ -31,26 +36,33 @@ function readPackageVersion(packageRoot) {
31
36
  return pkg.version;
32
37
  }
33
38
 
34
- function releaseTag(version) {
35
- const o = process.env.DSLINT_RELEASE_TAG?.trim();
36
- if (o) return o.startsWith("v") ? o : `v${o}`;
37
- return `v${version}`;
38
- }
39
-
40
39
  function releaseRepo(packageRoot) {
41
40
  const override = process.env.DSLINT_GITHUB_REPO?.trim();
42
41
  if (override) return override;
43
42
  return githubRepoFromPackage(packageRoot);
44
43
  }
45
44
 
46
- function assetUrl(packageRoot, tag, asset) {
47
- return `https://github.com/${releaseRepo(packageRoot)}/releases/download/${tag}/${asset}`;
45
+ /**
46
+ * @param {string} dest
47
+ * @param {{ buf: Buffer }} payload
48
+ */
49
+ async function writeVendorBinary(dest, { buf }) {
50
+ const tmp = `${dest}.part`;
51
+ writeFileSync(tmp, buf);
52
+ try {
53
+ if (await pathExists(dest)) unlinkSync(dest);
54
+ } catch {
55
+ /* ignore */
56
+ }
57
+ renameSync(tmp, dest);
58
+ if (process.platform !== "win32") {
59
+ await chmod(dest, 0o755);
60
+ }
48
61
  }
49
62
 
50
63
  /**
51
64
  * @param {string} packageRoot
52
65
  * @param {{ quiet?: boolean }} [opts]
53
- * @returns {Promise<boolean>} true if vendored binary exists after this call
54
66
  */
55
67
  export async function ensureDslintBinary(packageRoot = defaultPackageRoot, opts = {}) {
56
68
  const { quiet = false } = opts;
@@ -63,8 +75,8 @@ export async function ensureDslintBinary(packageRoot = defaultPackageRoot, opts
63
75
  const dest = vendorBinaryPath(packageRoot);
64
76
  if (await pathExists(dest)) return true;
65
77
 
66
- const asset = releaseAssetBaseName();
67
- if (!asset) {
78
+ const candidates = releaseAssetCandidateNames();
79
+ if (candidates.length === 0) {
68
80
  log(
69
81
  `[dslinter] No prebuilt scanner for ${process.platform}-${process.arch}.`,
70
82
  );
@@ -72,57 +84,95 @@ export async function ensureDslintBinary(packageRoot = defaultPackageRoot, opts
72
84
  }
73
85
 
74
86
  const version = readPackageVersion(packageRoot);
75
- const tagsToTry = [releaseTag(version)];
76
- if (process.env.DSLINT_USE_LATEST_RELEASE !== "0") {
77
- tagsToTry.push("latest");
87
+ const repo = releaseRepo(packageRoot);
88
+ await mkdir(join(packageRoot, "vendor"), { recursive: true });
89
+
90
+ const direct = await tryDirectAssetDownload(repo, version, candidates, log);
91
+ if (direct) {
92
+ await writeVendorBinary(dest, direct);
93
+ if (direct.tag !== `v${version}` && !quiet) {
94
+ log(
95
+ `[dslinter] Installed ${direct.name} from release ${direct.tag} (npm v${version}).`,
96
+ );
97
+ }
98
+ return true;
78
99
  }
79
100
 
80
- const vendorDir = join(packageRoot, "vendor");
81
- await mkdir(vendorDir, { recursive: true });
82
- const tmp = `${dest}.part`;
101
+ let apiDenied = false;
102
+ let tagExistsWithoutBinaries = null;
103
+ try {
104
+ const {
105
+ releases,
106
+ apiDenied: denied,
107
+ apiError,
108
+ tagExistsWithoutBinaries: emptyTag,
109
+ } = await fetchReleasesForVersion(repo, version);
110
+ apiDenied = denied;
111
+ tagExistsWithoutBinaries = emptyTag ?? null;
112
+
113
+ if (apiError) {
114
+ log(
115
+ `[dslinter] GitHub API error: ${apiError.message}`,
116
+ );
117
+ }
83
118
 
84
- for (const tag of tagsToTry) {
85
- const url = assetUrl(packageRoot, tag, asset);
86
- try {
87
- const res = await fetch(url, { redirect: "follow" });
88
- if (res.status === 404) continue;
89
- if (!res.ok) {
90
- log(`[dslinter] Download failed (${res.status}): ${url}`);
91
- continue;
92
- }
119
+ for (const release of releases) {
120
+ const asset = pickReleaseAsset(release, candidates);
121
+ if (!asset) continue;
122
+
123
+ const headers = {};
124
+ const token = githubAuthToken();
125
+ if (token) headers.Authorization = `Bearer ${token}`;
126
+
127
+ const res = await fetch(asset.browser_download_url, {
128
+ headers,
129
+ redirect: "follow",
130
+ });
131
+ if (!res.ok) continue;
93
132
 
94
133
  const buf = Buffer.from(await res.arrayBuffer());
95
- writeFileSync(tmp, buf);
96
- try {
97
- if (await pathExists(dest)) unlinkSync(dest);
98
- } catch {
99
- /* ignore */
100
- }
101
- renameSync(tmp, dest);
102
- if (process.platform !== "win32") {
103
- await chmod(dest, 0o755);
104
- }
105
- if (tag === "latest" && !quiet) {
134
+ await writeVendorBinary(dest, { buf });
135
+ if (release.tag_name !== `v${version}` && !quiet) {
106
136
  log(
107
- `[dslinter] Installed scanner from latest GitHub release (no asset for npm v${version}).`,
137
+ `[dslinter] Installed ${asset.name} from release ${release.tag_name} (npm v${version}).`,
108
138
  );
109
139
  }
110
140
  return true;
111
- } catch (err) {
112
- log(
113
- `[dslinter] Could not download: ${err instanceof Error ? err.message : err}`,
114
- );
115
- try {
116
- unlinkSync(tmp);
117
- } catch {
118
- /* ignore */
119
- }
120
141
  }
142
+ } catch (err) {
143
+ log(
144
+ `[dslinter] GitHub API: ${err instanceof Error ? err.message : err}`,
145
+ );
146
+ }
147
+
148
+ const tag = `v${version}`;
149
+ const releasePage = `https://github.com/${repo}/releases/tag/${tag}`;
150
+
151
+ if (tagExistsWithoutBinaries) {
152
+ log(
153
+ `[dslinter] GitHub release ${tagExistsWithoutBinaries} exists but has no scanner binaries attached.\n` +
154
+ ` Run the "Release dslinter binaries" workflow on ${repo} (Actions → workflow_dispatch, tag ${tagExistsWithoutBinaries}),\n` +
155
+ ` or push a new tag so CI uploads assets like ${candidates[0]}.\n` +
156
+ ` ${releasePage}`,
157
+ );
158
+ return false;
159
+ }
160
+
161
+ if (apiDenied || !githubAuthToken()) {
162
+ log(
163
+ `[dslinter] Cannot reach ${repo} releases without authentication (GitHub returned 404).\n` +
164
+ ` If you can open ${releasePage} in a browser, the repo is likely private.\n` +
165
+ ` Create a fine-grained or classic token with repo read access, then:\n` +
166
+ ` export GITHUB_TOKEN=ghp_...\n` +
167
+ ` npx dslinter\n` +
168
+ ` Or install from source: cargo install --git https://github.com/${repo} dslinter --locked`,
169
+ );
170
+ return false;
121
171
  }
122
172
 
123
173
  log(
124
- `[dslinter] No GitHub release with asset "${asset}" (tried ${tagsToTry.join(", ")}).\n` +
125
- ` Publish tag ${releaseTag(version)} with workflow release-dslint-binaries.yml, or set DSLINT_BIN to a local build.`,
174
+ `[dslinter] No release with asset ${candidates.join(" or ")} on ${repo} for ${tag}.\n` +
175
+ ` Create ${releasePage} and run release-dslint-binaries.yml, or set DSLINT_BIN.`,
126
176
  );
127
177
  return false;
128
178
  }
@@ -132,6 +182,6 @@ const isMain =
132
182
  fileURLToPath(import.meta.url) === process.argv[1];
133
183
 
134
184
  if (isMain) {
135
- const ok = await ensureDslintBinary();
136
- process.exit(ok ? 0 : 0);
185
+ await ensureDslintBinary();
186
+ process.exit(0);
137
187
  }
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Resolve and download GitHub release assets (API + direct URLs; supports private repos with a token).
3
+ */
4
+
5
+ const API = "https://api.github.com";
6
+
7
+ export function githubAuthToken() {
8
+ return (
9
+ process.env.GITHUB_TOKEN?.trim() || process.env.GH_TOKEN?.trim() || null
10
+ );
11
+ }
12
+
13
+ export function downloadAuthHeaders() {
14
+ const token = githubAuthToken();
15
+ return token ? { Authorization: `Bearer ${token}` } : {};
16
+ }
17
+
18
+ function apiHeaders() {
19
+ return {
20
+ Accept: "application/vnd.github+json",
21
+ "User-Agent": "dslinter-npm",
22
+ "X-GitHub-Api-Version": "2022-11-28",
23
+ ...downloadAuthHeaders(),
24
+ };
25
+ }
26
+
27
+ /**
28
+ * @param {string} repo `owner/name`
29
+ * @param {string} tag `v0.0.12` or `latest`
30
+ * @param {string} assetName
31
+ */
32
+ export function directAssetUrl(repo, tag, assetName) {
33
+ const file = encodeURIComponent(assetName);
34
+ if (tag === "latest") {
35
+ return `https://github.com/${repo}/releases/latest/download/${file}`;
36
+ }
37
+ return `https://github.com/${repo}/releases/download/${encodeURIComponent(tag)}/${file}`;
38
+ }
39
+
40
+ /**
41
+ * @param {string} npmVersion
42
+ */
43
+ export function releaseTagsToTry(npmVersion) {
44
+ const override = process.env.DSLINT_RELEASE_TAG?.trim();
45
+ if (override) {
46
+ return [override.startsWith("v") ? override : `v${override}`];
47
+ }
48
+ return [`v${npmVersion}`, npmVersion, "latest"];
49
+ }
50
+
51
+ /**
52
+ * @param {string} repo
53
+ * @param {string} path
54
+ * @returns {Promise<{ data: unknown } | { notFound: true } | { error: Error }>}
55
+ */
56
+ async function githubGet(repo, path) {
57
+ const res = await fetch(`${API}/repos/${repo}${path}`, {
58
+ headers: apiHeaders(),
59
+ redirect: "follow",
60
+ });
61
+ if (res.status === 404) return { notFound: true };
62
+ if (!res.ok) {
63
+ const body = await res.text().catch(() => "");
64
+ return {
65
+ error: new Error(`GitHub API ${res.status} for ${path}: ${body.slice(0, 200)}`),
66
+ };
67
+ }
68
+ return { data: await res.json() };
69
+ }
70
+
71
+ /**
72
+ * @param {string} repo
73
+ * @param {string} tag e.g. `v0.0.12` or `latest`
74
+ */
75
+ export async function fetchRelease(repo, tag) {
76
+ const path =
77
+ tag === "latest" ? "/releases/latest" : `/releases/tags/${encodeURIComponent(tag)}`;
78
+ let result = await githubGet(repo, path);
79
+ if (result.notFound && tag.startsWith("v")) {
80
+ result = await githubGet(
81
+ repo,
82
+ `/releases/tags/${encodeURIComponent(tag.slice(1))}`,
83
+ );
84
+ }
85
+ if ("data" in result) return result.data;
86
+ return null;
87
+ }
88
+
89
+ /**
90
+ * @param {string} repo
91
+ * @param {string} npmVersion
92
+ */
93
+ /** @param {unknown} release */
94
+ function hasScannerAssets(release) {
95
+ const names = release?.assets?.map((a) => a.name) ?? [];
96
+ return names.some((n) => n.startsWith("dslinter-") || n.startsWith("dslint-"));
97
+ }
98
+
99
+ export async function fetchReleasesForVersion(repo, npmVersion) {
100
+ const out = [];
101
+ const seen = new Set();
102
+ /** @type {string | null} */
103
+ let tagExistsWithoutBinaries = null;
104
+
105
+ const consider = (release) => {
106
+ if (!release?.tag_name) return;
107
+ const tag = String(release.tag_name);
108
+ const matches =
109
+ tag === `v${npmVersion}` || tag === npmVersion;
110
+ if (matches && !hasScannerAssets(release)) {
111
+ tagExistsWithoutBinaries = tag;
112
+ }
113
+ };
114
+
115
+ const push = (release) => {
116
+ if (!hasScannerAssets(release)) return;
117
+ const key = release.id ?? release.tag_name;
118
+ if (seen.has(key)) return;
119
+ seen.add(key);
120
+ out.push(release);
121
+ };
122
+
123
+ const vRelease = await fetchRelease(repo, `v${npmVersion}`);
124
+ consider(vRelease);
125
+ push(vRelease);
126
+
127
+ const bareRelease = await fetchRelease(repo, npmVersion);
128
+ consider(bareRelease);
129
+ push(bareRelease);
130
+
131
+ const latestRelease = await fetchRelease(repo, "latest");
132
+ push(latestRelease);
133
+
134
+ if (out.length > 0) {
135
+ return { releases: out, apiDenied: false, tagExistsWithoutBinaries };
136
+ }
137
+
138
+ const listResult = await githubGet(repo, "/releases?per_page=30");
139
+ if ("error" in listResult) {
140
+ return { releases: out, apiDenied: false, apiError: listResult.error };
141
+ }
142
+ if (listResult.notFound) {
143
+ return { releases: out, apiDenied: !githubAuthToken() };
144
+ }
145
+
146
+ const list = listResult.data;
147
+ if (!Array.isArray(list)) return { releases: out, apiDenied: false };
148
+
149
+ for (const release of list) {
150
+ const tag = String(release.tag_name ?? "");
151
+ if (tag === `v${npmVersion}` || tag === npmVersion) {
152
+ push(release);
153
+ }
154
+ }
155
+ if (out.length > 0) {
156
+ return { releases: out, apiDenied: false, tagExistsWithoutBinaries };
157
+ }
158
+
159
+ for (const release of list) {
160
+ const names = release.assets?.map((a) => a.name) ?? [];
161
+ if (names.some((n) => n.startsWith("dslinter-") || n.startsWith("dslint-"))) {
162
+ push(release);
163
+ break;
164
+ }
165
+ }
166
+
167
+ return { releases: out, apiDenied: false, tagExistsWithoutBinaries };
168
+ }
169
+
170
+ /**
171
+ * @param {string[]} candidateNames
172
+ * @param {{ assets: { name: string; browser_download_url: string }[] }} release
173
+ */
174
+ export function pickReleaseAsset(release, candidateNames) {
175
+ for (const name of candidateNames) {
176
+ const asset = release.assets.find((a) => a.name === name);
177
+ if (asset?.browser_download_url) return asset;
178
+ }
179
+ return null;
180
+ }
181
+
182
+ /**
183
+ * Download via public/private release URLs (no API metadata required).
184
+ * @param {(msg: string) => void} log
185
+ */
186
+ export async function tryDirectAssetDownload(repo, npmVersion, candidateNames, log) {
187
+ const verbose = process.env.DSLINT_VERBOSE === "1";
188
+ const headers = downloadAuthHeaders();
189
+ const tags = releaseTagsToTry(npmVersion);
190
+
191
+ for (const tag of tags) {
192
+ for (const name of candidateNames) {
193
+ const url = directAssetUrl(repo, tag, name);
194
+ if (verbose) log(`[dslinter] GET ${url}`);
195
+ try {
196
+ const res = await fetch(url, { headers, redirect: "follow" });
197
+ if (res.status === 404) continue;
198
+ if (!res.ok) {
199
+ if (verbose) log(`[dslinter] ${res.status} ${url}`);
200
+ continue;
201
+ }
202
+ const contentType = res.headers.get("content-type") ?? "";
203
+ if (contentType.includes("text/html")) continue;
204
+
205
+ const buf = Buffer.from(await res.arrayBuffer());
206
+ if (buf.length < 512) continue;
207
+
208
+ return { buf, tag, name, url };
209
+ } catch (err) {
210
+ if (verbose) {
211
+ log(
212
+ `[dslinter] ${url}: ${err instanceof Error ? err.message : err}`,
213
+ );
214
+ }
215
+ }
216
+ }
217
+ }
218
+ return null;
219
+ }
@@ -1,25 +1,32 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { githubRepoFromPackage } from "./resolve-dslint-binary.mjs";
4
5
 
5
6
  const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
6
7
  const version = JSON.parse(
7
8
  readFileSync(join(packageRoot, "package.json"), "utf8"),
8
9
  ).version;
10
+ const repo = githubRepoFromPackage(packageRoot);
9
11
 
10
12
  process.stderr.write(`dslinter: scanner binary not available.
11
13
 
12
- This npm package is NOT the same as \`cargo install dslint\` on crates.io (that is a
13
- different "design file" linter and will crash or misbehave).
14
+ Do NOT run: cargo install dslint
15
+ That installs a different package on crates.io (design-file linter).
14
16
 
15
- To run the design-system scanner:
17
+ Install the design-system scanner from this repo instead:
16
18
 
17
- 1. Re-run after a GitHub release exists for v${version} (prebuilt download), or
18
- 2. Build from this repo and point at it:
19
- cargo install --git https://github.com/jrmybtlr/DSLinter dslinter --locked
20
- export DSLINT_BIN="$(command -v dslinter)"
21
- npx dslinter ...
22
- 3. Or set DSLINT_BIN to your local target/release/dslinter
19
+ cargo install --git https://github.com/${repo} dslinter --locked
20
+ export DSLINT_BIN="$(command -v dslinter)"
21
+ npx dslinter ...
23
22
 
24
- Releases: https://github.com/jrmybtlr/DSLinter/releases
23
+ If releases exist at https://github.com/${repo}/releases/tag/v${version} but
24
+ download failed, the repo may be private — set a GitHub token then retry:
25
+
26
+ export GITHUB_TOKEN=ghp_... # needs read access to ${repo}
27
+ npx dslinter
28
+
29
+ Or point at a local build: DSLINT_BIN=/path/to/dslinter
30
+
31
+ Tip: DSLINT_VERBOSE=1 shows each download URL tried.
25
32
  `);
@@ -58,6 +58,14 @@ export function releaseAssetBaseName(proc = process) {
58
58
  return null;
59
59
  }
60
60
 
61
+ /** Primary GitHub asset name plus legacy `dslint-*` names from older releases. */
62
+ export function releaseAssetCandidateNames(proc = process) {
63
+ const primary = releaseAssetBaseName(proc);
64
+ if (!primary) return [];
65
+ const legacy = primary.replace(/^dslinter/, "dslint");
66
+ return legacy === primary ? [primary] : [primary, legacy];
67
+ }
68
+
61
69
  /**
62
70
  * @param {string} packageRoot — directory containing package.json
63
71
  * @param {NodeJS.Process} [proc]
@@ -1,10 +1,16 @@
1
1
  import { join } from "node:path";
2
2
  import { describe, expect, it } from "vitest";
3
+ import {
4
+ directAssetUrl,
5
+ pickReleaseAsset,
6
+ releaseTagsToTry,
7
+ } from "../scripts/github-release.mjs";
3
8
  import {
4
9
  CLI_BINARY_NAME,
5
10
  DEFAULT_GITHUB_REPO,
6
11
  parseGitHubRepo,
7
12
  releaseAssetBaseName,
13
+ releaseAssetCandidateNames,
8
14
  vendorBinaryPath,
9
15
  } from "../scripts/resolve-dslint-binary.mjs";
10
16
 
@@ -33,6 +39,55 @@ describe("parseGitHubRepo", () => {
33
39
  });
34
40
  });
35
41
 
42
+ describe("directAssetUrl", () => {
43
+ it("uses releases/latest/download for latest tag", () => {
44
+ expect(
45
+ directAssetUrl("jrmybtlr/DSLinter", "latest", "dslinter-aarch64-apple-darwin"),
46
+ ).toBe(
47
+ "https://github.com/jrmybtlr/DSLinter/releases/latest/download/dslinter-aarch64-apple-darwin",
48
+ );
49
+ });
50
+
51
+ it("uses releases/download for version tags", () => {
52
+ expect(
53
+ directAssetUrl("jrmybtlr/DSLinter", "v0.0.12", "dslinter-aarch64-apple-darwin"),
54
+ ).toBe(
55
+ "https://github.com/jrmybtlr/DSLinter/releases/download/v0.0.12/dslinter-aarch64-apple-darwin",
56
+ );
57
+ });
58
+ });
59
+
60
+ describe("releaseTagsToTry", () => {
61
+ it("tries v-prefixed tag first", () => {
62
+ expect(releaseTagsToTry("0.0.12")).toEqual(["v0.0.12", "0.0.12", "latest"]);
63
+ });
64
+ });
65
+
66
+ describe("releaseAssetCandidateNames", () => {
67
+ it("includes legacy dslint asset name", () => {
68
+ expect(releaseAssetCandidateNames(proc("darwin", "arm64"))).toEqual([
69
+ "dslinter-aarch64-apple-darwin",
70
+ "dslint-aarch64-apple-darwin",
71
+ ]);
72
+ });
73
+ });
74
+
75
+ describe("pickReleaseAsset", () => {
76
+ it("prefers primary name then legacy", () => {
77
+ const release = {
78
+ assets: [
79
+ {
80
+ name: "dslint-aarch64-apple-darwin",
81
+ browser_download_url: "https://example.com/legacy",
82
+ },
83
+ ],
84
+ };
85
+ expect(
86
+ pickReleaseAsset(release, releaseAssetCandidateNames(proc("darwin", "arm64"))),
87
+ ).toMatchObject({ name: "dslint-aarch64-apple-darwin" });
88
+ });
89
+ });
90
+
36
91
  describe("releaseAssetBaseName", () => {
37
92
  it("maps darwin arm64", () => {
38
93
  expect(releaseAssetBaseName(proc("darwin", "arm64"))).toBe(