@vera-ai/cli 1.4.2 → 2.0.0
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 +4 -4
- package/bin/vera.js +280 -166
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Code search for AI agents. Vera indexes your codebase using tree-sitter parsing and hybrid search (BM25 + vector similarity + optional cross-encoder reranking), then returns ranked code snippets as Markdown codeblocks by default, or JSON with `--json`.
|
|
4
4
|
|
|
5
|
-
This package downloads and wraps the native Vera binary for your platform.
|
|
5
|
+
This package downloads and wraps the native Vera binary for your platform. It downloads the exact package version, checks the release archive size and SHA-256, and stores a completed binary cache. Later runs use that cache without network access. Older caches are downloaded and verified once before they can be reused offline. On Linux without glibc (such as Alpine or NixOS), the wrapper selects the musl binary. Other native targets use their platform system libraries; optional ONNX backends have additional runtime requirements. Set `VERA_TARGET` to override target detection (e.g., `VERA_TARGET=x86_64-unknown-linux-musl npm install -g @vera-ai/cli`).
|
|
6
6
|
|
|
7
|
-
The default local embedding model
|
|
7
|
+
The default local embedding model, `minishlab/potion-code-16M-v2`, runs on CPU and works offline after its first download. API mode is also available; Qwen/OpenRouter is recommended among API presets (paid usage). The [benchmark report](https://github.com/VeraTools/Vera/blob/master/docs/benchmarks.md) separates the full 1,251-task suite from smaller screening subsets, and the [agent benchmark](https://github.com/VeraTools/Vera/blob/master/docs/benchmarks-history.md#agent-level-benchmark) records context measurements and their sample limits.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
@@ -19,9 +19,9 @@ vera setup --potion-code --index .
|
|
|
19
19
|
vera search "authentication logic"
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
`vera setup`
|
|
22
|
+
`vera setup` runs the full wizard: configure a backend, optionally install agent skills, and optionally index. Local CPU is first and selected by default; API mode is second. Indexing defaults to Yes for local backends and No for API mode. `vera setup --api` uses the shorter API configuration flow, with Qwen/OpenRouter first and recommended (one shared key). See the [models guide](https://github.com/VeraTools/Vera/blob/master/docs/models.md#api-mode) for other providers and non-interactive setup.
|
|
23
23
|
|
|
24
|
-
The preferred agent integration is the CLI plus the Vera skill: `vera agent install` installs it for supported coding agents and can add a short usage snippet to your project's `AGENTS.md`, `CLAUDE.md`, `COPILOT.md`, or editor rules file. Vera also ships an optional MCP server (`vera mcp`); see the [MCP guide](https://github.com/VeraTools/Vera/blob/master/docs/mcp.md) if your client is MCP-first.
|
|
24
|
+
The preferred agent integration is the CLI plus the Vera skill: `vera agent install` installs it for supported coding agents and can add a short usage snippet to your project's `AGENTS.md`, `CLAUDE.md`, `COPILOT.md`, or editor rules file. The client selector uses Space to toggle and Enter to continue. Empty selection changes nothing; unselected installations remain intact. Vera also ships an optional MCP server (`vera mcp`); see the [MCP guide](https://github.com/VeraTools/Vera/blob/master/docs/mcp.md) if your client is MCP-first.
|
|
25
25
|
|
|
26
26
|
## Common Tasks
|
|
27
27
|
|
package/bin/vera.js
CHANGED
|
@@ -9,12 +9,21 @@ const http = require("node:http");
|
|
|
9
9
|
const https = require("node:https");
|
|
10
10
|
const os = require("node:os");
|
|
11
11
|
const path = require("node:path");
|
|
12
|
-
const { spawnSync } = require("node:child_process");
|
|
12
|
+
const { spawn, spawnSync } = require("node:child_process");
|
|
13
|
+
const { pipeline } = require("node:stream/promises");
|
|
14
|
+
const { Transform } = require("node:stream");
|
|
15
|
+
const zlib = require("node:zlib");
|
|
13
16
|
|
|
14
17
|
const { version: packageVersion } = require("../package.json");
|
|
15
18
|
|
|
16
19
|
const DEFAULT_REPO = "VeraTools/Vera";
|
|
17
20
|
const MAX_REDIRECTS = 5;
|
|
21
|
+
const REQUEST_TIMEOUT = 30_000;
|
|
22
|
+
const DOWNLOAD_TIMEOUT = 180_000;
|
|
23
|
+
const MAX_MANIFEST_BYTES = 1024 * 1024;
|
|
24
|
+
const MAX_ARCHIVE_BYTES = 512 * 1024 * 1024;
|
|
25
|
+
const MAX_BINARY_BYTES = 512 * 1024 * 1024;
|
|
26
|
+
const VERSION_PATTERN = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[A-Za-z0-9.-]+)?$/;
|
|
18
27
|
|
|
19
28
|
function parseArgs(argv) {
|
|
20
29
|
if (argv.length === 0) {
|
|
@@ -26,20 +35,16 @@ function parseArgs(argv) {
|
|
|
26
35
|
|
|
27
36
|
function detectMusl() {
|
|
28
37
|
if (process.platform !== "linux") return false;
|
|
38
|
+
const result = spawnSync("ldd", ["--version"], {
|
|
39
|
+
stdio: ["pipe", "pipe", "pipe"], timeout: 5000,
|
|
40
|
+
});
|
|
41
|
+
const output = (result.stdout || "").toString() + (result.stderr || "").toString();
|
|
42
|
+
if (/musl/i.test(output)) return true;
|
|
43
|
+
if (!result.error && /glibc|GNU libc/i.test(output)) return false;
|
|
29
44
|
try {
|
|
30
|
-
|
|
31
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
32
|
-
});
|
|
33
|
-
const output = (result.stdout || "").toString() + (result.stderr || "").toString();
|
|
34
|
-
return /musl/i.test(output);
|
|
45
|
+
return fs.readdirSync("/lib").some((entry) => entry.startsWith("ld-musl-"));
|
|
35
46
|
} catch {
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
const entries = fs.readdirSync("/lib");
|
|
39
|
-
return entries.some((e) => e.startsWith("ld-musl-"));
|
|
40
|
-
} catch {
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
47
|
+
return false;
|
|
43
48
|
}
|
|
44
49
|
}
|
|
45
50
|
|
|
@@ -78,10 +83,6 @@ function manifestUrl(version) {
|
|
|
78
83
|
return `${defaultReleaseBaseUrl()}/releases/download/v${version}/release-manifest.json`;
|
|
79
84
|
}
|
|
80
85
|
|
|
81
|
-
function latestManifestUrl() {
|
|
82
|
-
return `${defaultReleaseBaseUrl()}/releases/latest/download/release-manifest.json`;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
86
|
function defaultVeraHome() {
|
|
86
87
|
return process.env.VERA_HOME || path.join(os.homedir(), ".vera");
|
|
87
88
|
}
|
|
@@ -102,7 +103,8 @@ function currentInstallMethod() {
|
|
|
102
103
|
async function readInstallMetadata() {
|
|
103
104
|
try {
|
|
104
105
|
const raw = await fsp.readFile(installMetadataPath(), "utf8");
|
|
105
|
-
|
|
106
|
+
const value = JSON.parse(raw);
|
|
107
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
106
108
|
} catch {
|
|
107
109
|
return {};
|
|
108
110
|
}
|
|
@@ -117,6 +119,8 @@ async function writeInstallMetadata({ installMethod, version, binaryPath, target
|
|
|
117
119
|
version: version ?? current.version ?? null,
|
|
118
120
|
binary_path: binaryPath ?? current.binary_path ?? null,
|
|
119
121
|
target: target ?? current.target ?? null,
|
|
122
|
+
manifest_url: process.env.VERA_MANIFEST_URL || null,
|
|
123
|
+
requested_version: packageVersion,
|
|
120
124
|
};
|
|
121
125
|
const tmpPath = `${metadataPath}.tmp.${process.pid}`;
|
|
122
126
|
await fsp.writeFile(tmpPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
@@ -164,71 +168,70 @@ function shimName() {
|
|
|
164
168
|
return process.platform === "win32" ? "vera.cmd" : "vera";
|
|
165
169
|
}
|
|
166
170
|
|
|
167
|
-
|
|
168
|
-
const
|
|
171
|
+
function openResponse(url, timeout = REQUEST_TIMEOUT, redirects = 0, deadline = Date.now() + timeout) {
|
|
172
|
+
const parsed = new URL(url);
|
|
173
|
+
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
174
|
+
throw new Error("release downloads require an HTTP or HTTPS URL");
|
|
175
|
+
}
|
|
169
176
|
return new Promise((resolve, reject) => {
|
|
170
|
-
const
|
|
177
|
+
const remaining = deadline - Date.now();
|
|
178
|
+
if (remaining <= 0) return reject(new Error("release download timed out"));
|
|
179
|
+
const client = parsed.protocol === "https:" ? https : http;
|
|
180
|
+
const request = client.get(parsed, (response) => {
|
|
171
181
|
const status = response.statusCode || 0;
|
|
172
182
|
if ([301, 302, 303, 307, 308].includes(status) && response.headers.location) {
|
|
173
183
|
if (redirects >= MAX_REDIRECTS) {
|
|
174
|
-
reject(new Error(
|
|
175
|
-
|
|
184
|
+
reject(new Error("too many release redirects"));
|
|
185
|
+
} else {
|
|
186
|
+
resolve(Promise.resolve().then(() => openResponse(new URL(response.headers.location, parsed).toString(), timeout, redirects + 1, deadline)));
|
|
176
187
|
}
|
|
177
|
-
|
|
178
|
-
const nextUrl = new URL(response.headers.location, url).toString();
|
|
179
|
-
resolve(fetchText(nextUrl, redirects + 1));
|
|
188
|
+
response.destroy();
|
|
180
189
|
return;
|
|
181
190
|
}
|
|
182
|
-
|
|
183
191
|
if (status < 200 || status >= 300) {
|
|
192
|
+
response.destroy();
|
|
184
193
|
reject(new Error(`request failed for ${url}: ${status}`));
|
|
185
194
|
return;
|
|
186
195
|
}
|
|
187
|
-
|
|
188
|
-
let body = "";
|
|
189
|
-
response.setEncoding("utf8");
|
|
190
|
-
response.on("data", (chunk) => {
|
|
191
|
-
body += chunk;
|
|
192
|
-
});
|
|
193
|
-
response.on("end", () => resolve(body));
|
|
196
|
+
resolve(response);
|
|
194
197
|
});
|
|
195
|
-
|
|
198
|
+
const timer = setTimeout(() => request.destroy(new Error("release download timed out")), remaining);
|
|
199
|
+
request.on("close", () => clearTimeout(timer));
|
|
196
200
|
request.on("error", reject);
|
|
197
201
|
});
|
|
198
202
|
}
|
|
199
203
|
|
|
200
|
-
async function
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
response.resume();
|
|
215
|
-
resolve(downloadFile(nextUrl, destination, redirects + 1));
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (status < 200 || status >= 300) {
|
|
220
|
-
reject(new Error(`download failed for ${url}: ${status}`));
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
204
|
+
async function fetchText(url, timeout = REQUEST_TIMEOUT) {
|
|
205
|
+
const response = await openResponse(url, timeout);
|
|
206
|
+
const chunks = [];
|
|
207
|
+
let size = 0;
|
|
208
|
+
for await (const chunk of response) {
|
|
209
|
+
size += chunk.length;
|
|
210
|
+
if (size > MAX_MANIFEST_BYTES) {
|
|
211
|
+
response.destroy();
|
|
212
|
+
throw new Error("release manifest exceeds its size limit");
|
|
213
|
+
}
|
|
214
|
+
chunks.push(chunk);
|
|
215
|
+
}
|
|
216
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
217
|
+
}
|
|
223
218
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
219
|
+
async function downloadFile(url, destination, expectedSize, timeout = DOWNLOAD_TIMEOUT) {
|
|
220
|
+
let size = 0;
|
|
221
|
+
try {
|
|
222
|
+
const response = await openResponse(url, timeout);
|
|
223
|
+
const bound = new Transform({
|
|
224
|
+
transform(chunk, encoding, callback) {
|
|
225
|
+
size += chunk.length;
|
|
226
|
+
callback(size > expectedSize ? new Error("release archive exceeds its size limit") : null, chunk);
|
|
227
|
+
},
|
|
228
228
|
});
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
})
|
|
229
|
+
await pipeline(response, bound, fs.createWriteStream(destination, { flags: "wx" }));
|
|
230
|
+
if (size !== expectedSize) throw new Error("release archive size mismatch");
|
|
231
|
+
} catch (error) {
|
|
232
|
+
await fsp.rm(destination, { force: true });
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
232
235
|
}
|
|
233
236
|
|
|
234
237
|
async function sha256(filePath) {
|
|
@@ -242,50 +245,147 @@ async function sha256(filePath) {
|
|
|
242
245
|
});
|
|
243
246
|
}
|
|
244
247
|
|
|
245
|
-
function
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
throw result.error;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (result.status !== 0) {
|
|
252
|
-
throw new Error(`${command} exited with status ${result.status}`);
|
|
253
|
-
}
|
|
248
|
+
function safeMember(name) {
|
|
249
|
+
return name && !name.startsWith("/") && !/[\\:\x00]/.test(name) &&
|
|
250
|
+
name.replace(/\/$/, "").split("/").every((part) => part && part !== "." && part !== "..");
|
|
254
251
|
}
|
|
255
252
|
|
|
256
|
-
async function extractArchive(archivePath, destination) {
|
|
257
|
-
|
|
253
|
+
async function extractArchive(archivePath, destination, target) {
|
|
254
|
+
const expected = `vera-${target}/${binaryName()}`;
|
|
258
255
|
if (archivePath.endsWith(".zip")) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
256
|
+
// .NET reads only the selected entry; archive paths never become filesystem paths.
|
|
257
|
+
const script = `
|
|
258
|
+
$ErrorActionPreference = 'Stop'
|
|
259
|
+
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
|
260
|
+
$archive = [System.IO.Compression.ZipFile]::OpenRead($env:VERA_INSTALL_ARCHIVE)
|
|
261
|
+
try {
|
|
262
|
+
$matches = @()
|
|
263
|
+
$total = 0
|
|
264
|
+
$names = @{}
|
|
265
|
+
if ($archive.Entries.Count -gt 1024) { throw 'Too many release archive members' }
|
|
266
|
+
foreach ($entry in $archive.Entries) {
|
|
267
|
+
$name = $entry.FullName
|
|
268
|
+
$type = ($entry.ExternalAttributes -shr 16) -band 61440
|
|
269
|
+
$parts = $name.TrimEnd('/').Split('/')
|
|
270
|
+
if ($names.ContainsKey($name) -or !$name -or $name.StartsWith('/') -or $name.Contains('\\') -or $name.Contains(':') -or
|
|
271
|
+
($parts | Where-Object { $_ -eq '..' -or $_ -eq '.' -or $_ -eq '' }) -or
|
|
272
|
+
($type -ne 0 -and $type -ne 32768 -and $type -ne 16384)) { throw 'Unsafe release archive member' }
|
|
273
|
+
$names[$name] = $true
|
|
274
|
+
$total += $entry.Length
|
|
275
|
+
if ($total -gt ${MAX_BINARY_BYTES}) { throw 'Release archive exceeds its size limit' }
|
|
276
|
+
if ($name -ceq $env:VERA_INSTALL_MEMBER) {
|
|
277
|
+
if ($type -eq 16384) { throw 'Release binary must be a regular file' }
|
|
278
|
+
$matches += $entry
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if ($matches.Count -ne 1 -or $matches[0].Length -le 0) { throw 'Release archive must contain exactly one Vera binary' }
|
|
282
|
+
$source = $matches[0].Open()
|
|
283
|
+
$output = [System.IO.File]::Open($env:VERA_INSTALL_BINARY, [System.IO.FileMode]::CreateNew)
|
|
284
|
+
try {
|
|
285
|
+
$buffer = New-Object byte[] 65536
|
|
286
|
+
$copied = 0
|
|
287
|
+
while (($count = $source.Read($buffer, 0, $buffer.Length)) -gt 0) {
|
|
288
|
+
$copied += $count
|
|
289
|
+
if ($copied -gt $matches[0].Length -or $copied -gt ${MAX_BINARY_BYTES}) { throw 'Release binary exceeds its size limit' }
|
|
290
|
+
$output.Write($buffer, 0, $count)
|
|
291
|
+
}
|
|
292
|
+
} finally { $source.Dispose(); $output.Dispose() }
|
|
293
|
+
if ((Get-Item -LiteralPath $env:VERA_INSTALL_BINARY).Length -ne $matches[0].Length) { throw 'Incomplete release binary' }
|
|
294
|
+
} finally { $archive.Dispose() }
|
|
295
|
+
`;
|
|
296
|
+
const result = spawnSync("powershell", ["-NoProfile", "-NonInteractive", "-Command", script], {
|
|
297
|
+
stdio: "inherit", timeout: 30_000,
|
|
298
|
+
env: { ...process.env, VERA_INSTALL_ARCHIVE: archivePath, VERA_INSTALL_BINARY: destination, VERA_INSTALL_MEMBER: expected },
|
|
299
|
+
});
|
|
300
|
+
if (result.error) throw result.error;
|
|
301
|
+
if (result.status !== 0) throw new Error("release ZIP extraction failed");
|
|
269
302
|
return;
|
|
270
303
|
}
|
|
271
304
|
|
|
272
|
-
|
|
305
|
+
const output = await fsp.open(destination, "wx");
|
|
306
|
+
const input = fs.createReadStream(archivePath);
|
|
307
|
+
const gunzip = zlib.createGunzip();
|
|
308
|
+
input.on("error", (error) => gunzip.destroy(error));
|
|
309
|
+
input.pipe(gunzip);
|
|
310
|
+
let buffer = Buffer.alloc(0);
|
|
311
|
+
let remaining = 0;
|
|
312
|
+
let padding = 0;
|
|
313
|
+
let selected = false;
|
|
314
|
+
let matches = 0;
|
|
315
|
+
let expanded = 0;
|
|
316
|
+
let members = 0;
|
|
317
|
+
let ended = false;
|
|
318
|
+
const names = new Set();
|
|
319
|
+
try {
|
|
320
|
+
for await (const chunk of gunzip) {
|
|
321
|
+
expanded += chunk.length;
|
|
322
|
+
if (expanded > MAX_BINARY_BYTES + 1024 * 1024) throw new Error("release archive exceeds its size limit");
|
|
323
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
324
|
+
while (buffer.length) {
|
|
325
|
+
if (remaining || padding) {
|
|
326
|
+
const count = Math.min(buffer.length, remaining || padding);
|
|
327
|
+
if (remaining) {
|
|
328
|
+
if (selected) await output.writeFile(buffer.subarray(0, count));
|
|
329
|
+
remaining -= count;
|
|
330
|
+
} else padding -= count;
|
|
331
|
+
buffer = buffer.subarray(count);
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (buffer.length < 512) break;
|
|
335
|
+
const header = buffer.subarray(0, 512);
|
|
336
|
+
buffer = buffer.subarray(512);
|
|
337
|
+
if (header.every((byte) => byte === 0)) { ended = true; continue; }
|
|
338
|
+
if (ended || ++members > 1024) throw new Error("invalid release tar archive");
|
|
339
|
+
const field = (start, length) => header.subarray(start, start + length).toString("utf8").split("\0")[0];
|
|
340
|
+
const octal = (start, length) => {
|
|
341
|
+
const value = field(start, length).trim();
|
|
342
|
+
if (!/^[0-7]+$/.test(value)) throw new Error("invalid release tar header");
|
|
343
|
+
return parseInt(value, 8);
|
|
344
|
+
};
|
|
345
|
+
const checksum = octal(148, 8);
|
|
346
|
+
const actual = header.reduce((sum, byte, index) => sum + (index >= 148 && index < 156 ? 32 : byte), 0);
|
|
347
|
+
if (actual !== checksum) throw new Error("invalid release tar checksum");
|
|
348
|
+
const prefix = field(345, 155);
|
|
349
|
+
const name = (prefix ? `${prefix}/` : "") + field(0, 100);
|
|
350
|
+
const type = String.fromCharCode(header[156]);
|
|
351
|
+
if (names.has(name) || !safeMember(name) || !["0", "\0", "5"].includes(type)) throw new Error("unsafe release archive member");
|
|
352
|
+
names.add(name);
|
|
353
|
+
remaining = octal(124, 12);
|
|
354
|
+
if (remaining > MAX_BINARY_BYTES || (type === "5" && remaining)) throw new Error("invalid release tar size");
|
|
355
|
+
padding = (512 - remaining % 512) % 512;
|
|
356
|
+
selected = name === expected && type !== "5";
|
|
357
|
+
if (selected && (++matches !== 1 || remaining === 0)) throw new Error("release archive must contain exactly one Vera binary");
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (remaining || padding || buffer.some((byte) => byte !== 0) || !ended || matches !== 1) {
|
|
361
|
+
throw new Error("incomplete release tar archive or missing Vera binary");
|
|
362
|
+
}
|
|
363
|
+
} finally {
|
|
364
|
+
input.destroy();
|
|
365
|
+
gunzip.destroy();
|
|
366
|
+
await output.close();
|
|
367
|
+
}
|
|
273
368
|
}
|
|
274
369
|
|
|
275
370
|
async function createShim(binaryPath) {
|
|
276
371
|
const binDir = pickUserBinDir();
|
|
277
372
|
await fsp.mkdir(binDir, { recursive: true });
|
|
278
373
|
const shimPath = path.join(binDir, shimName());
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
374
|
+
if (path.join(await fsp.realpath(binDir), shimName()) === await fsp.realpath(binaryPath)) return shimPath;
|
|
375
|
+
const staging = await fsp.mkdtemp(path.join(binDir, ".vera-shim-"));
|
|
376
|
+
const stagedShim = path.join(staging, shimName());
|
|
377
|
+
try {
|
|
378
|
+
if (process.platform === "win32") {
|
|
379
|
+
const contents = `@echo off\r\nsetlocal DisableDelayedExpansion\r\n"${binaryPath.replace(/%/g, "%%")}" %*\r\n`;
|
|
380
|
+
await fsp.writeFile(stagedShim, contents, "utf8");
|
|
381
|
+
} else {
|
|
382
|
+
const quoted = "'" + binaryPath.replace(/'/g, "'\"'\"'") + "'";
|
|
383
|
+
await fsp.writeFile(stagedShim, `#!/bin/sh\nexec ${quoted} "$@"\n`, { mode: 0o755 });
|
|
384
|
+
}
|
|
385
|
+
await fsp.rename(stagedShim, shimPath);
|
|
386
|
+
} finally {
|
|
387
|
+
await fsp.rm(staging, { recursive: true, force: true });
|
|
287
388
|
}
|
|
288
|
-
|
|
289
389
|
return shimPath;
|
|
290
390
|
}
|
|
291
391
|
|
|
@@ -294,91 +394,101 @@ function isOnPath(dirPath) {
|
|
|
294
394
|
}
|
|
295
395
|
|
|
296
396
|
async function loadManifest() {
|
|
297
|
-
const
|
|
397
|
+
const manifest = JSON.parse(await fetchText(manifestUrl(packageVersion)));
|
|
398
|
+
if (!manifest || typeof manifest.version !== "string" || !VERSION_PATTERN.test(manifest.version)) {
|
|
399
|
+
throw new Error("invalid release version");
|
|
400
|
+
}
|
|
401
|
+
if (!process.env.VERA_MANIFEST_URL && manifest.version !== packageVersion) {
|
|
402
|
+
throw new Error(`requested Vera ${packageVersion}, manifest contains ${manifest.version}`);
|
|
403
|
+
}
|
|
404
|
+
return manifest;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async function cachedBinary(target) {
|
|
408
|
+
let version = packageVersion;
|
|
409
|
+
if (process.env.VERA_MANIFEST_URL) {
|
|
410
|
+
const metadata = await readInstallMetadata();
|
|
411
|
+
if (metadata.manifest_url !== process.env.VERA_MANIFEST_URL || metadata.requested_version !== packageVersion) return null;
|
|
412
|
+
version = metadata.version;
|
|
413
|
+
if (typeof version !== "string" || !VERSION_PATTERN.test(version)) return null;
|
|
414
|
+
}
|
|
415
|
+
const binaryPath = path.join(defaultVeraHome(), "bin", version, target, binaryName());
|
|
298
416
|
try {
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
417
|
+
const receipt = JSON.parse(await fsp.readFile(`${binaryPath}.receipt.json`, "utf8"));
|
|
418
|
+
const stat = await fsp.lstat(binaryPath);
|
|
419
|
+
if (receipt && stat.isFile() && stat.size > 0 && receipt.version === version && receipt.target === target &&
|
|
420
|
+
receipt.size === stat.size && receipt.sha256 === await sha256(binaryPath)) {
|
|
421
|
+
return { binaryPath, version };
|
|
304
422
|
}
|
|
423
|
+
} catch (error) {
|
|
424
|
+
if (!["ENOENT", "ENOTDIR"].includes(error.code) && !(error instanceof SyntaxError)) throw error;
|
|
425
|
+
}
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
305
428
|
|
|
306
|
-
|
|
307
|
-
|
|
429
|
+
async function finishInstall(binaryPath, version, target, announce = false) {
|
|
430
|
+
const shimPath = await createShim(binaryPath);
|
|
431
|
+
if (announce && !isOnPath(path.dirname(shimPath))) {
|
|
432
|
+
console.error(`Added Vera to ${path.dirname(shimPath)}. Add that directory to PATH to run \`vera\` directly.`);
|
|
308
433
|
}
|
|
434
|
+
await writeInstallMetadata({ installMethod: currentInstallMethod(), version, binaryPath, target });
|
|
435
|
+
return { binaryPath, version };
|
|
309
436
|
}
|
|
310
437
|
|
|
311
438
|
async function ensureBinaryInstalled() {
|
|
312
|
-
const manifest = await loadManifest();
|
|
313
439
|
const target = resolveTarget(process.platform, process.arch);
|
|
440
|
+
if (!/^[A-Za-z0-9_-]+$/.test(target)) throw new Error("invalid release target");
|
|
441
|
+
const cached = await cachedBinary(target);
|
|
442
|
+
if (cached) return finishInstall(cached.binaryPath, cached.version, target);
|
|
443
|
+
const manifest = await loadManifest();
|
|
314
444
|
const asset = manifest.assets && manifest.assets[target];
|
|
315
|
-
if (!asset) {
|
|
316
|
-
|
|
445
|
+
if (!asset) throw new Error(`no release asset for target ${target}`);
|
|
446
|
+
const extension = target.endsWith("windows-msvc") ? ".zip" : ".tar.gz";
|
|
447
|
+
if (asset.archive !== `vera-${target}${extension}` || !Number.isSafeInteger(asset.size) ||
|
|
448
|
+
asset.size <= 0 || asset.size > MAX_ARCHIVE_BYTES || !/^[0-9a-f]{64}$/.test(asset.sha256)) {
|
|
449
|
+
throw new Error("invalid release archive metadata");
|
|
317
450
|
}
|
|
318
|
-
|
|
319
451
|
const version = manifest.version;
|
|
320
452
|
const installDir = path.join(defaultVeraHome(), "bin", version, target);
|
|
321
453
|
const binaryPath = path.join(installDir, binaryName());
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (stat.size > 1_000_000) {
|
|
325
|
-
await createShim(binaryPath);
|
|
326
|
-
await writeInstallMetadata({
|
|
327
|
-
installMethod: currentInstallMethod(),
|
|
328
|
-
version,
|
|
329
|
-
binaryPath,
|
|
330
|
-
target,
|
|
331
|
-
});
|
|
332
|
-
return { binaryPath, version };
|
|
333
|
-
}
|
|
334
|
-
// Stale or broken file (e.g. leftover shim from interrupted install).
|
|
335
|
-
await fsp.rm(binaryPath);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const tempRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "vera-install-"));
|
|
454
|
+
await fsp.mkdir(installDir, { recursive: true });
|
|
455
|
+
const tempRoot = await fsp.mkdtemp(path.join(installDir, ".install-"));
|
|
339
456
|
try {
|
|
340
457
|
const archivePath = path.join(tempRoot, asset.archive);
|
|
341
|
-
const
|
|
342
|
-
|
|
458
|
+
const stagedBinary = path.join(tempRoot, binaryName());
|
|
343
459
|
console.error(`Downloading Vera ${version} for ${target}...`);
|
|
344
|
-
await downloadFile(asset.download_url, archivePath);
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
if (
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
await
|
|
352
|
-
|
|
353
|
-
await fsp.
|
|
354
|
-
await fsp.copyFile(extractedBinary, binaryPath);
|
|
355
|
-
if (process.platform !== "win32") {
|
|
356
|
-
await fsp.chmod(binaryPath, 0o755);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
const shimPath = await createShim(binaryPath);
|
|
360
|
-
if (!isOnPath(path.dirname(shimPath))) {
|
|
361
|
-
console.error(`Added Vera to ${path.dirname(shimPath)}. Add that directory to PATH to run \`vera\` directly.`);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
await writeInstallMetadata({
|
|
365
|
-
installMethod: currentInstallMethod(),
|
|
366
|
-
version,
|
|
367
|
-
binaryPath,
|
|
368
|
-
target,
|
|
369
|
-
});
|
|
370
|
-
return { binaryPath, version };
|
|
460
|
+
await downloadFile(asset.download_url, archivePath, asset.size);
|
|
461
|
+
if (await sha256(archivePath) !== asset.sha256) throw new Error(`checksum mismatch for ${asset.archive}`);
|
|
462
|
+
await extractArchive(archivePath, stagedBinary, target);
|
|
463
|
+
if (process.platform !== "win32") await fsp.chmod(stagedBinary, 0o755);
|
|
464
|
+
const stat = await fsp.stat(stagedBinary);
|
|
465
|
+
if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_BINARY_BYTES) throw new Error("invalid release binary");
|
|
466
|
+
const receiptPath = path.join(tempRoot, "receipt.json");
|
|
467
|
+
await fsp.writeFile(receiptPath, JSON.stringify({ version, target, size: stat.size, sha256: await sha256(stagedBinary) }));
|
|
468
|
+
await fsp.rename(stagedBinary, binaryPath);
|
|
469
|
+
await fsp.rename(receiptPath, `${binaryPath}.receipt.json`);
|
|
371
470
|
} finally {
|
|
372
471
|
await fsp.rm(tempRoot, { recursive: true, force: true });
|
|
373
472
|
}
|
|
473
|
+
return finishInstall(binaryPath, version, target, true);
|
|
374
474
|
}
|
|
375
475
|
|
|
376
|
-
function runBinary(binaryPath, args) {
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
476
|
+
async function runBinary(binaryPath, args) {
|
|
477
|
+
const child = spawn(binaryPath, args, { stdio: "inherit" });
|
|
478
|
+
const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
479
|
+
const forward = new Map(signals.map((signal) => [signal, () => child.kill(signal)]));
|
|
480
|
+
for (const [signal, handler] of forward) process.on(signal, handler);
|
|
481
|
+
try {
|
|
482
|
+
const result = await new Promise((resolve, reject) => {
|
|
483
|
+
child.on("error", reject);
|
|
484
|
+
child.on("exit", (code, signal) => resolve({ code, signal }));
|
|
485
|
+
});
|
|
486
|
+
for (const [signal, handler] of forward) process.removeListener(signal, handler);
|
|
487
|
+
if (result.signal && process.platform !== "win32") process.kill(process.pid, result.signal);
|
|
488
|
+
else process.exitCode = result.code ?? 1;
|
|
489
|
+
} finally {
|
|
490
|
+
for (const [signal, handler] of forward) process.removeListener(signal, handler);
|
|
380
491
|
}
|
|
381
|
-
process.exit(result.status ?? 0);
|
|
382
492
|
}
|
|
383
493
|
|
|
384
494
|
async function main() {
|
|
@@ -397,19 +507,23 @@ async function main() {
|
|
|
397
507
|
|
|
398
508
|
if (command === "install") {
|
|
399
509
|
console.error(`Vera ${version} installed.`);
|
|
400
|
-
runBinary(binaryPath, ["agent", "install", ...rest]);
|
|
510
|
+
await runBinary(binaryPath, ["agent", "install", ...rest]);
|
|
401
511
|
return;
|
|
402
512
|
}
|
|
403
513
|
|
|
404
514
|
if (command === "help") {
|
|
405
|
-
runBinary(binaryPath, ["--help"]);
|
|
515
|
+
await runBinary(binaryPath, ["--help"]);
|
|
406
516
|
return;
|
|
407
517
|
}
|
|
408
518
|
|
|
409
|
-
runBinary(binaryPath, [command, ...rest]);
|
|
519
|
+
await runBinary(binaryPath, [command, ...rest]);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (require.main === module) {
|
|
523
|
+
main().catch((error) => {
|
|
524
|
+
console.error(error.message);
|
|
525
|
+
process.exitCode = 1;
|
|
526
|
+
});
|
|
410
527
|
}
|
|
411
528
|
|
|
412
|
-
|
|
413
|
-
console.error(error.message);
|
|
414
|
-
process.exit(1);
|
|
415
|
-
});
|
|
529
|
+
module.exports = { detectMusl, fetchText, downloadFile, extractArchive, ensureBinaryInstalled, createShim, runBinary };
|