ailonk-search 0.1.2 → 0.1.5
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/install.js +159 -33
- package/package.json +1 -1
- package/run.js +26 -8
package/install.js
CHANGED
|
@@ -8,20 +8,19 @@ const http = require("http");
|
|
|
8
8
|
|
|
9
9
|
const REPO = "lk19940215/ailonk-search";
|
|
10
10
|
const BIN_NAME = "ailonk-search";
|
|
11
|
-
const
|
|
11
|
+
const MAX_RETRIES = 3;
|
|
12
|
+
const MAX_REDIRECTS = 5;
|
|
13
|
+
const TIMEOUT_MS = 60_000;
|
|
14
|
+
const VERSION_TAG_RE = /^v?\d+\.\d+\.\d+(-[\w.-]+)?$/i;
|
|
12
15
|
|
|
13
16
|
function getPlatformTarget() {
|
|
14
|
-
const arch = process.arch;
|
|
15
|
-
const platform = process.platform;
|
|
16
|
-
|
|
17
17
|
const map = {
|
|
18
18
|
"darwin-arm64": "aarch64-apple-darwin",
|
|
19
19
|
"darwin-x64": "x86_64-apple-darwin",
|
|
20
20
|
"linux-x64": "x86_64-unknown-linux-gnu",
|
|
21
21
|
"win32-x64": "x86_64-pc-windows-msvc",
|
|
22
22
|
};
|
|
23
|
-
|
|
24
|
-
const key = `${platform}-${arch}`;
|
|
23
|
+
const key = `${process.platform}-${process.arch}`;
|
|
25
24
|
const target = map[key];
|
|
26
25
|
if (!target) {
|
|
27
26
|
console.error(`Unsupported platform: ${key}`);
|
|
@@ -37,65 +36,192 @@ function getBinaryName(target) {
|
|
|
37
36
|
: `${BIN_NAME}-${target}`;
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
function
|
|
39
|
+
function applyMirror(url) {
|
|
40
|
+
const mirror = process.env.GITHUB_MIRROR;
|
|
41
|
+
if (mirror) {
|
|
42
|
+
return `${mirror.replace(/\/+$/, "")}/${url}`;
|
|
43
|
+
}
|
|
44
|
+
return url;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeVersionTag(input, source) {
|
|
48
|
+
const tag = input.startsWith("v") ? input : `v${input}`;
|
|
49
|
+
if (!VERSION_TAG_RE.test(tag)) {
|
|
50
|
+
throw new Error(`Invalid version tag from ${source}: ${input}`);
|
|
51
|
+
}
|
|
52
|
+
return tag;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function request(url, options, redirectsLeft = MAX_REDIRECTS) {
|
|
41
56
|
return new Promise((resolve, reject) => {
|
|
42
|
-
const client = url.startsWith("https") ? https : http;
|
|
43
|
-
client
|
|
44
|
-
.
|
|
45
|
-
if (
|
|
46
|
-
return
|
|
57
|
+
const client = url.startsWith("https:") ? https : http;
|
|
58
|
+
const req = client.get(url, options, (res) => {
|
|
59
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
60
|
+
if (redirectsLeft <= 0) {
|
|
61
|
+
return reject(new Error(`Too many redirects for ${url}`));
|
|
47
62
|
}
|
|
63
|
+
const next = new URL(res.headers.location, url).href;
|
|
64
|
+
return request(next, options, redirectsLeft - 1).then(resolve, reject);
|
|
65
|
+
}
|
|
66
|
+
resolve(res);
|
|
67
|
+
});
|
|
68
|
+
req.on("timeout", () => {
|
|
69
|
+
req.destroy();
|
|
70
|
+
reject(new Error(`Timeout fetching ${url}`));
|
|
71
|
+
});
|
|
72
|
+
req.on("error", reject);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function fetchJSON(url) {
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
request(
|
|
79
|
+
url,
|
|
80
|
+
{
|
|
81
|
+
headers: {
|
|
82
|
+
"User-Agent": "ailonk-search-npm",
|
|
83
|
+
Accept: "application/vnd.github.v3+json",
|
|
84
|
+
},
|
|
85
|
+
timeout: TIMEOUT_MS,
|
|
86
|
+
}
|
|
87
|
+
).then(
|
|
88
|
+
(res) => {
|
|
48
89
|
if (res.statusCode !== 200) {
|
|
49
90
|
return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
|
50
91
|
}
|
|
51
92
|
const chunks = [];
|
|
52
93
|
res.on("data", (c) => chunks.push(c));
|
|
53
|
-
res.on("end", () =>
|
|
94
|
+
res.on("end", () => {
|
|
95
|
+
try {
|
|
96
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString()));
|
|
97
|
+
} catch {
|
|
98
|
+
reject(new Error(`Invalid JSON from ${url}`));
|
|
99
|
+
}
|
|
100
|
+
});
|
|
54
101
|
res.on("error", reject);
|
|
55
|
-
}
|
|
56
|
-
|
|
102
|
+
},
|
|
103
|
+
reject
|
|
104
|
+
);
|
|
57
105
|
});
|
|
58
106
|
}
|
|
59
107
|
|
|
108
|
+
function download(url) {
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
request(url, { headers: { "User-Agent": "ailonk-search-npm" }, timeout: TIMEOUT_MS }).then(
|
|
111
|
+
(res) => {
|
|
112
|
+
if (res.statusCode !== 200) {
|
|
113
|
+
return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
|
114
|
+
}
|
|
115
|
+
const total = parseInt(res.headers["content-length"], 10) || 0;
|
|
116
|
+
const chunks = [];
|
|
117
|
+
let received = 0;
|
|
118
|
+
res.on("data", (c) => {
|
|
119
|
+
chunks.push(c);
|
|
120
|
+
received += c.length;
|
|
121
|
+
if (total > 0 && process.stderr.isTTY) {
|
|
122
|
+
const pct = ((received / total) * 100).toFixed(0);
|
|
123
|
+
const mb = (received / 1048576).toFixed(1);
|
|
124
|
+
process.stderr.write(`\r Progress: ${mb}MB / ${(total / 1048576).toFixed(1)}MB (${pct}%)`);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
res.on("end", () => {
|
|
128
|
+
if (process.stderr.isTTY) process.stderr.write("\n");
|
|
129
|
+
resolve(Buffer.concat(chunks));
|
|
130
|
+
});
|
|
131
|
+
res.on("error", reject);
|
|
132
|
+
},
|
|
133
|
+
reject
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function downloadWithRetry(url, retries = MAX_RETRIES) {
|
|
139
|
+
for (let i = 1; i <= retries; i++) {
|
|
140
|
+
try {
|
|
141
|
+
return await download(url);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
if (i === retries) throw err;
|
|
144
|
+
const wait = i * 2000;
|
|
145
|
+
console.log(` Retry ${i}/${retries - 1} in ${wait / 1000}s: ${err.message}`);
|
|
146
|
+
await new Promise((r) => setTimeout(r, wait));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function getTargetVersion() {
|
|
152
|
+
const pinned = process.env.AILONK_VERSION;
|
|
153
|
+
if (pinned) {
|
|
154
|
+
const tag = normalizeVersionTag(pinned, "AILONK_VERSION");
|
|
155
|
+
console.log(`Using pinned version: ${tag} (AILONK_VERSION)`);
|
|
156
|
+
return tag;
|
|
157
|
+
}
|
|
158
|
+
const apiUrl = `https://api.github.com/repos/${REPO}/releases/latest`;
|
|
159
|
+
const tryUrl = process.env.GITHUB_MIRROR ? applyMirror(apiUrl) : apiUrl;
|
|
160
|
+
try {
|
|
161
|
+
const release = await fetchJSON(tryUrl);
|
|
162
|
+
if (!release.tag_name) {
|
|
163
|
+
throw new Error("release response missing tag_name");
|
|
164
|
+
}
|
|
165
|
+
return normalizeVersionTag(release.tag_name, "GitHub API");
|
|
166
|
+
} catch (err) {
|
|
167
|
+
const fallback = normalizeVersionTag(require("./package.json").version, "package.json");
|
|
168
|
+
console.log(` Could not fetch latest release (${err.message}), falling back to ${fallback}`);
|
|
169
|
+
return fallback;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
60
173
|
async function main() {
|
|
61
174
|
const target = getPlatformTarget();
|
|
62
175
|
const assetName = getBinaryName(target);
|
|
63
|
-
const tag =
|
|
64
|
-
const
|
|
176
|
+
const tag = await getTargetVersion();
|
|
177
|
+
const version = tag.replace(/^v/, "");
|
|
65
178
|
|
|
66
179
|
const binDir = __dirname;
|
|
67
|
-
const dest = path.join(
|
|
68
|
-
binDir,
|
|
69
|
-
process.platform === "win32" ? `${BIN_NAME}.exe` : BIN_NAME
|
|
70
|
-
);
|
|
71
|
-
|
|
180
|
+
const dest = path.join(binDir, process.platform === "win32" ? `${BIN_NAME}.exe` : BIN_NAME);
|
|
72
181
|
const versionFile = path.join(binDir, ".installed-version");
|
|
182
|
+
|
|
73
183
|
if (fs.existsSync(dest) && fs.existsSync(versionFile)) {
|
|
74
184
|
const installed = fs.readFileSync(versionFile, "utf8").trim();
|
|
75
|
-
if (installed ===
|
|
76
|
-
console.log(`${BIN_NAME} v${
|
|
185
|
+
if (installed === version) {
|
|
186
|
+
console.log(`${BIN_NAME} v${version} already installed`);
|
|
77
187
|
return;
|
|
78
188
|
}
|
|
79
189
|
}
|
|
80
190
|
|
|
191
|
+
const rawUrl = `https://github.com/${REPO}/releases/download/${tag}/${assetName}`;
|
|
192
|
+
const url = applyMirror(rawUrl);
|
|
193
|
+
|
|
81
194
|
console.log(`Downloading ${BIN_NAME} ${tag} for ${target}...`);
|
|
82
195
|
console.log(` ${url}`);
|
|
83
196
|
|
|
84
197
|
try {
|
|
85
|
-
const data = await
|
|
86
|
-
|
|
198
|
+
const data = await downloadWithRetry(url);
|
|
199
|
+
if (!data.length) {
|
|
200
|
+
throw new Error("Downloaded file is empty");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const tmp = `${dest}.tmp`;
|
|
204
|
+
fs.writeFileSync(tmp, data);
|
|
87
205
|
if (process.platform !== "win32") {
|
|
88
|
-
fs.chmodSync(
|
|
206
|
+
fs.chmodSync(tmp, 0o755);
|
|
89
207
|
}
|
|
90
|
-
fs.
|
|
91
|
-
|
|
208
|
+
fs.renameSync(tmp, dest);
|
|
209
|
+
fs.writeFileSync(versionFile, version);
|
|
210
|
+
console.log(`Installed ${BIN_NAME} v${version} to ${dest}`);
|
|
92
211
|
} catch (err) {
|
|
212
|
+
const tmp = `${dest}.tmp`;
|
|
213
|
+
if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
|
|
93
214
|
console.error(`Failed to download ${BIN_NAME}: ${err.message}`);
|
|
94
|
-
|
|
95
|
-
`\
|
|
96
|
-
|
|
215
|
+
if (!process.env.GITHUB_MIRROR) {
|
|
216
|
+
console.error(`\nTip: In China, set GITHUB_MIRROR to speed up downloads:`);
|
|
217
|
+
console.error(` GITHUB_MIRROR=https://ghproxy.com npm install ailonk-search`);
|
|
218
|
+
}
|
|
219
|
+
console.error(`\nOr build from source:\n cargo install --git https://github.com/${REPO}.git`);
|
|
97
220
|
process.exit(1);
|
|
98
221
|
}
|
|
99
222
|
}
|
|
100
223
|
|
|
101
|
-
main()
|
|
224
|
+
main().catch((err) => {
|
|
225
|
+
console.error(err.message);
|
|
226
|
+
process.exit(1);
|
|
227
|
+
});
|
package/package.json
CHANGED
package/run.js
CHANGED
|
@@ -1,26 +1,44 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
|
+
const fs = require("fs");
|
|
4
5
|
const { spawn } = require("child_process");
|
|
5
6
|
const path = require("path");
|
|
6
7
|
|
|
7
8
|
const ext = process.platform === "win32" ? ".exe" : "";
|
|
8
9
|
const bin = path.join(__dirname, `ailonk-search${ext}`);
|
|
9
10
|
|
|
11
|
+
if (!fs.existsSync(bin)) {
|
|
12
|
+
console.error("ailonk-search binary not found.");
|
|
13
|
+
console.error("Run: npm rebuild ailonk-search");
|
|
14
|
+
console.error("Or: node install.js");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
10
18
|
const child = spawn(bin, process.argv.slice(2), {
|
|
11
19
|
stdio: "inherit",
|
|
12
20
|
env: process.env,
|
|
21
|
+
windowsHide: true,
|
|
13
22
|
});
|
|
14
23
|
|
|
15
24
|
child.on("error", (err) => {
|
|
16
|
-
|
|
17
|
-
console.error(
|
|
18
|
-
"ailonk-search binary not found. Run: npm rebuild ailonk-search"
|
|
19
|
-
);
|
|
20
|
-
} else {
|
|
21
|
-
console.error(err.message);
|
|
22
|
-
}
|
|
25
|
+
console.error(err.message);
|
|
23
26
|
process.exit(1);
|
|
24
27
|
});
|
|
25
28
|
|
|
26
|
-
|
|
29
|
+
for (const sig of ["SIGINT", "SIGTERM"]) {
|
|
30
|
+
process.on(sig, () => {
|
|
31
|
+
if (child.exitCode !== null || child.killed) return;
|
|
32
|
+
child.kill(process.platform === "win32" ? undefined : sig);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const SIGNAL_EXIT = { SIGINT: 130, SIGTERM: 143 };
|
|
37
|
+
|
|
38
|
+
child.on("exit", (code, signal) => {
|
|
39
|
+
if (signal) {
|
|
40
|
+
process.exit(SIGNAL_EXIT[signal] || 1);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
process.exit(code != null ? code : 1);
|
|
44
|
+
});
|