agnix 0.37.1 → 0.37.3
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/install.js +156 -25
- package/package.json +2 -2
package/install.js
CHANGED
|
@@ -5,10 +5,35 @@ const fs = require('fs');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { execSync } = require('child_process');
|
|
7
7
|
const os = require('os');
|
|
8
|
+
const crypto = require('crypto');
|
|
8
9
|
|
|
9
10
|
const GITHUB_REPO = 'agent-sh/agnix';
|
|
10
11
|
const VERSION = require('./package.json').version;
|
|
11
12
|
|
|
13
|
+
function removeIfExists(filePath) {
|
|
14
|
+
try {
|
|
15
|
+
if (fs.existsSync(filePath)) {
|
|
16
|
+
fs.unlinkSync(filePath);
|
|
17
|
+
}
|
|
18
|
+
} catch (_) {
|
|
19
|
+
// best-effort cleanup
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function restoreWrapperBackup(wrapperPath, wrapperBackup, installCompleted) {
|
|
24
|
+
if (!fs.existsSync(wrapperBackup)) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (installCompleted) {
|
|
29
|
+
removeIfExists(wrapperBackup);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
removeIfExists(wrapperPath);
|
|
34
|
+
fs.renameSync(wrapperBackup, wrapperPath);
|
|
35
|
+
}
|
|
36
|
+
|
|
12
37
|
/**
|
|
13
38
|
* Get platform-specific asset name and binary name.
|
|
14
39
|
*/
|
|
@@ -62,43 +87,60 @@ function getPlatformInfo() {
|
|
|
62
87
|
*/
|
|
63
88
|
function downloadFile(url, destPath) {
|
|
64
89
|
return new Promise((resolve, reject) => {
|
|
65
|
-
|
|
90
|
+
let file = null;
|
|
91
|
+
let settled = false;
|
|
92
|
+
|
|
93
|
+
const fail = (err) => {
|
|
94
|
+
if (settled) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
settled = true;
|
|
98
|
+
if (file) {
|
|
99
|
+
file.destroy();
|
|
100
|
+
}
|
|
101
|
+
removeIfExists(destPath);
|
|
102
|
+
reject(err);
|
|
103
|
+
};
|
|
66
104
|
|
|
67
105
|
const request = https.get(url, (response) => {
|
|
68
106
|
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
69
107
|
const redirectUrl = response.headers.location;
|
|
70
108
|
if (redirectUrl) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
downloadFile(
|
|
109
|
+
response.resume();
|
|
110
|
+
const nextUrl = new URL(redirectUrl, url).href;
|
|
111
|
+
downloadFile(nextUrl, destPath).then(resolve).catch(reject);
|
|
74
112
|
return;
|
|
75
113
|
}
|
|
76
114
|
}
|
|
77
115
|
|
|
78
116
|
if (response.statusCode !== 200) {
|
|
79
|
-
|
|
80
|
-
|
|
117
|
+
response.resume();
|
|
118
|
+
fail(new Error(`Download failed with status ${response.statusCode}`));
|
|
81
119
|
return;
|
|
82
120
|
}
|
|
83
121
|
|
|
122
|
+
file = fs.createWriteStream(destPath);
|
|
84
123
|
response.pipe(file);
|
|
124
|
+
response.on('error', fail);
|
|
85
125
|
|
|
86
126
|
file.on('finish', () => {
|
|
87
|
-
|
|
88
|
-
|
|
127
|
+
if (settled) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
settled = true;
|
|
131
|
+
file.close((err) => {
|
|
132
|
+
if (err) {
|
|
133
|
+
removeIfExists(destPath);
|
|
134
|
+
reject(err);
|
|
135
|
+
} else {
|
|
136
|
+
resolve();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
89
139
|
});
|
|
140
|
+
file.on('error', fail);
|
|
90
141
|
});
|
|
91
142
|
|
|
92
|
-
request.on('error',
|
|
93
|
-
file.close();
|
|
94
|
-
fs.unlinkSync(destPath);
|
|
95
|
-
reject(err);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
file.on('error', (err) => {
|
|
99
|
-
fs.unlinkSync(destPath);
|
|
100
|
-
reject(err);
|
|
101
|
-
});
|
|
143
|
+
request.on('error', fail);
|
|
102
144
|
});
|
|
103
145
|
}
|
|
104
146
|
|
|
@@ -120,11 +162,81 @@ function extractArchive(archivePath, destDir) {
|
|
|
120
162
|
}
|
|
121
163
|
}
|
|
122
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Compute the SHA-256 of a file as a lowercase hex string, streaming the file
|
|
167
|
+
* so large archives are not read fully into memory.
|
|
168
|
+
*/
|
|
169
|
+
function sha256File(filePath) {
|
|
170
|
+
return new Promise((resolve, reject) => {
|
|
171
|
+
const hash = crypto.createHash('sha256');
|
|
172
|
+
const stream = fs.createReadStream(filePath);
|
|
173
|
+
stream.on('error', reject);
|
|
174
|
+
stream.on('data', (chunk) => hash.update(chunk));
|
|
175
|
+
stream.on('end', () => resolve(hash.digest('hex')));
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function parseSha256Sidecar(contents, expectedFileName) {
|
|
180
|
+
const expectedBase = path.basename(expectedFileName);
|
|
181
|
+
for (const line of contents.split(/\r?\n/)) {
|
|
182
|
+
const trimmed = line.trim();
|
|
183
|
+
if (!trimmed) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const [rawHash, rawFileName] = trimmed.split(/\s+/, 2);
|
|
188
|
+
if (!rawFileName) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const normalizedFileName = rawFileName.replace(/^\*/, '').replace(/\\/g, '/');
|
|
193
|
+
const baseName = path.posix.basename(normalizedFileName);
|
|
194
|
+
if (baseName !== expectedBase) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const expected = rawHash.toLowerCase();
|
|
199
|
+
if (!/^[0-9a-f]{64}$/.test(expected)) {
|
|
200
|
+
throw new Error(`Invalid checksum file for ${expectedBase}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return expected;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
throw new Error(`Checksum file does not contain an entry for ${expectedBase}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Download the `.sha256` sidecar for an archive and verify the archive against
|
|
211
|
+
* it before extraction, so a tampered download is rejected (mirrors
|
|
212
|
+
* scripts/download.sh). Throws on a missing/malformed sidecar or hash mismatch.
|
|
213
|
+
*/
|
|
214
|
+
async function verifyChecksum(archivePath, checksumUrl, deps = { downloadFile }) {
|
|
215
|
+
const checksumPath = `${archivePath}.sha256`;
|
|
216
|
+
try {
|
|
217
|
+
await deps.downloadFile(checksumUrl, checksumPath);
|
|
218
|
+
const raw = fs.readFileSync(checksumPath, 'utf8').trim();
|
|
219
|
+
const expected = parseSha256Sidecar(raw, path.basename(archivePath));
|
|
220
|
+
const actual = (await sha256File(archivePath)).toLowerCase();
|
|
221
|
+
if (actual !== expected) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
`Checksum mismatch for ${path.basename(archivePath)}: expected ${expected}, got ${actual}`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
console.log('Checksum verified.');
|
|
227
|
+
} finally {
|
|
228
|
+
removeIfExists(checksumPath);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
123
232
|
async function main() {
|
|
124
233
|
const platformInfo = getPlatformInfo();
|
|
125
234
|
const binDir = path.join(__dirname, 'bin');
|
|
126
235
|
const binaryPath = path.join(binDir, platformInfo.binary);
|
|
127
236
|
const extractedPath = path.join(binDir, platformInfo.extractedName);
|
|
237
|
+
const wrapperPath = path.join(binDir, 'agnix');
|
|
238
|
+
const wrapperBackup = path.join(binDir, 'agnix.backup');
|
|
239
|
+
let installCompleted = false;
|
|
128
240
|
|
|
129
241
|
// Skip if binary already exists
|
|
130
242
|
if (fs.existsSync(binaryPath)) {
|
|
@@ -144,19 +256,15 @@ async function main() {
|
|
|
144
256
|
|
|
145
257
|
try {
|
|
146
258
|
// Save wrapper script if it exists (npm places it during install)
|
|
147
|
-
const wrapperPath = path.join(binDir, 'agnix');
|
|
148
|
-
const wrapperBackup = path.join(binDir, 'agnix.backup');
|
|
149
259
|
if (fs.existsSync(wrapperPath) && wrapperPath !== binaryPath) {
|
|
150
260
|
fs.copyFileSync(wrapperPath, wrapperBackup);
|
|
151
261
|
}
|
|
152
262
|
|
|
153
263
|
await downloadFile(downloadUrl, archivePath);
|
|
264
|
+
await verifyChecksum(archivePath, `${downloadUrl}.sha256`);
|
|
154
265
|
console.log('Extracting...');
|
|
155
266
|
extractArchive(archivePath, binDir);
|
|
156
267
|
|
|
157
|
-
// Clean up archive
|
|
158
|
-
fs.unlinkSync(archivePath);
|
|
159
|
-
|
|
160
268
|
// Rename extracted binary to avoid conflict with wrapper script
|
|
161
269
|
if (fs.existsSync(extractedPath) && extractedPath !== binaryPath) {
|
|
162
270
|
fs.renameSync(extractedPath, binaryPath);
|
|
@@ -177,14 +285,37 @@ async function main() {
|
|
|
177
285
|
throw new Error('Binary not found after extraction');
|
|
178
286
|
}
|
|
179
287
|
|
|
288
|
+
installCompleted = true;
|
|
180
289
|
console.log('agnix installed successfully');
|
|
181
290
|
} catch (error) {
|
|
182
291
|
console.error(`Failed to install agnix: ${error.message}`);
|
|
183
292
|
console.error('');
|
|
184
293
|
console.error('You can install manually:');
|
|
185
294
|
console.error(' cargo install agnix-cli');
|
|
186
|
-
process.
|
|
295
|
+
process.exitCode = 1;
|
|
296
|
+
} finally {
|
|
297
|
+
// Best-effort cleanup so a failed install leaves no stale archive or backup.
|
|
298
|
+
removeIfExists(archivePath);
|
|
299
|
+
try {
|
|
300
|
+
restoreWrapperBackup(wrapperPath, wrapperBackup, installCompleted);
|
|
301
|
+
} catch (_) {
|
|
302
|
+
// ignore
|
|
303
|
+
}
|
|
187
304
|
}
|
|
188
305
|
}
|
|
189
306
|
|
|
190
|
-
main
|
|
307
|
+
if (require.main === module) {
|
|
308
|
+
main();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
module.exports = {
|
|
312
|
+
downloadFile,
|
|
313
|
+
extractArchive,
|
|
314
|
+
getPlatformInfo,
|
|
315
|
+
main,
|
|
316
|
+
parseSha256Sidecar,
|
|
317
|
+
removeIfExists,
|
|
318
|
+
restoreWrapperBackup,
|
|
319
|
+
sha256File,
|
|
320
|
+
verifyChecksum,
|
|
321
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agnix",
|
|
3
|
-
"version": "0.37.
|
|
3
|
+
"version": "0.37.3",
|
|
4
4
|
"description": "Linter for AI agent configurations. Validates SKILL.md, CLAUDE.md, hooks, MCP, and more.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
],
|
|
39
39
|
"scripts": {
|
|
40
40
|
"postinstall": "node install.js",
|
|
41
|
-
"test": "node test/test.js"
|
|
41
|
+
"test": "node test/install-helpers.test.js && node test/test.js"
|
|
42
42
|
},
|
|
43
43
|
"engines": {
|
|
44
44
|
"node": ">=16.0.0"
|