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.
Files changed (2) hide show
  1. package/package.json +42 -0
  2. 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
+ });