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