nexvora 0.2.0 → 0.2.1
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/bin/nexvora.js +21 -7
- package/package.json +9 -5
- package/scripts/package-binaries.mjs +74 -0
- package/scripts/postinstall.mjs +160 -0
package/bin/nexvora.js
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
const { spawn } = require("node:child_process");
|
|
20
|
+
const { existsSync } = require("node:fs");
|
|
21
|
+
const { join } = require("node:path");
|
|
20
22
|
|
|
21
23
|
/** os-cpu -> per-platform package that carries the prebuilt binary. */
|
|
22
24
|
const PLATFORM_PACKAGES = {
|
|
@@ -41,17 +43,29 @@ function resolveBinary() {
|
|
|
41
43
|
);
|
|
42
44
|
}
|
|
43
45
|
const exe = process.platform === "win32" ? "nexvora.exe" : "nexvora";
|
|
46
|
+
|
|
47
|
+
// 1. Normal path: the per-platform optionalDependency installed cleanly.
|
|
44
48
|
try {
|
|
45
49
|
return require.resolve(`${pkg}/bin/${exe}`);
|
|
46
50
|
} catch {
|
|
47
|
-
|
|
48
|
-
`the native binary for ${key} is missing.\n` +
|
|
49
|
-
`Its package '${pkg}' was not installed — usually because optional\n` +
|
|
50
|
-
`dependencies were skipped. Reinstall with them enabled:\n` +
|
|
51
|
-
` npm install -g nexvora\n` +
|
|
52
|
-
`(remove any --no-optional / --omit=optional flag, and check proxy settings).`,
|
|
53
|
-
);
|
|
51
|
+
/* not installed — try the postinstall fallback below */
|
|
54
52
|
}
|
|
53
|
+
|
|
54
|
+
// 2. Fallback: the postinstall script (scripts/postinstall.mjs) downloads the
|
|
55
|
+
// binary into vendor/<os-cpu>/ when npm skips the optional dependency.
|
|
56
|
+
const vendored = join(__dirname, "..", "vendor", key, exe);
|
|
57
|
+
if (existsSync(vendored)) {
|
|
58
|
+
return vendored;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 3. Neither present — guide the user.
|
|
62
|
+
fail(
|
|
63
|
+
`the native binary for ${key} is missing.\n` +
|
|
64
|
+
`Its package '${pkg}' was not installed (npm skipped the optional\n` +
|
|
65
|
+
`dependency) and the postinstall fallback could not fetch it. Reinstall:\n` +
|
|
66
|
+
` npm install -g nexvora\n` +
|
|
67
|
+
`(remove any --no-optional / --omit=optional flag, and check proxy/registry settings).`,
|
|
68
|
+
);
|
|
55
69
|
}
|
|
56
70
|
|
|
57
71
|
const child = spawn(resolveBinary(), process.argv.slice(2), { stdio: "inherit" });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexvora",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "NexVora — one binary for the MCP server, CLI, and donor daemon. Single login, run anything.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nexvora",
|
|
@@ -15,8 +15,12 @@
|
|
|
15
15
|
"bin": {
|
|
16
16
|
"nexvora": "bin/nexvora.js"
|
|
17
17
|
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"postinstall": "node scripts/postinstall.mjs"
|
|
20
|
+
},
|
|
18
21
|
"files": [
|
|
19
22
|
"bin",
|
|
23
|
+
"scripts",
|
|
20
24
|
"README.md"
|
|
21
25
|
],
|
|
22
26
|
"engines": {
|
|
@@ -24,10 +28,10 @@
|
|
|
24
28
|
},
|
|
25
29
|
"//": "The actual native (Rust) binary ships in exactly one of these per-platform packages; npm installs only the one matching the user's os/cpu (see each package's os/cpu fields). bin/nexvora.js resolves and execs it.",
|
|
26
30
|
"optionalDependencies": {
|
|
27
|
-
"@nexvora/cli-darwin-arm64": "0.2.
|
|
28
|
-
"@nexvora/cli-darwin-x64": "0.2.
|
|
29
|
-
"@nexvora/cli-linux-x64": "0.2.
|
|
30
|
-
"@nexvora/cli-win32-x64": "0.2.
|
|
31
|
+
"@nexvora/cli-darwin-arm64": "0.2.1",
|
|
32
|
+
"@nexvora/cli-darwin-x64": "0.2.1",
|
|
33
|
+
"@nexvora/cli-linux-x64": "0.2.1",
|
|
34
|
+
"@nexvora/cli-win32-x64": "0.2.1"
|
|
31
35
|
},
|
|
32
36
|
"publishConfig": {
|
|
33
37
|
"access": "public",
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Populates the per-platform npm packages with the freshly built Rust binaries
|
|
3
|
+
// and syncs every version to the main package's version. Run in CI after
|
|
4
|
+
// `cargo build --release` for each target, before `npm publish`.
|
|
5
|
+
//
|
|
6
|
+
// Expected layout of built binaries (override the root with NEXVORA_BIN_DIR):
|
|
7
|
+
// <binDir>/aarch64-apple-darwin/nexvora
|
|
8
|
+
// <binDir>/x86_64-apple-darwin/nexvora
|
|
9
|
+
// <binDir>/x86_64-unknown-linux-gnu/nexvora
|
|
10
|
+
// <binDir>/aarch64-unknown-linux-gnu/nexvora
|
|
11
|
+
// <binDir>/x86_64-pc-windows-msvc/nexvora.exe
|
|
12
|
+
//
|
|
13
|
+
// Publish order (optional deps must exist first):
|
|
14
|
+
// node scripts/package-binaries.mjs
|
|
15
|
+
// for d in npm/*/ ; do (cd "$d" && npm publish) ; done
|
|
16
|
+
// npm publish # the main `nexvora` launcher package, last
|
|
17
|
+
|
|
18
|
+
import { chmodSync, copyFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { dirname, join } from "node:path";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
|
|
22
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const root = join(here, "..");
|
|
24
|
+
const binDir = process.env.NEXVORA_BIN_DIR ?? join(root, "..", "..", "nexvora-daemon", "target");
|
|
25
|
+
|
|
26
|
+
// Rust target triple -> { package dir, windows? }
|
|
27
|
+
const TARGETS = {
|
|
28
|
+
"aarch64-apple-darwin": { pkg: "cli-darwin-arm64", windows: false },
|
|
29
|
+
"x86_64-apple-darwin": { pkg: "cli-darwin-x64", windows: false },
|
|
30
|
+
"x86_64-unknown-linux-gnu": { pkg: "cli-linux-x64", windows: false },
|
|
31
|
+
"x86_64-pc-windows-msvc": { pkg: "cli-win32-x64", windows: true },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// `nexvora` is the entry point the npm launcher execs; it finds `nexvora-daemon`
|
|
35
|
+
// (the engine) as a sibling in the same bin/ directory. Both ship per platform.
|
|
36
|
+
const BINARIES = ["nexvora", "nexvora-daemon"];
|
|
37
|
+
|
|
38
|
+
// Version precedence: explicit CLI arg (CI passes the release tag) > package.json.
|
|
39
|
+
const mainPkgVersion = JSON.parse(readFileSync(join(root, "package.json"), "utf8")).version;
|
|
40
|
+
const version = process.argv[2] ?? mainPkgVersion;
|
|
41
|
+
console.log(`Packaging nexvora native binaries at v${version}`);
|
|
42
|
+
|
|
43
|
+
for (const [triple, { pkg, windows }] of Object.entries(TARGETS)) {
|
|
44
|
+
const pkgDir = join(root, "npm", pkg);
|
|
45
|
+
|
|
46
|
+
for (const base of BINARIES) {
|
|
47
|
+
const exe = windows ? `${base}.exe` : base;
|
|
48
|
+
const src = join(binDir, triple, "release", exe);
|
|
49
|
+
const outBin = join(pkgDir, "bin", exe);
|
|
50
|
+
mkdirSync(dirname(outBin), { recursive: true });
|
|
51
|
+
copyFileSync(src, outBin);
|
|
52
|
+
if (!windows) {
|
|
53
|
+
chmodSync(outBin, 0o755);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Keep each platform package's version in lockstep with the launcher.
|
|
58
|
+
const pkgJsonPath = join(pkgDir, "package.json");
|
|
59
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
|
|
60
|
+
pkgJson.version = version;
|
|
61
|
+
writeFileSync(pkgJsonPath, `${JSON.stringify(pkgJson, null, 2)}\n`);
|
|
62
|
+
|
|
63
|
+
console.log(` ${triple} -> @nexvora/${pkg} (${BINARIES.join(", ")})`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Stamp the launcher's own version and pin optionalDependencies to it.
|
|
67
|
+
const mainPath = join(root, "package.json");
|
|
68
|
+
const main = JSON.parse(readFileSync(mainPath, "utf8"));
|
|
69
|
+
main.version = version;
|
|
70
|
+
for (const key of Object.keys(main.optionalDependencies ?? {})) {
|
|
71
|
+
main.optionalDependencies[key] = version;
|
|
72
|
+
}
|
|
73
|
+
writeFileSync(mainPath, `${JSON.stringify(main, null, 2)}\n`);
|
|
74
|
+
console.log("Stamped launcher version + optionalDependencies to", version);
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Postinstall fallback for the native binary.
|
|
3
|
+
//
|
|
4
|
+
// The binary normally arrives via the per-platform optionalDependency
|
|
5
|
+
// (`@nexvora/cli-<os>-<cpu>`). But npm silently SKIPS optionalDependencies in
|
|
6
|
+
// several common situations — custom global prefixes, `--omit=optional`,
|
|
7
|
+
// `--no-optional`, certain proxy/CI setups, and assorted npm bugs — which leaves
|
|
8
|
+
// the launcher with no binary to exec. This script closes that gap: if the
|
|
9
|
+
// optional package isn't resolvable after install, it downloads that exact
|
|
10
|
+
// package's tarball from the registry (same version as this launcher) and
|
|
11
|
+
// extracts the binaries into `vendor/<os-cpu>/`, where bin/nexvora.js also looks.
|
|
12
|
+
//
|
|
13
|
+
// Best-effort by design: it NEVER fails the install. If the download can't
|
|
14
|
+
// happen (offline, locked-down proxy), bin/nexvora.js prints a clear, actionable
|
|
15
|
+
// error at runtime instead.
|
|
16
|
+
|
|
17
|
+
import { createRequire } from "node:module";
|
|
18
|
+
import { dirname, join, basename } from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { gunzipSync } from "node:zlib";
|
|
22
|
+
import { get } from "node:https";
|
|
23
|
+
|
|
24
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const root = join(here, ".."); // the launcher package dir (node_modules/nexvora at install time)
|
|
26
|
+
|
|
27
|
+
/** os-cpu -> per-platform package carrying the prebuilt binary. */
|
|
28
|
+
const PLATFORM_PACKAGES = {
|
|
29
|
+
"darwin-arm64": "@nexvora/cli-darwin-arm64",
|
|
30
|
+
"darwin-x64": "@nexvora/cli-darwin-x64",
|
|
31
|
+
"linux-x64": "@nexvora/cli-linux-x64",
|
|
32
|
+
"win32-x64": "@nexvora/cli-win32-x64",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function log(msg) {
|
|
36
|
+
process.stdout.write(`nexvora(postinstall): ${msg}\n`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Fetch a URL to a Buffer, following redirects (registry -> CDN). */
|
|
40
|
+
function fetchBuffer(url, redirectsLeft = 5) {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const req = get(url, { headers: { "user-agent": "nexvora-postinstall" } }, (res) => {
|
|
43
|
+
const status = res.statusCode ?? 0;
|
|
44
|
+
if (status >= 300 && status < 400 && res.headers.location) {
|
|
45
|
+
res.resume();
|
|
46
|
+
if (redirectsLeft <= 0) {
|
|
47
|
+
reject(new Error("too many redirects"));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const next = new URL(res.headers.location, url).toString();
|
|
51
|
+
resolve(fetchBuffer(next, redirectsLeft - 1));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (status !== 200) {
|
|
55
|
+
res.resume();
|
|
56
|
+
reject(new Error(`HTTP ${status} for ${url}`));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const chunks = [];
|
|
60
|
+
res.on("data", (c) => chunks.push(c));
|
|
61
|
+
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
62
|
+
});
|
|
63
|
+
req.on("error", reject);
|
|
64
|
+
req.setTimeout(30_000, () => req.destroy(new Error("request timed out")));
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Resolve the exact tarball URL for pkg@version from the registry packument. */
|
|
69
|
+
async function resolveTarballUrl(pkg, version) {
|
|
70
|
+
// Honour an explicitly configured registry (npm exposes config as env during
|
|
71
|
+
// lifecycle scripts); default to the public registry.
|
|
72
|
+
const registry = (process.env.npm_config_registry || "https://registry.npmjs.org/").replace(/\/+$/, "/");
|
|
73
|
+
const url = `${registry}${pkg.replace("/", "%2f")}`;
|
|
74
|
+
const packument = JSON.parse((await fetchBuffer(url)).toString("utf8"));
|
|
75
|
+
const tarball = packument?.versions?.[version]?.dist?.tarball;
|
|
76
|
+
if (!tarball) {
|
|
77
|
+
throw new Error(`no tarball for ${pkg}@${version} in the registry`);
|
|
78
|
+
}
|
|
79
|
+
return tarball;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Extract specific binaries from a gzipped npm tarball (ustar). npm tarballs put
|
|
84
|
+
* everything under a `package/` prefix; we only want `package/bin/<name>`.
|
|
85
|
+
*/
|
|
86
|
+
function extractBinaries(tgz, destDir, wantBasenames) {
|
|
87
|
+
const tar = gunzipSync(tgz);
|
|
88
|
+
const want = new Set(wantBasenames);
|
|
89
|
+
const written = [];
|
|
90
|
+
mkdirSync(destDir, { recursive: true });
|
|
91
|
+
let offset = 0;
|
|
92
|
+
while (offset + 512 <= tar.length) {
|
|
93
|
+
const header = tar.subarray(offset, offset + 512);
|
|
94
|
+
const name = header.subarray(0, 100).toString("utf8").replace(/\0.*$/, "");
|
|
95
|
+
if (name === "") break; // end-of-archive (zero block)
|
|
96
|
+
const size = parseInt(header.subarray(124, 136).toString("utf8").replace(/\0.*$/, "").trim(), 8) || 0;
|
|
97
|
+
const typeflag = header[156];
|
|
98
|
+
const isFile = typeflag === 0x30 || typeflag === 0; // '0' or NUL = regular file
|
|
99
|
+
const dataStart = offset + 512;
|
|
100
|
+
const base = basename(name);
|
|
101
|
+
if (isFile && name.startsWith("package/bin/") && want.has(base)) {
|
|
102
|
+
const out = join(destDir, base);
|
|
103
|
+
writeFileSync(out, tar.subarray(dataStart, dataStart + size));
|
|
104
|
+
if (process.platform !== "win32") {
|
|
105
|
+
chmodSync(out, 0o755);
|
|
106
|
+
}
|
|
107
|
+
written.push(base);
|
|
108
|
+
}
|
|
109
|
+
offset = dataStart + Math.ceil(size / 512) * 512;
|
|
110
|
+
}
|
|
111
|
+
return written;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function main() {
|
|
115
|
+
const key = `${process.platform}-${process.arch}`;
|
|
116
|
+
const pkg = PLATFORM_PACKAGES[key];
|
|
117
|
+
if (!pkg) {
|
|
118
|
+
// Unsupported platform: nothing to fetch. The launcher reports this clearly.
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const exe = process.platform === "win32" ? "nexvora.exe" : "nexvora";
|
|
123
|
+
const daemonExe = process.platform === "win32" ? "nexvora-daemon.exe" : "nexvora-daemon";
|
|
124
|
+
|
|
125
|
+
// Fast path: the optional dependency installed normally — nothing to do.
|
|
126
|
+
const require = createRequire(import.meta.url);
|
|
127
|
+
try {
|
|
128
|
+
require.resolve(`${pkg}/bin/${exe}`);
|
|
129
|
+
return;
|
|
130
|
+
} catch {
|
|
131
|
+
/* not installed — fall through to the fallback download */
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const vendorDir = join(root, "vendor", key);
|
|
135
|
+
if (existsSync(join(vendorDir, exe))) {
|
|
136
|
+
return; // already vendored on a previous run
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const version = JSON.parse(readFileSync(join(root, "package.json"), "utf8")).version;
|
|
140
|
+
log(`'${pkg}' was not installed (npm skipped the optional dependency); fetching the ${key} binary for v${version}...`);
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const tarballUrl = await resolveTarballUrl(pkg, version);
|
|
144
|
+
const tgz = await fetchBuffer(tarballUrl);
|
|
145
|
+
const written = extractBinaries(tgz, vendorDir, [exe, daemonExe]);
|
|
146
|
+
if (!written.includes(exe)) {
|
|
147
|
+
throw new Error(`tarball did not contain bin/${exe}`);
|
|
148
|
+
}
|
|
149
|
+
log(`fallback binary installed -> vendor/${key}/ (${written.join(", ")})`);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
log(`could not fetch the fallback binary: ${err.message}`);
|
|
152
|
+
log(`reinstall with optional dependencies enabled ('npm install -g nexvora', no --omit=optional),`);
|
|
153
|
+
log(`or check proxy/registry settings. The launcher will guide you if you run it without a binary.`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Never let a postinstall failure break `npm install`.
|
|
158
|
+
main().catch((err) => {
|
|
159
|
+
log(`unexpected error (ignored): ${err?.message ?? err}`);
|
|
160
|
+
});
|