@asale/repolish 0.3.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/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # repolish
2
+
3
+ Score and improve what an open-source repository looks like to a first-time
4
+ visitor — from the command line.
5
+
6
+ ```bash
7
+ npx @asale/repolish check .
8
+ ```
9
+
10
+ This package is a thin launcher. On install it downloads the release binary for
11
+ your platform from [the GitHub releases][releases], verifies its `.sha256`, and
12
+ runs it. There is no JavaScript implementation of the checks — the tool itself
13
+ is a single static Rust binary.
14
+
15
+ If you already have a Rust toolchain, `cargo install repolish` gets you the same
16
+ binary without this wrapper.
17
+
18
+ Linux builds are glibc-only. On musl (Alpine and similar) the installer says so
19
+ and stops rather than leaving behind a binary that cannot run; use
20
+ `cargo install repolish` there.
21
+
22
+ Everything else — what it checks, how scoring works, the GitHub Action — is in
23
+ [the main README](https://github.com/asale-ai/repolish#readme).
24
+
25
+ [releases]: https://github.com/asale-ai/repolish/releases
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ // 启动器:把参数原样交给真正的二进制,退出码原样带回来。
3
+ //
4
+ // 退出码必须传回去,这一点不能有例外:repolish 的 `--min-score` 门禁、
5
+ // `verify` 的失败、`--base` 的基线错误,全靠退出码区分,而 CI 读的正是
6
+ // `npx repolish` 的退出码。
7
+ //
8
+ // 装的时候跳过了 scripts(`npm ci --ignore-scripts` 在 CI 里很常见)就
9
+ // 现下一次。第一次运行时多等几秒,好过甩一句「找不到二进制」。
10
+
11
+ 'use strict';
12
+
13
+ const { spawnSync } = require('child_process');
14
+ const { download, isInstalled, BIN_PATH } = require('../install.js');
15
+
16
+ function run() {
17
+ const res = spawnSync(BIN_PATH, process.argv.slice(2), { stdio: 'inherit' });
18
+ if (res.error) {
19
+ console.error(`repolish: could not run ${BIN_PATH}: ${res.error.message}`);
20
+ process.exit(1);
21
+ }
22
+ // 被信号杀掉时 status 是 null。返回 1 而不是 0——
23
+ // 一个被 SIGKILL 的检查在 CI 里绝不能读成「通过」。
24
+ process.exit(res.status === null ? 1 : res.status);
25
+ }
26
+
27
+ if (isInstalled()) {
28
+ run();
29
+ } else {
30
+ download().then(run, (e) => {
31
+ console.error(`repolish: ${e.message}`);
32
+ process.exit(1);
33
+ });
34
+ }
package/install.js ADDED
@@ -0,0 +1,272 @@
1
+ // 下载与这个 npm 包版本严格对应的那一份发布二进制,校验 sha256,解包。
2
+ //
3
+ // 为什么要有这个包:repolish 检查的仓库绝大多数不是 Rust 项目,而
4
+ // `cargo install repolish` 要求对方先装一套 Rust 工具链。对一个「跑一次看看
5
+ // 分数」的工具来说,那是一道劝退的门槛。`npx repolish check .` 没有这道门槛。
6
+ //
7
+ // 三条规矩:
8
+ //
9
+ // - **版本严格对齐。** package.json 的 version 就是要下载的 release tag。
10
+ // npm 上装到 0.4.0 却跑起来是 0.3.0 的二进制,是最难查的一类问题。
11
+ // - **校验和必须过。** 对不上就删掉、报错、退出,绝不留下一个可执行文件。
12
+ // - **失败要说得出下一步。** 一个没有构建产物的平台(musl、32 位)拿到的
13
+ // 应该是「用 cargo install」,不是一句 404。
14
+
15
+ 'use strict';
16
+
17
+ const fs = require('fs');
18
+ const os = require('os');
19
+ const path = require('path');
20
+ const zlib = require('zlib');
21
+ const crypto = require('crypto');
22
+ const { execFileSync } = require('child_process');
23
+
24
+ const REPO = 'asale-ai/repolish';
25
+ const VERSION = require('./package.json').version;
26
+ const BIN_DIR = path.join(__dirname, 'bin');
27
+ const EXE = process.platform === 'win32' ? 'repolish.exe' : 'repolish';
28
+ const BIN_PATH = path.join(BIN_DIR, EXE);
29
+
30
+ /// release 的产物命名是 .github/workflows/release.yml 定下的契约:
31
+ /// repolish-v{version}-{target}.{tar.gz|zip}
32
+ /// install.sh 与 action.yml 拼的是同一个名字。改一处就要改三处。
33
+ function target() {
34
+ const arch = { x64: 'x86_64', arm64: 'aarch64' }[process.arch];
35
+ if (!arch) {
36
+ fail(
37
+ `repolish ships x86_64 and aarch64 builds; this machine is ${process.arch}.`
38
+ );
39
+ }
40
+
41
+ switch (process.platform) {
42
+ case 'darwin':
43
+ return { triple: `${arch}-apple-darwin`, ext: 'tar.gz' };
44
+ case 'win32':
45
+ if (arch !== 'x86_64') {
46
+ fail('repolish ships only an x86_64 build for Windows today.');
47
+ }
48
+ return { triple: 'x86_64-pc-windows-msvc', ext: 'zip' };
49
+ case 'linux':
50
+ // Linux 的构建是 glibc-only。装一个跑起来就 linker error 的二进制,
51
+ // 比明说「这里装不了」糟得多——install.sh 在同一处做同样的判断。
52
+ if (isMusl()) {
53
+ fail(
54
+ 'musl libc detected (Alpine or similar). repolish only ships glibc builds today.'
55
+ );
56
+ }
57
+ return { triple: `${arch}-unknown-linux-gnu`, ext: 'tar.gz' };
58
+ default:
59
+ fail(`unsupported operating system: ${process.platform}`);
60
+ }
61
+ }
62
+
63
+ function isMusl() {
64
+ const header = process.report && process.report.getReport().header;
65
+ if (header && typeof header.glibcVersionRuntime === 'string') return false;
66
+ if (header && 'glibcVersionRuntime' in header) return true;
67
+ // 老版本 Node 没有这个字段。退回问 ldd,问不出来就当作 glibc——
68
+ // 猜错的代价是一句清楚的 linker error,而误判成 musl 会拦下本来装得上的机器。
69
+ try {
70
+ return /musl/i.test(execFileSync('ldd', ['--version'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }));
71
+ } catch (e) {
72
+ try {
73
+ return /musl/i.test(e.stderr || '');
74
+ } catch (_) {
75
+ return false;
76
+ }
77
+ }
78
+ }
79
+
80
+ function fail(message) {
81
+ console.error(`repolish: ${message}`);
82
+ console.error('Build it from source instead: cargo install repolish');
83
+ console.error(`Or download a binary: https://github.com/${REPO}/releases`);
84
+ process.exit(1);
85
+ }
86
+
87
+ /// 一次 HTTP 失败是「服务器说了不」,还是「线路抖了一下」。
88
+ ///
89
+ /// 这两件事必须分开,因为它们的正确反应相反:404 说明这个东西确实不存在,
90
+ /// 重试一百次也一样;ETIMEDOUT / ECONNRESET 只说明这一次没通,而 GitHub 的
91
+ /// release 下载偶尔就是会这样。
92
+ function isRetryable(err) {
93
+ if (err && typeof err.status === 'number') {
94
+ // 4xx 是服务器的结论,别浪费时间;5xx 和 408/429 值得再试一次
95
+ return err.status >= 500 || err.status === 408 || err.status === 429;
96
+ }
97
+ return true; // 连接层的错误,一律重试
98
+ }
99
+
100
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
101
+
102
+ /// 单次请求的超时。
103
+ ///
104
+ /// **必须显式设。** 默认没有超时,一条走不通的线路要等操作系统的 connect
105
+ /// 超时,在 macOS 上是四十秒左右;乘上三次重试就是两分钟的沉默。实测过:
106
+ /// 一次 `npm install` 在这里挂了 124 秒才报错。
107
+ const TIMEOUT_MS = 20000;
108
+
109
+ async function once(url, redirects = 0) {
110
+ if (redirects > 5) throw new Error(`too many redirects for ${url}`);
111
+ const https = require('https');
112
+ return new Promise((resolve, reject) => {
113
+ const req = https.get(
114
+ url,
115
+ { headers: { 'user-agent': `repolish-npm/${VERSION}` }, timeout: TIMEOUT_MS },
116
+ (res) => {
117
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
118
+ res.resume();
119
+ resolve(once(new URL(res.headers.location, url).toString(), redirects + 1));
120
+ return;
121
+ }
122
+ if (res.statusCode !== 200) {
123
+ res.resume();
124
+ const e = new Error(`${res.statusCode} for ${url}`);
125
+ e.status = res.statusCode;
126
+ reject(e);
127
+ return;
128
+ }
129
+ const chunks = [];
130
+ res.on('data', (c) => chunks.push(c));
131
+ res.on('end', () => resolve(Buffer.concat(chunks)));
132
+ res.on('error', reject);
133
+ }
134
+ );
135
+ // timeout 事件不会自己中断请求——不 destroy 的话它就一直挂着
136
+ req.on('timeout', () => {
137
+ req.destroy(new Error(`timed out after ${TIMEOUT_MS / 1000}s`));
138
+ });
139
+ req.on('error', reject);
140
+ });
141
+ }
142
+
143
+ /// 重试三次,退避 1s / 2s。
144
+ ///
145
+ /// 装东西是使用者做的第一件事,而 `npx` 那一下没有「再试一次」的按钮——
146
+ /// 第一次超时就等于这个工具在他那里根本装不上。
147
+ ///
148
+ /// 每次重试都说出来。一个安静地卡着的安装过程,使用者唯一能做的判断是
149
+ /// 「它是不是死了」,而那个判断通常以 Ctrl-C 结束。
150
+ async function get(url, attempts = 3) {
151
+ let last;
152
+ for (let i = 0; i < attempts; i++) {
153
+ try {
154
+ return await once(url);
155
+ } catch (e) {
156
+ last = e;
157
+ if (!isRetryable(e) || i === attempts - 1) break;
158
+ const wait = 1000 * 2 ** i;
159
+ process.stderr.write(
160
+ `repolish: ${e.message} — retrying in ${wait / 1000}s (${i + 2}/${attempts})\n`
161
+ );
162
+ await sleep(wait);
163
+ }
164
+ }
165
+ throw last;
166
+ }
167
+
168
+ /// tar.gz 里只有一个我们要的文件。为了一个只解一份归档的功能拖进 tar 依赖
169
+ /// 不值当,所以:gzip 用 Node 自带的 zlib 解,tar 头自己读——格式是 512 字节
170
+ /// 定长块,读起来比引一个包短。
171
+ function extractFromTar(buf, wanted) {
172
+ let off = 0;
173
+ while (off + 512 <= buf.length) {
174
+ const name = buf.toString('utf8', off, off + 100).replace(/\0.*$/, '');
175
+ if (!name) break;
176
+ const size = parseInt(buf.toString('utf8', off + 124, off + 136).replace(/\0.*$/, '').trim(), 8) || 0;
177
+ const start = off + 512;
178
+ if (path.basename(name) === wanted && size > 0) {
179
+ return buf.subarray(start, start + size);
180
+ }
181
+ off = start + Math.ceil(size / 512) * 512;
182
+ }
183
+ return null;
184
+ }
185
+
186
+ /// zip 也一样:只为取一个文件,没必要引依赖。找到 local file header,
187
+ /// 按压缩方法解出来。
188
+ function extractFromZip(buf, wanted) {
189
+ for (let i = 0; i + 30 <= buf.length; i++) {
190
+ if (buf.readUInt32LE(i) !== 0x04034b50) continue;
191
+ const method = buf.readUInt16LE(i + 8);
192
+ const compressed = buf.readUInt32LE(i + 18);
193
+ const nameLen = buf.readUInt16LE(i + 26);
194
+ const extraLen = buf.readUInt16LE(i + 28);
195
+ const name = buf.toString('utf8', i + 30, i + 30 + nameLen);
196
+ const start = i + 30 + nameLen + extraLen;
197
+ if (path.basename(name) !== wanted) continue;
198
+ const body = buf.subarray(start, start + compressed);
199
+ return method === 0 ? body : zlib.inflateRawSync(body);
200
+ }
201
+ return null;
202
+ }
203
+
204
+ async function download() {
205
+ const { triple, ext } = target();
206
+ const tag = `v${VERSION}`;
207
+ const asset = `repolish-${tag}-${triple}.${ext}`;
208
+ const base = `https://github.com/${REPO}/releases/download/${tag}`;
209
+
210
+ process.stderr.write(`repolish: fetching ${asset}\n`);
211
+
212
+ let archive;
213
+ try {
214
+ archive = await get(`${base}/${asset}`);
215
+ } catch (e) {
216
+ fail(`could not download ${asset} (${e.message}).`);
217
+ }
218
+
219
+ // 校验和对不上就什么都不留下。一个来路不明的可执行文件放在 node_modules
220
+ // 里,比装不上危险得多。
221
+ //
222
+ // **拿不到校验文件与拿到一个不匹配的,反应不同;但拿不到的两种原因,
223
+ // 反应也不同。** 404 是「这个 release 确实没有 .sha256」——旧版本就是这样,
224
+ // 为此拒绝安装说不过去。而 ECONNRESET 只是这一次没通,它对那个文件存不存在
225
+ // 一无所知,把它当成「没有校验和」就是让一次网络抖动把校验悄悄关掉。
226
+ // 这一条是实测撞出来的:归档下下来了,紧接着那个几十字节的 .sha256 被
227
+ // 重置了连接,于是一个未经验证的可执行文件带着一行警告落了盘。
228
+ let sums;
229
+ try {
230
+ sums = (await get(`${base}/${asset}.sha256`)).toString('utf8');
231
+ } catch (e) {
232
+ if (e && e.status === 404) {
233
+ process.stderr.write(
234
+ `repolish: warning — ${asset}.sha256 is not in this release; installing unverified\n`
235
+ );
236
+ } else {
237
+ fail(
238
+ `could not fetch ${asset}.sha256 (${e.message}).\n` +
239
+ ' The archive downloaded but could not be verified, so nothing was installed.\n' +
240
+ ' This is usually transient — run it again.'
241
+ );
242
+ }
243
+ }
244
+ if (sums) {
245
+ const expected = sums.trim().split(/\s+/)[0];
246
+ const actual = crypto.createHash('sha256').update(archive).digest('hex');
247
+ if (expected && expected !== actual) {
248
+ fail(`checksum mismatch for ${asset}\n expected ${expected}\n got ${actual}`);
249
+ }
250
+ }
251
+
252
+ const body =
253
+ ext === 'zip'
254
+ ? extractFromZip(archive, EXE)
255
+ : extractFromTar(zlib.gunzipSync(archive), EXE);
256
+ if (!body) fail(`${asset} did not contain ${EXE}`);
257
+
258
+ fs.mkdirSync(BIN_DIR, { recursive: true });
259
+ fs.writeFileSync(BIN_PATH, body, { mode: 0o755 });
260
+ process.stderr.write(`repolish: installed ${BIN_PATH}\n`);
261
+ }
262
+
263
+ function isInstalled() {
264
+ return fs.existsSync(BIN_PATH) && fs.statSync(BIN_PATH).size > 0;
265
+ }
266
+
267
+ module.exports = { download, isInstalled, BIN_PATH, VERSION };
268
+
269
+ if (require.main === module) {
270
+ if (isInstalled()) process.exit(0);
271
+ download().catch((e) => fail(e.message));
272
+ }
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@asale/repolish",
3
+ "version": "0.3.0",
4
+ "description": "Scores what your open-source repo looks like to a first-time visitor, and checks that the commands your README promises actually exist.",
5
+ "keywords": ["readme", "documentation", "lint", "cli", "open-source", "repository"],
6
+ "homepage": "https://github.com/asale-ai/repolish#readme",
7
+ "bugs": "https://github.com/asale-ai/repolish/issues",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/asale-ai/repolish.git",
11
+ "directory": "npm"
12
+ },
13
+ "license": "MIT OR Apache-2.0",
14
+ "bin": { "repolish": "bin/repolish.js" },
15
+ "files": ["bin/repolish.js", "install.js", "README.md"],
16
+ "scripts": {
17
+ "postinstall": "node install.js",
18
+ "test": "node test.js"
19
+ },
20
+ "publishConfig": { "access": "public" },
21
+ "engines": { "node": ">=18" },
22
+ "os": ["darwin", "linux", "win32"],
23
+ "cpu": ["x64", "arm64"]
24
+ }