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