ace-tool-rs 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/package.json +42 -0
- package/run.js +563 -0
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ace-tool-rs",
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "MCP server for codebase indexing, semantic search, and prompt enhancement",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/missdeer/ace-tool-rs.git"
|
|
8
|
+
},
|
|
9
|
+
"author": "missdeer",
|
|
10
|
+
"license": "GPL-3.0-only",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/missdeer/ace-tool-rs/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/missdeer/ace-tool-rs#readme",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"mcp",
|
|
17
|
+
"codebase",
|
|
18
|
+
"indexing",
|
|
19
|
+
"semantic-search",
|
|
20
|
+
"prompt-enhancement",
|
|
21
|
+
"rust",
|
|
22
|
+
"cli"
|
|
23
|
+
],
|
|
24
|
+
"bin": {
|
|
25
|
+
"ace-tool-rs": "./run.js"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"run.js"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=14.14.0"
|
|
32
|
+
},
|
|
33
|
+
"os": [
|
|
34
|
+
"darwin",
|
|
35
|
+
"linux",
|
|
36
|
+
"win32"
|
|
37
|
+
],
|
|
38
|
+
"cpu": [
|
|
39
|
+
"x64",
|
|
40
|
+
"arm64"
|
|
41
|
+
]
|
|
42
|
+
}
|
package/run.js
ADDED
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawn } = require("child_process");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const https = require("https");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const crypto = require("crypto");
|
|
9
|
+
|
|
10
|
+
const PACKAGE_NAME = "ace-tool-rs";
|
|
11
|
+
const REPO_OWNER = "missdeer";
|
|
12
|
+
const REPO_NAME = "ace-tool-rs";
|
|
13
|
+
const MAX_REDIRECTS = 10;
|
|
14
|
+
const REQUEST_TIMEOUT = 60000; // 60 seconds
|
|
15
|
+
const MAX_RETRIES = 3;
|
|
16
|
+
const RETRY_DELAY = 1000; // 1 second
|
|
17
|
+
|
|
18
|
+
// Helper for retry with exponential backoff
|
|
19
|
+
async function withRetry(fn, retries = MAX_RETRIES) {
|
|
20
|
+
let lastError;
|
|
21
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
22
|
+
try {
|
|
23
|
+
return await fn();
|
|
24
|
+
} catch (err) {
|
|
25
|
+
lastError = err;
|
|
26
|
+
// Don't retry on non-retryable errors
|
|
27
|
+
if (
|
|
28
|
+
err.message.includes("Unsupported") ||
|
|
29
|
+
err.message.includes("rate limit") ||
|
|
30
|
+
err.message.includes("404")
|
|
31
|
+
) {
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
if (attempt < retries - 1) {
|
|
35
|
+
const delay = RETRY_DELAY * Math.pow(2, attempt);
|
|
36
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
throw lastError;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Cache package version
|
|
44
|
+
let cachedVersion = null;
|
|
45
|
+
|
|
46
|
+
// Get package version from package.json
|
|
47
|
+
function getPackageVersion() {
|
|
48
|
+
if (cachedVersion === null) {
|
|
49
|
+
cachedVersion = require("./package.json").version;
|
|
50
|
+
}
|
|
51
|
+
return cachedVersion;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Get cache directory based on OS
|
|
55
|
+
function getCacheDir() {
|
|
56
|
+
const homeDir = os.homedir();
|
|
57
|
+
const version = getPackageVersion();
|
|
58
|
+
let baseDir;
|
|
59
|
+
|
|
60
|
+
switch (process.platform) {
|
|
61
|
+
case "win32":
|
|
62
|
+
baseDir = path.join(
|
|
63
|
+
process.env.LOCALAPPDATA || path.join(homeDir, "AppData", "Local"),
|
|
64
|
+
PACKAGE_NAME
|
|
65
|
+
);
|
|
66
|
+
break;
|
|
67
|
+
case "darwin":
|
|
68
|
+
baseDir = path.join(homeDir, "Library", "Caches", PACKAGE_NAME);
|
|
69
|
+
break;
|
|
70
|
+
default:
|
|
71
|
+
baseDir = path.join(
|
|
72
|
+
process.env.XDG_CACHE_HOME || path.join(homeDir, ".cache"),
|
|
73
|
+
PACKAGE_NAME
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Include version in cache path to handle upgrades
|
|
78
|
+
return path.join(baseDir, version);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Get asset name based on platform (matching release.yml)
|
|
82
|
+
function getAssetName() {
|
|
83
|
+
const platform = process.platform;
|
|
84
|
+
const arch = process.arch;
|
|
85
|
+
|
|
86
|
+
switch (platform) {
|
|
87
|
+
case "darwin":
|
|
88
|
+
// macOS uses universal binary (supports both x64 and arm64)
|
|
89
|
+
return "ace-tool-rs_Darwin_universal.tar.gz";
|
|
90
|
+
case "linux":
|
|
91
|
+
if (arch !== "x64") {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`Unsupported architecture: ${arch} on Linux. Only x64 is supported.`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return "ace-tool-rs_Linux_x86_64.tar.gz";
|
|
97
|
+
case "win32":
|
|
98
|
+
if (arch !== "x64") {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`Unsupported architecture: ${arch} on Windows. Only x64 is supported.`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return "ace-tool-rs_Windows_x86_64.zip";
|
|
104
|
+
default:
|
|
105
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getBinaryName() {
|
|
110
|
+
return process.platform === "win32" ? `${PACKAGE_NAME}.exe` : PACKAGE_NAME;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function httpsGet(url, options = {}, redirectCount = 0) {
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
if (redirectCount > MAX_REDIRECTS) {
|
|
116
|
+
reject(new Error("Too many redirects"));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const req = https.get(url, options, (res) => {
|
|
121
|
+
// Handle redirects
|
|
122
|
+
if (
|
|
123
|
+
res.statusCode >= 300 &&
|
|
124
|
+
res.statusCode < 400 &&
|
|
125
|
+
res.headers.location
|
|
126
|
+
) {
|
|
127
|
+
// Consume response to free up connection
|
|
128
|
+
res.resume();
|
|
129
|
+
const redirectUrl = res.headers.location;
|
|
130
|
+
// Only follow HTTPS redirects
|
|
131
|
+
if (!redirectUrl.startsWith("https://")) {
|
|
132
|
+
reject(new Error(`Insecure redirect to: ${redirectUrl}`));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
httpsGet(redirectUrl, options, redirectCount + 1)
|
|
136
|
+
.then(resolve)
|
|
137
|
+
.catch(reject);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (res.statusCode === 403) {
|
|
142
|
+
res.resume();
|
|
143
|
+
reject(
|
|
144
|
+
new Error(
|
|
145
|
+
"GitHub API rate limit exceeded. Please try again later or set GITHUB_TOKEN environment variable."
|
|
146
|
+
)
|
|
147
|
+
);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (res.statusCode !== 200) {
|
|
152
|
+
res.resume();
|
|
153
|
+
reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const chunks = [];
|
|
158
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
159
|
+
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
160
|
+
res.on("error", reject);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
req.on("error", reject);
|
|
164
|
+
req.setTimeout(REQUEST_TIMEOUT, () => {
|
|
165
|
+
req.destroy();
|
|
166
|
+
reject(new Error("Request timeout"));
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Parse JSON with helpful error message
|
|
172
|
+
function parseJSON(data, context) {
|
|
173
|
+
try {
|
|
174
|
+
return JSON.parse(data.toString());
|
|
175
|
+
} catch (err) {
|
|
176
|
+
const preview = data.toString().slice(0, 200);
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Failed to parse ${context} response. GitHub may be experiencing issues. Response preview: ${preview}`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function getReleaseByTag(version) {
|
|
184
|
+
const tag = `v${version}`;
|
|
185
|
+
const url = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/tags/${tag}`;
|
|
186
|
+
const options = {
|
|
187
|
+
headers: {
|
|
188
|
+
"User-Agent": PACKAGE_NAME,
|
|
189
|
+
Accept: "application/vnd.github.v3+json",
|
|
190
|
+
...(process.env.GITHUB_TOKEN && {
|
|
191
|
+
Authorization: `token ${process.env.GITHUB_TOKEN}`,
|
|
192
|
+
}),
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const data = await httpsGet(url, options);
|
|
198
|
+
return parseJSON(data, "release");
|
|
199
|
+
} catch (error) {
|
|
200
|
+
// If the specific version tag doesn't exist, fall back to latest
|
|
201
|
+
if (error.message.includes("404")) {
|
|
202
|
+
console.log(
|
|
203
|
+
`Release v${version} not found, falling back to latest release...`
|
|
204
|
+
);
|
|
205
|
+
return getLatestRelease();
|
|
206
|
+
}
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function getLatestRelease() {
|
|
212
|
+
const url = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`;
|
|
213
|
+
const options = {
|
|
214
|
+
headers: {
|
|
215
|
+
"User-Agent": PACKAGE_NAME,
|
|
216
|
+
Accept: "application/vnd.github.v3+json",
|
|
217
|
+
...(process.env.GITHUB_TOKEN && {
|
|
218
|
+
Authorization: `token ${process.env.GITHUB_TOKEN}`,
|
|
219
|
+
}),
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const data = await httpsGet(url, options);
|
|
224
|
+
return parseJSON(data, "latest release");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function downloadToFile(url, destPath, options = {}, redirectCount = 0) {
|
|
228
|
+
return new Promise((resolve, reject) => {
|
|
229
|
+
if (redirectCount > MAX_REDIRECTS) {
|
|
230
|
+
reject(new Error("Too many redirects"));
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const file = fs.createWriteStream(destPath);
|
|
235
|
+
const req = https.get(url, options, (res) => {
|
|
236
|
+
// Handle redirects
|
|
237
|
+
if (
|
|
238
|
+
res.statusCode >= 300 &&
|
|
239
|
+
res.statusCode < 400 &&
|
|
240
|
+
res.headers.location
|
|
241
|
+
) {
|
|
242
|
+
res.resume(); // Consume response
|
|
243
|
+
file.close(() => {
|
|
244
|
+
try {
|
|
245
|
+
fs.unlinkSync(destPath);
|
|
246
|
+
} catch {}
|
|
247
|
+
const redirectUrl = res.headers.location;
|
|
248
|
+
if (!redirectUrl.startsWith("https://")) {
|
|
249
|
+
reject(new Error(`Insecure redirect to: ${redirectUrl}`));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
downloadToFile(redirectUrl, destPath, options, redirectCount + 1)
|
|
253
|
+
.then(resolve)
|
|
254
|
+
.catch(reject);
|
|
255
|
+
});
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (res.statusCode !== 200) {
|
|
260
|
+
res.resume(); // Consume response
|
|
261
|
+
file.close(() => {
|
|
262
|
+
try {
|
|
263
|
+
fs.unlinkSync(destPath);
|
|
264
|
+
} catch {}
|
|
265
|
+
reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
|
|
266
|
+
});
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
res.pipe(file);
|
|
271
|
+
file.on("finish", () => {
|
|
272
|
+
file.close(() => resolve());
|
|
273
|
+
});
|
|
274
|
+
file.on("error", (err) => {
|
|
275
|
+
file.close(() => {
|
|
276
|
+
try {
|
|
277
|
+
fs.unlinkSync(destPath);
|
|
278
|
+
} catch {}
|
|
279
|
+
reject(err);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
req.on("error", (err) => {
|
|
285
|
+
file.close(() => {
|
|
286
|
+
try {
|
|
287
|
+
fs.unlinkSync(destPath);
|
|
288
|
+
} catch {}
|
|
289
|
+
reject(err);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
req.setTimeout(REQUEST_TIMEOUT, () => {
|
|
294
|
+
req.destroy();
|
|
295
|
+
file.close(() => {
|
|
296
|
+
try {
|
|
297
|
+
fs.unlinkSync(destPath);
|
|
298
|
+
} catch {}
|
|
299
|
+
reject(new Error("Download timeout"));
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function extractTarGz(archivePath, destDir) {
|
|
306
|
+
return new Promise((resolve, reject) => {
|
|
307
|
+
const tar = spawn("tar", ["-xzf", archivePath, "-C", destDir], {
|
|
308
|
+
stdio: "inherit",
|
|
309
|
+
});
|
|
310
|
+
tar.on("close", (code) => {
|
|
311
|
+
if (code === 0) resolve();
|
|
312
|
+
else reject(new Error(`tar exited with code ${code}`));
|
|
313
|
+
});
|
|
314
|
+
tar.on("error", (err) => {
|
|
315
|
+
if (err.code === "ENOENT") {
|
|
316
|
+
reject(new Error("tar command not found. Please install tar."));
|
|
317
|
+
} else {
|
|
318
|
+
reject(err);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function extractZip(archivePath, destDir) {
|
|
325
|
+
return new Promise((resolve, reject) => {
|
|
326
|
+
// Escape paths for PowerShell: escape backticks and single quotes
|
|
327
|
+
const escapePath = (p) => p.replace(/`/g, "``").replace(/'/g, "''");
|
|
328
|
+
const unzipProcess = spawn(
|
|
329
|
+
"powershell",
|
|
330
|
+
[
|
|
331
|
+
"-NoProfile",
|
|
332
|
+
"-ExecutionPolicy",
|
|
333
|
+
"Bypass",
|
|
334
|
+
"-Command",
|
|
335
|
+
`Expand-Archive -LiteralPath '${escapePath(archivePath)}' -DestinationPath '${escapePath(destDir)}' -Force`,
|
|
336
|
+
],
|
|
337
|
+
{ stdio: "inherit" }
|
|
338
|
+
);
|
|
339
|
+
unzipProcess.on("close", (code) => {
|
|
340
|
+
if (code === 0) resolve();
|
|
341
|
+
else reject(new Error(`PowerShell Expand-Archive exited with code ${code}`));
|
|
342
|
+
});
|
|
343
|
+
unzipProcess.on("error", (err) => {
|
|
344
|
+
if (err.code === "ENOENT") {
|
|
345
|
+
reject(new Error("PowerShell not found. Please install PowerShell 5.0+."));
|
|
346
|
+
} else {
|
|
347
|
+
reject(err);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Move file with fallback for cross-device moves
|
|
354
|
+
function moveFile(src, dest) {
|
|
355
|
+
try {
|
|
356
|
+
fs.renameSync(src, dest);
|
|
357
|
+
} catch (err) {
|
|
358
|
+
if (err.code === "EXDEV") {
|
|
359
|
+
// Cross-device move: copy + delete
|
|
360
|
+
fs.copyFileSync(src, dest);
|
|
361
|
+
fs.unlinkSync(src);
|
|
362
|
+
} else {
|
|
363
|
+
throw err;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Create a lock file to prevent concurrent downloads
|
|
369
|
+
function acquireLock(lockPath) {
|
|
370
|
+
try {
|
|
371
|
+
fs.writeFileSync(lockPath, process.pid.toString(), { flag: "wx" });
|
|
372
|
+
return true;
|
|
373
|
+
} catch (err) {
|
|
374
|
+
if (err.code === "EEXIST") {
|
|
375
|
+
// Check if the process that created the lock is still running
|
|
376
|
+
try {
|
|
377
|
+
const pid = parseInt(fs.readFileSync(lockPath, "utf8"), 10);
|
|
378
|
+
try {
|
|
379
|
+
process.kill(pid, 0); // Check if process exists
|
|
380
|
+
return false; // Process is still running
|
|
381
|
+
} catch {
|
|
382
|
+
// Process is not running, remove stale lock
|
|
383
|
+
fs.unlinkSync(lockPath);
|
|
384
|
+
return acquireLock(lockPath);
|
|
385
|
+
}
|
|
386
|
+
} catch {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
throw err;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function releaseLock(lockPath) {
|
|
395
|
+
try {
|
|
396
|
+
fs.unlinkSync(lockPath);
|
|
397
|
+
} catch {
|
|
398
|
+
// Ignore errors
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function downloadAndExtract(cacheDir) {
|
|
403
|
+
const assetName = getAssetName();
|
|
404
|
+
const binaryName = getBinaryName();
|
|
405
|
+
const binaryPath = path.join(cacheDir, binaryName);
|
|
406
|
+
const lockPath = path.join(cacheDir, ".lock");
|
|
407
|
+
const tempId = crypto.randomBytes(8).toString("hex");
|
|
408
|
+
|
|
409
|
+
// Ensure cache directory exists
|
|
410
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
411
|
+
|
|
412
|
+
// Try to acquire lock
|
|
413
|
+
if (!acquireLock(lockPath)) {
|
|
414
|
+
// Wait for other process to complete
|
|
415
|
+
console.log("Another process is downloading, waiting...");
|
|
416
|
+
let attempts = 0;
|
|
417
|
+
while (!fs.existsSync(binaryPath) && attempts < 60) {
|
|
418
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
419
|
+
attempts++;
|
|
420
|
+
}
|
|
421
|
+
if (fs.existsSync(binaryPath)) {
|
|
422
|
+
return binaryPath;
|
|
423
|
+
}
|
|
424
|
+
throw new Error("Timeout waiting for download to complete");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
try {
|
|
428
|
+
// Double check after acquiring lock
|
|
429
|
+
if (fs.existsSync(binaryPath)) {
|
|
430
|
+
return binaryPath;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Get release for the specific version (with retry)
|
|
434
|
+
const version = getPackageVersion();
|
|
435
|
+
console.log(`Downloading ${PACKAGE_NAME} v${version}...`);
|
|
436
|
+
|
|
437
|
+
const release = await withRetry(() => getReleaseByTag(version));
|
|
438
|
+
const asset = release.assets.find((a) => a.name === assetName);
|
|
439
|
+
|
|
440
|
+
if (!asset) {
|
|
441
|
+
const availableAssets = release.assets.map((a) => a.name).join(", ");
|
|
442
|
+
throw new Error(
|
|
443
|
+
`No matching asset found: ${assetName}. Available: ${availableAssets}`
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Download to temporary file first
|
|
448
|
+
const tempArchive = path.join(cacheDir, `${tempId}-${assetName}`);
|
|
449
|
+
const tempExtractDir = path.join(cacheDir, `${tempId}-extract`);
|
|
450
|
+
|
|
451
|
+
const downloadOptions = {
|
|
452
|
+
headers: {
|
|
453
|
+
"User-Agent": PACKAGE_NAME,
|
|
454
|
+
Accept: "application/octet-stream",
|
|
455
|
+
...(process.env.GITHUB_TOKEN && {
|
|
456
|
+
Authorization: `token ${process.env.GITHUB_TOKEN}`,
|
|
457
|
+
}),
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
await withRetry(() =>
|
|
462
|
+
downloadToFile(asset.browser_download_url, tempArchive, downloadOptions)
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
// Extract to temporary directory
|
|
466
|
+
console.log("Extracting...");
|
|
467
|
+
fs.mkdirSync(tempExtractDir, { recursive: true });
|
|
468
|
+
|
|
469
|
+
if (assetName.endsWith(".zip")) {
|
|
470
|
+
await extractZip(tempArchive, tempExtractDir);
|
|
471
|
+
} else {
|
|
472
|
+
await extractTarGz(tempArchive, tempExtractDir);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Find the binary in the extracted directory
|
|
476
|
+
const extractedBinary = path.join(tempExtractDir, binaryName);
|
|
477
|
+
if (!fs.existsSync(extractedBinary)) {
|
|
478
|
+
throw new Error(
|
|
479
|
+
`Binary not found in archive. Expected: ${binaryName} in extracted contents.`
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Atomic move to final location (with cross-device fallback)
|
|
484
|
+
moveFile(extractedBinary, binaryPath);
|
|
485
|
+
|
|
486
|
+
// Make binary executable on Unix
|
|
487
|
+
if (process.platform !== "win32") {
|
|
488
|
+
fs.chmodSync(binaryPath, 0o755);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Clean up
|
|
492
|
+
fs.unlinkSync(tempArchive);
|
|
493
|
+
fs.rmSync(tempExtractDir, { recursive: true, force: true });
|
|
494
|
+
|
|
495
|
+
console.log(`Installed ${PACKAGE_NAME} to ${binaryPath}`);
|
|
496
|
+
return binaryPath;
|
|
497
|
+
} catch (error) {
|
|
498
|
+
console.error(`Failed to download ${PACKAGE_NAME}: ${error.message}`);
|
|
499
|
+
console.error("");
|
|
500
|
+
console.error("You can install manually:");
|
|
501
|
+
console.error(
|
|
502
|
+
" 1. Download from https://github.com/missdeer/ace-tool-rs/releases"
|
|
503
|
+
);
|
|
504
|
+
console.error(` 2. Place binary at: ${binaryPath}`);
|
|
505
|
+
console.error("");
|
|
506
|
+
console.error("Or install via cargo:");
|
|
507
|
+
console.error(" cargo install ace-tool-rs");
|
|
508
|
+
process.exit(1);
|
|
509
|
+
} finally {
|
|
510
|
+
releaseLock(lockPath);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async function run() {
|
|
515
|
+
const cacheDir = getCacheDir();
|
|
516
|
+
const binaryName = getBinaryName();
|
|
517
|
+
const binaryPath = path.join(cacheDir, binaryName);
|
|
518
|
+
|
|
519
|
+
// Check if binary exists in cache
|
|
520
|
+
if (!fs.existsSync(binaryPath)) {
|
|
521
|
+
await downloadAndExtract(cacheDir);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Final check that binary exists
|
|
525
|
+
if (!fs.existsSync(binaryPath)) {
|
|
526
|
+
console.error(`Binary not found at ${binaryPath}`);
|
|
527
|
+
process.exit(1);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Run the binary with all arguments
|
|
531
|
+
const args = process.argv.slice(2);
|
|
532
|
+
const child = spawn(binaryPath, args, {
|
|
533
|
+
stdio: "inherit",
|
|
534
|
+
env: process.env,
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
// Forward signals to child process
|
|
538
|
+
const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
539
|
+
signals.forEach((signal) => {
|
|
540
|
+
process.on(signal, () => {
|
|
541
|
+
if (!child.killed) {
|
|
542
|
+
child.kill(signal);
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
child.on("error", (error) => {
|
|
548
|
+
console.error(`Failed to start ${PACKAGE_NAME}: ${error.message}`);
|
|
549
|
+
process.exit(1);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
child.on("exit", (code, signal) => {
|
|
553
|
+
if (signal) {
|
|
554
|
+
process.exit(128 + (os.constants.signals[signal] || 0));
|
|
555
|
+
}
|
|
556
|
+
process.exit(code ?? 0);
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
run().catch((error) => {
|
|
561
|
+
console.error(error);
|
|
562
|
+
process.exit(1);
|
|
563
|
+
});
|