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.
Files changed (3) hide show
  1. package/install.js +159 -33
  2. package/package.json +1 -1
  3. 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 VERSION = require("./package.json").version;
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 download(url) {
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
- .get(url, { headers: { "User-Agent": "ailonk-search-npm" } }, (res) => {
45
- if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
46
- return download(res.headers.location).then(resolve, reject);
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", () => resolve(Buffer.concat(chunks)));
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
- .on("error", reject);
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 = `v${VERSION}`;
64
- const url = `https://github.com/${REPO}/releases/download/${tag}/${assetName}`;
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 === VERSION) {
76
- console.log(`${BIN_NAME} v${VERSION} already installed`);
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 download(url);
86
- fs.writeFileSync(dest, data);
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(dest, 0o755);
206
+ fs.chmodSync(tmp, 0o755);
89
207
  }
90
- fs.writeFileSync(versionFile, VERSION);
91
- console.log(`Installed ${BIN_NAME} v${VERSION} to ${dest}`);
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
- console.error(
95
- `\nYou can build from source instead:\n cargo install --git https://github.com/${REPO}.git`
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ailonk-search",
3
- "version": "0.1.2",
3
+ "version": "0.1.5",
4
4
  "description": "MCP server using real Chrome for web search & Markdown extraction",
5
5
  "license": "MIT",
6
6
  "repository": {
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
- if (err.code === "ENOENT") {
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
- child.on("exit", (code) => process.exit(code != null ? code : 1));
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
+ });