@muthuishere/citenexuscli 0.9.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/bin/citenexus.mjs +31 -0
- package/lib/install.mjs +186 -0
- package/package.json +28 -0
- package/scripts/postinstall.mjs +16 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// CiteNexus CLI launcher: resolve the platform binary for this {version,
|
|
3
|
+
// platform} (cached, or lazily downloaded + SHA256-verified on first run — so the
|
|
4
|
+
// package works even when postinstall was skipped), then exec it transparently.
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { ensureBinary } from "../lib/install.mjs";
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
let bin;
|
|
10
|
+
try {
|
|
11
|
+
bin = await ensureBinary();
|
|
12
|
+
} catch (err) {
|
|
13
|
+
console.error(String(err && err.message ? err.message : err));
|
|
14
|
+
console.error(
|
|
15
|
+
"citenexus: could not obtain the platform binary. Set CITENEXUS_BINARY to an " +
|
|
16
|
+
"explicit path, or check network/CITENEXUS_DOWNLOAD_BASE."
|
|
17
|
+
);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
const child = spawn(bin, process.argv.slice(2), { stdio: "inherit" });
|
|
21
|
+
child.on("error", (err) => {
|
|
22
|
+
console.error(`citenexus: failed to exec ${bin}: ${err.message}`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
});
|
|
25
|
+
child.on("exit", (code, signal) => {
|
|
26
|
+
if (signal) process.kill(process.pid, signal);
|
|
27
|
+
else process.exit(code ?? 0);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
main();
|
package/lib/install.mjs
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// Shared install/launch logic for the CiteNexus CLI npm package: platform
|
|
2
|
+
// detection, cache paths, SHA256-verified download from GitHub Releases, and
|
|
3
|
+
// binary resolution. Both bin/citenexus.mjs (launcher) and scripts/postinstall.mjs
|
|
4
|
+
// import this. Every filesystem/network dependency is injectable so the behavior
|
|
5
|
+
// is testable offline against a local server. Zero third-party dependencies.
|
|
6
|
+
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { createWriteStream } from "node:fs";
|
|
9
|
+
import { chmod, mkdir, rename, rm, stat, readFile } from "node:fs/promises";
|
|
10
|
+
import { homedir, tmpdir } from "node:os";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { Readable } from "node:stream";
|
|
13
|
+
import { pipeline } from "node:stream/promises";
|
|
14
|
+
|
|
15
|
+
// version is read from package.json at load time (single source of truth).
|
|
16
|
+
export async function packageVersion(env = process.env) {
|
|
17
|
+
if (env.CITENEXUS_VERSION) return env.CITENEXUS_VERSION;
|
|
18
|
+
const pkgUrl = new URL("../package.json", import.meta.url);
|
|
19
|
+
const pkg = JSON.parse(await readFile(pkgUrl, "utf8"));
|
|
20
|
+
return pkg.version;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Default GitHub Releases download base. Assets live under
|
|
24
|
+
// ${base}/<tag>/<asset> with a sibling SHA256SUMS file. The CLI ships as its OWN
|
|
25
|
+
// custom release (tag `cli-v<version>`), decoupled from the core's `v*` releases.
|
|
26
|
+
export const DEFAULT_DOWNLOAD_BASE =
|
|
27
|
+
"https://github.com/muthuishere/citenexus/releases/download";
|
|
28
|
+
|
|
29
|
+
// releaseTag is the GitHub release the binary is pulled from. The CLI's custom
|
|
30
|
+
// release is tagged `cli-v<version>`; $CITENEXUS_RELEASE_TAG overrides it (mirrors
|
|
31
|
+
// / testing).
|
|
32
|
+
export function releaseTag(version, env = process.env) {
|
|
33
|
+
return env.CITENEXUS_RELEASE_TAG || `cli-v${version}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// platformKey maps this runtime to an <os>-<arch> token. Throws on unsupported.
|
|
37
|
+
export function platformKey(platform = process.platform, arch = process.arch) {
|
|
38
|
+
const os = { darwin: "darwin", linux: "linux", win32: "win" }[platform];
|
|
39
|
+
const cpu = { arm64: "arm64", x64: "x64" }[arch];
|
|
40
|
+
if (!os || !cpu) {
|
|
41
|
+
throw new Error(`citenexus: unsupported platform ${platform}/${arch}`);
|
|
42
|
+
}
|
|
43
|
+
return `${os}-${cpu}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// assetName is the release asset for a platform key: citenexus-<os>-<arch>(.exe).
|
|
47
|
+
export function assetName(key = platformKey()) {
|
|
48
|
+
return key.startsWith("win-") ? `citenexus-${key}.exe` : `citenexus-${key}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// binaryName is the on-disk executable name (adds .exe on Windows).
|
|
52
|
+
export function binaryName(key = platformKey()) {
|
|
53
|
+
return key.startsWith("win-") ? "citenexus.exe" : "citenexus";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// cacheRoot is $CITENEXUS_CACHE or ~/.cache/citenexus.
|
|
57
|
+
export function cacheRoot(env = process.env, home = homedir()) {
|
|
58
|
+
return env.CITENEXUS_CACHE || join(home, ".cache", "citenexus");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// binaryPath is <cacheRoot>/<version>/<platform>/citenexus(.exe).
|
|
62
|
+
export function binaryPath(opts = {}) {
|
|
63
|
+
const env = opts.env || process.env;
|
|
64
|
+
const key = opts.platform || platformKey();
|
|
65
|
+
const version = opts.version;
|
|
66
|
+
const root = opts.cacheRoot || cacheRoot(env, opts.home);
|
|
67
|
+
return join(root, version, key, binaryName(key));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function fileExists(path) {
|
|
71
|
+
try {
|
|
72
|
+
await stat(path);
|
|
73
|
+
return true;
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// sha256File returns the lowercase hex digest of a file.
|
|
80
|
+
export async function sha256File(path) {
|
|
81
|
+
const hash = createHash("sha256");
|
|
82
|
+
hash.update(await readFile(path));
|
|
83
|
+
return hash.digest("hex");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// parseChecksums parses `shasum -a 256` output (`<hex> <name>`) into a map.
|
|
87
|
+
export function parseChecksums(text) {
|
|
88
|
+
const map = {};
|
|
89
|
+
for (const line of text.split("\n")) {
|
|
90
|
+
const m = line.trim().match(/^([0-9a-fA-F]{64})\s+\*?(.+)$/);
|
|
91
|
+
if (m) map[m[2].trim()] = m[1].toLowerCase();
|
|
92
|
+
}
|
|
93
|
+
return map;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// httpGet fetches a URL, honoring HTTPS_PROXY when a proxy dispatcher is available
|
|
97
|
+
// (best-effort; falls back to a direct connection). Returns a fetch Response.
|
|
98
|
+
async function httpGet(url, env) {
|
|
99
|
+
const proxy = env.HTTPS_PROXY || env.https_proxy;
|
|
100
|
+
let dispatcher;
|
|
101
|
+
if (proxy) {
|
|
102
|
+
try {
|
|
103
|
+
const { ProxyAgent } = await import("undici");
|
|
104
|
+
dispatcher = new ProxyAgent(proxy);
|
|
105
|
+
} catch {
|
|
106
|
+
// undici ProxyAgent unavailable — proceed with a direct connection.
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const res = await fetch(url, dispatcher ? { dispatcher } : undefined);
|
|
110
|
+
if (!res.ok) throw new Error(`citenexus: GET ${url} → HTTP ${res.status}`);
|
|
111
|
+
return res;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// download streams url into dest atomically (via a temp file + rename).
|
|
115
|
+
export async function download(url, dest, env = process.env) {
|
|
116
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
117
|
+
const res = await httpGet(url, env);
|
|
118
|
+
const tmp = join(tmpdir(), `citenexus-dl-${process.pid}-${Date.now()}`);
|
|
119
|
+
await pipeline(Readable.fromWeb(res.body), createWriteStream(tmp));
|
|
120
|
+
await rename(tmp, dest);
|
|
121
|
+
return dest;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// fetchText downloads a small text resource (e.g. SHA256SUMS).
|
|
125
|
+
export async function fetchText(url, env = process.env) {
|
|
126
|
+
const res = await httpGet(url, env);
|
|
127
|
+
return res.text();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// verifyChecksum throws unless the file's SHA256 equals expected (lowercase hex).
|
|
131
|
+
export async function verifyChecksum(path, expected) {
|
|
132
|
+
const actual = await sha256File(path);
|
|
133
|
+
if (!expected) {
|
|
134
|
+
throw new Error(`citenexus: no published checksum to verify ${path}`);
|
|
135
|
+
}
|
|
136
|
+
if (actual.toLowerCase() !== expected.toLowerCase()) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`citenexus: checksum mismatch — refusing to run ${path} (expected ${expected}, got ${actual})`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// downloadBase resolves the release base URL (mirror override wins).
|
|
144
|
+
export function downloadBase(env = process.env) {
|
|
145
|
+
return env.CITENEXUS_DOWNLOAD_BASE || DEFAULT_DOWNLOAD_BASE;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ensureBinary returns a path to a verified, executable binary, downloading it if
|
|
149
|
+
// necessary. Resolution order:
|
|
150
|
+
// 1. $CITENEXUS_BINARY (explicit path) — used as-is, no download, no checksum.
|
|
151
|
+
// 2. a cached binary for this {version, platform} — re-used with NO network.
|
|
152
|
+
// 3. download <base>/v<version>/<asset>, verify against the release SHA256SUMS,
|
|
153
|
+
// chmod +x, cache, and return it.
|
|
154
|
+
// A tampered download is deleted and refused.
|
|
155
|
+
export async function ensureBinary(opts = {}) {
|
|
156
|
+
const env = opts.env || process.env;
|
|
157
|
+
if (env.CITENEXUS_BINARY) return env.CITENEXUS_BINARY;
|
|
158
|
+
|
|
159
|
+
const version = opts.version || (await packageVersion(env));
|
|
160
|
+
const key = opts.platform || platformKey();
|
|
161
|
+
const dest = binaryPath({ ...opts, env, version, platform: key });
|
|
162
|
+
|
|
163
|
+
if (!opts.force && (await fileExists(dest))) {
|
|
164
|
+
return dest; // cache hit — no network.
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const base = opts.downloadBase || downloadBase(env);
|
|
168
|
+
const tag = opts.tag || releaseTag(version, env);
|
|
169
|
+
const asset = assetName(key);
|
|
170
|
+
const sumsText = await fetchText(`${base}/${tag}/SHA256SUMS`, env);
|
|
171
|
+
const sums = parseChecksums(sumsText);
|
|
172
|
+
const expected = sums[asset];
|
|
173
|
+
if (!expected) {
|
|
174
|
+
throw new Error(`citenexus: ${asset} not listed in SHA256SUMS for ${tag}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
await download(`${base}/${tag}/${asset}`, dest, env);
|
|
178
|
+
try {
|
|
179
|
+
await verifyChecksum(dest, expected);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
await rm(dest, { force: true });
|
|
182
|
+
throw err;
|
|
183
|
+
}
|
|
184
|
+
await chmod(dest, 0o755);
|
|
185
|
+
return dest;
|
|
186
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@muthuishere/citenexuscli",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "CiteNexus CLI — evidence-first, multilingual RAG from your shell. Ingests artifacts and answers ONLY from retrieved, cited evidence, abstaining when evidence is weak, missing, or conflicting. A self-contained Go+Rust binary, fetched per platform from a dedicated GitHub Release (cli-v* tag).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/muthuishere/citenexus.git",
|
|
10
|
+
"directory": "cli"
|
|
11
|
+
},
|
|
12
|
+
"keywords": ["rag", "retrieval", "cli", "evidence", "citations", "grounded", "abstain"],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"citenexus": "bin/citenexus.mjs"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"bin",
|
|
21
|
+
"scripts",
|
|
22
|
+
"lib"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"postinstall": "node scripts/postinstall.mjs",
|
|
26
|
+
"test": "node --test \"test/**/*.test.mjs\""
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Best-effort prefetch of the platform binary at npm install time. A failure here
|
|
2
|
+
// is NEVER fatal: the launcher (bin/citenexus.mjs) lazily downloads + verifies on
|
|
3
|
+
// first run, so `--ignore-scripts`, offline, and CI installs all still work.
|
|
4
|
+
import { ensureBinary } from "../lib/install.mjs";
|
|
5
|
+
|
|
6
|
+
try {
|
|
7
|
+
const bin = await ensureBinary();
|
|
8
|
+
console.log(`citenexus: binary ready at ${bin}`);
|
|
9
|
+
} catch (err) {
|
|
10
|
+
console.warn(
|
|
11
|
+
`citenexus: could not prefetch the binary (${
|
|
12
|
+
err && err.message ? err.message : err
|
|
13
|
+
}); it will be downloaded on first run.`
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
process.exit(0);
|