@vikat/vikat-gateway-cli 1.0.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.
Files changed (2) hide show
  1. package/bin.js +444 -0
  2. package/package.json +36 -0
package/bin.js ADDED
@@ -0,0 +1,444 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createHash } from "crypto";
4
+ import { chmodSync, createWriteStream, existsSync, fsyncSync, mkdirSync, readFileSync, appendFileSync, renameSync, unlinkSync } from "fs";
5
+ import { homedir } from "os";
6
+ import { dirname, join } from "path";
7
+ import { Readable } from "stream";
8
+ import { createInterface } from "readline";
9
+
10
+ const BASE_URL = "https://downloads.vikat.ai";
11
+ // Matches the migration CLI. Without a bound, a hung host hangs the install.
12
+ const REQUEST_TIMEOUT_MS = 30_000;
13
+
14
+ // Parse CLI version from command line arguments
15
+ function parseCliVersion() {
16
+ const args = process.argv.slice(2);
17
+ let cliVersion = "latest"; // Default to latest
18
+
19
+ // Find --cli-version argument
20
+ const versionArgIndex = args.findIndex((arg) => arg.startsWith("--cli-version"));
21
+
22
+ if (versionArgIndex !== -1) {
23
+ const versionArg = args[versionArgIndex];
24
+
25
+ if (versionArg.includes("=")) {
26
+ // Format: --cli-version=v1.2.3
27
+ cliVersion = versionArg.split("=")[1];
28
+ if (!cliVersion) {
29
+ console.error("--cli-version requires a value");
30
+ process.exit(1);
31
+ }
32
+ } else if (versionArgIndex + 1 < args.length) {
33
+ // Format: --cli-version v1.2.3
34
+ cliVersion = args[versionArgIndex + 1];
35
+ } else {
36
+ console.error("--cli-version requires a value");
37
+ process.exit(1);
38
+ }
39
+ }
40
+
41
+ return validateCliVersion(cliVersion);
42
+ }
43
+
44
+ // Validate a CLI version and NORMALISE it to the form the download server uses.
45
+ //
46
+ // ─── THE BARE VERSION IS THE ONE ON THE SERVER ──────────────────────────────
47
+ //
48
+ // deploy/publish.sh uploads to s3://…/vikat-cli/$VERSION/… with VERSION exactly
49
+ // as release.sh cut it: bare, no "v". This function used to REQUIRE the "v"
50
+ // prefix and pass it straight through into the URL, so the only input form it
51
+ // accepted produced the one path that does not exist:
52
+ //
53
+ // /vikat-cli/v1.6.3/darwin/arm64/vikat -> 404
54
+ // /vikat-cli/1.6.3/darwin/arm64/vikat -> 200
55
+ //
56
+ // Both forms are accepted now, because "v1.7.0" is what a person types and what
57
+ // the git tag looks like, and the bare form is what S3 holds. The prefix is
58
+ // stripped here, once, so every caller below builds a URL that can resolve.
59
+ function validateCliVersion(version) {
60
+ if (version === "latest") {
61
+ return version;
62
+ }
63
+
64
+ // Accept "v1.2.3" or "1.2.3", with an optional pre-release suffix.
65
+ const versionRegex = /^v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/;
66
+ const match = versionRegex.exec(version);
67
+ if (match) {
68
+ return match[1]; // normalised: always bare
69
+ }
70
+
71
+ console.error(`Invalid CLI version format: ${version}`);
72
+ console.error(`CLI version must be "latest", "1.2.3", "v1.2.3", or "v1.2.3-prerelease1"`);
73
+ process.exit(1);
74
+ }
75
+
76
+ const VERSION = parseCliVersion();
77
+
78
+ function getPlatformArchAndBinary() {
79
+ const platform = process.platform;
80
+ const arch = process.arch;
81
+
82
+ let platformDir;
83
+ let archDir;
84
+ let binaryName;
85
+
86
+ if (platform === "darwin") {
87
+ platformDir = "darwin";
88
+ if (arch === "arm64") archDir = "arm64";
89
+ else archDir = "amd64";
90
+ binaryName = "vikat";
91
+ } else if (platform === "linux") {
92
+ platformDir = "linux";
93
+ if (arch === "x64") archDir = "amd64";
94
+ else if (arch === "ia32") archDir = "386";
95
+ else archDir = arch; // fallback
96
+ binaryName = "vikat";
97
+ } else if (platform === "win32") {
98
+ platformDir = "windows";
99
+ if (arch === "x64") archDir = "amd64";
100
+ else if (arch === "ia32") archDir = "386";
101
+ else archDir = arch; // fallback
102
+ binaryName = "vikat.exe";
103
+ } else {
104
+ console.error(`Unsupported platform/arch: ${platform}/${arch}`);
105
+ process.exit(1);
106
+ }
107
+
108
+ return { platformDir, archDir, binaryName };
109
+ }
110
+
111
+ async function downloadBinary(url, dest) {
112
+ const res = await fetch(url);
113
+
114
+ if (!res.ok) {
115
+ console.error(`❌ Download failed: ${res.status} ${res.statusText}`);
116
+ process.exit(1);
117
+ }
118
+
119
+ const contentLength = res.headers.get("content-length");
120
+ const totalSize = contentLength ? parseInt(contentLength, 10) : null;
121
+ let downloadedSize = 0;
122
+
123
+ const fileStream = createWriteStream(dest, { flags: "w" });
124
+ await new Promise((resolve, reject) => {
125
+ try {
126
+ // Convert the fetch response body to a Node.js readable stream
127
+ const nodeStream = Readable.fromWeb(res.body);
128
+
129
+ // Add progress tracking
130
+ nodeStream.on("data", (chunk) => {
131
+ downloadedSize += chunk.length;
132
+ if (totalSize) {
133
+ const progress = ((downloadedSize / totalSize) * 100).toFixed(1);
134
+ process.stdout.write(`\r⏱️ Downloading Binary: ${progress}% (${formatBytes(downloadedSize)}/${formatBytes(totalSize)})`);
135
+ } else {
136
+ process.stdout.write(`\r⏱️ Downloaded: ${formatBytes(downloadedSize)}`);
137
+ }
138
+ });
139
+
140
+ nodeStream.pipe(fileStream);
141
+ fileStream.on("finish", () => {
142
+ process.stdout.write("\n");
143
+
144
+ // Ensure file is fully written to disk
145
+ try {
146
+ fsyncSync(fileStream.fd);
147
+ } catch (syncError) {
148
+ // fsync might fail on some systems, ignore
149
+ }
150
+
151
+ resolve();
152
+ });
153
+ fileStream.on("error", reject);
154
+ nodeStream.on("error", reject);
155
+ } catch (error) {
156
+ reject(error);
157
+ }
158
+ });
159
+
160
+ chmodSync(dest, 0o755);
161
+ }
162
+
163
+ // Check if a specific version exists on the download server
164
+ async function checkVersionExists(version, platformDir, archDir, binaryName) {
165
+ const url = `${BASE_URL}/vikat-cli/${version}/${platformDir}/${archDir}/${binaryName}`;
166
+ const res = await fetch(url, { method: "HEAD" });
167
+ return res.ok;
168
+ }
169
+
170
+ // Resolve the "latest" pointer to a concrete version, then prove that version
171
+ // actually has a binary for this platform before returning it.
172
+ //
173
+ // The two-step matters. version.txt is written LAST by publish.sh, deliberately,
174
+ // so the binaries are in place before anything advertises them — but a partial
175
+ // upload, or a platform that was never built, still leaves the pointer naming a
176
+ // version with no artifact for THIS os/arch. Checking here turns that into a
177
+ // message naming the version and the platform, instead of a bare 404 later.
178
+ async function fetchLatestVersion(platformDir, archDir, binaryName) {
179
+ const url = `${BASE_URL}/vikat-cli/latest/version.txt`;
180
+ let res;
181
+ try {
182
+ res = await fetch(url);
183
+ } catch (error) {
184
+ console.error(`❌ Could not reach the download server: ${error.message}`);
185
+ console.error(` ${url}`);
186
+ process.exit(1);
187
+ }
188
+ if (!res.ok) {
189
+ console.error(`❌ Could not resolve the latest CLI version (HTTP ${res.status}).`);
190
+ console.error(` ${url}`);
191
+ console.error(`Pass an explicit version instead, e.g. --cli-version 1.7.0`);
192
+ process.exit(1);
193
+ }
194
+
195
+ // Trimmed because publish.sh writes it with `printf '%s'` — no trailing
196
+ // newline today — but a hand-corrected pointer file would very likely have
197
+ // one, and a stray "\n" inside a URL path is a 404 nobody can read.
198
+ const raw = (await res.text()).trim();
199
+ const version = validateCliVersion(raw);
200
+ if (version === "latest") {
201
+ console.error(`❌ ${url} contains "latest", which would resolve to itself.`);
202
+ process.exit(1);
203
+ }
204
+
205
+ if (!(await checkVersionExists(version, platformDir, archDir, binaryName))) {
206
+ console.error(`❌ Latest is ${version}, but no ${platformDir}/${archDir} binary was published for it.`);
207
+ console.error(` ${BASE_URL}/vikat-cli/${version}/${platformDir}/${archDir}/${binaryName}`);
208
+ process.exit(1);
209
+ }
210
+ return version;
211
+ }
212
+
213
+ // Verify the downloaded binary against its SHA-256 checksum.
214
+ //
215
+ // FAILS CLOSED. This warned and returned, and the caller then moved the
216
+ // unverified binary into ~/.vikat/bin — a directory the installer appends to the
217
+ // user's shell PATH. A check that can be skipped by declining to serve one file
218
+ // is not a check: anyone able to serve a malicious binary is equally able to
219
+ // withhold its sidecar. The sibling migration CLI already refused; this now
220
+ // matches it.
221
+ async function verifyChecksum(binaryPath, checksumUrl) {
222
+ let res;
223
+ try {
224
+ res = await fetch(checksumUrl, { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
225
+ } catch (err) {
226
+ unlinkSync(binaryPath);
227
+ console.error(`❌ Could not fetch the checksum (${err.message}). Refusing to run an unverified binary.`);
228
+ process.exit(1);
229
+ }
230
+ if (!res.ok) {
231
+ unlinkSync(binaryPath);
232
+ console.error(`❌ Checksum file not available (${res.status}). Refusing to run an unverified binary.`);
233
+ process.exit(1);
234
+ }
235
+
236
+ const checksumContent = (await res.text()).trim();
237
+ // Format: "<hash> <filename>" (shasum output)
238
+ const expectedHash = checksumContent.split(/\s+/)[0];
239
+ if (!expectedHash) {
240
+ console.warn("⚠️ Could not parse checksum file, skipping verification");
241
+ return;
242
+ }
243
+
244
+ const fileBuffer = readFileSync(binaryPath);
245
+ const actualHash = createHash("sha256").update(fileBuffer).digest("hex");
246
+
247
+ if (actualHash !== expectedHash) {
248
+ const { unlinkSync } = await import("fs");
249
+ unlinkSync(binaryPath);
250
+ console.error(`❌ Checksum verification failed!`);
251
+ console.error(` Expected: ${expectedHash}`);
252
+ console.error(` Got: ${actualHash}`);
253
+ console.error(` The downloaded binary has been deleted for safety.`);
254
+ process.exit(1);
255
+ }
256
+
257
+ console.log("✅ Checksum verified");
258
+ }
259
+
260
+ function formatBytes(bytes) {
261
+ if (bytes === 0) return "0 B";
262
+ const k = 1024;
263
+ const sizes = ["B", "KB", "MB", "GB"];
264
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
265
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
266
+ }
267
+
268
+ // Detect the user's shell and return the RC file path and the PATH export line
269
+ function getShellConfig() {
270
+ const home = homedir();
271
+ const shell = (process.env.SHELL || "").toLowerCase();
272
+
273
+ if (process.platform === "win32") {
274
+ return null; // Windows — manual PATH setup
275
+ }
276
+
277
+ if (shell.endsWith("/fish") || shell.endsWith("/fish.exe")) {
278
+ return {
279
+ rcFile: join(home, ".config", "fish", "config.fish"),
280
+ exportLine: "fish_add_path $HOME/.vikat/bin",
281
+ shellName: "fish",
282
+ };
283
+ }
284
+
285
+ if (shell.endsWith("/zsh")) {
286
+ return {
287
+ rcFile: join(home, ".zshrc"),
288
+ exportLine: 'export PATH="$HOME/.vikat/bin:$PATH"',
289
+ shellName: "zsh",
290
+ };
291
+ }
292
+
293
+ if (shell.endsWith("/bash") || shell.endsWith("/bash.exe")) {
294
+ const rcFiles =
295
+ process.platform === "darwin"
296
+ ? [join(home, ".bash_profile"), join(home, ".bashrc")]
297
+ : [join(home, ".bashrc")];
298
+ return {
299
+ rcFile: rcFiles[0],
300
+ extraRcFiles: rcFiles.slice(1),
301
+ exportLine: 'export PATH="$HOME/.vikat/bin:$PATH"',
302
+ shellName: "bash",
303
+ };
304
+ }
305
+
306
+ return null;
307
+ }
308
+
309
+ // Check if the PATH export line is already present in the given file
310
+ function hasPathLine(filePath) {
311
+ if (!existsSync(filePath)) return false;
312
+ try {
313
+ const content = readFileSync(filePath, "utf-8");
314
+ return content.includes(".vikat/bin");
315
+ } catch {
316
+ return false;
317
+ }
318
+ }
319
+
320
+ // Prompt the user with a yes/no question
321
+ async function promptYesNo(question) {
322
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
323
+ return false;
324
+ }
325
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
326
+ return new Promise((resolve) => {
327
+ rl.question(question, (answer) => {
328
+ rl.close();
329
+ const normalized = (answer || "").trim().toLowerCase();
330
+ resolve(normalized === "" || normalized === "y" || normalized === "yes");
331
+ });
332
+ });
333
+ }
334
+
335
+ function printStartMessage(message) {
336
+ console.log(`\n${message}`);
337
+ console.log(`Enter 'vikat' when you're ready to start the CLI.`);
338
+ }
339
+
340
+ async function main() {
341
+ const { platformDir, archDir, binaryName } = getPlatformArchAndBinary();
342
+
343
+ let namedVersion;
344
+
345
+ if (VERSION === "latest") {
346
+ // ─── "latest" IS A POINTER FILE, NOT A DIRECTORY ────────────────────
347
+ //
348
+ // This used to HEAD /vikat-cli/latest/<os>/<arch>/vikat and, when that
349
+ // 404'd, tell the user to pass --cli-version. It 404s always:
350
+ // deploy/publish.sh writes exactly ONE object under that prefix,
351
+ // vikat-cli/latest/version.txt (publish.sh:250), and puts the binaries
352
+ // under /vikat-cli/$VERSION/ (publish.sh:237). So the documented install
353
+ // — `npx @vikat/vikat-gateway-cli` — could never work, and neither could
354
+ // the workaround it printed.
355
+ //
356
+ // Resolve the pointer first, then fetch the concrete version. That is
357
+ // exactly what the Go self-updater does (cli/internal/update/check.go
358
+ // reads the same version.txt), so the two installers now agree instead
359
+ // of disagreeing.
360
+ namedVersion = await fetchLatestVersion(platformDir, archDir, binaryName);
361
+ } else {
362
+ // For explicitly specified versions, verify it exists on the server
363
+ const versionExists = await checkVersionExists(VERSION, platformDir, archDir, binaryName);
364
+ if (!versionExists) {
365
+ console.error(`❌ CLI version '${VERSION}' not found.`);
366
+ console.error(`Please verify the version exists at: ${BASE_URL}/vikat-cli/`);
367
+ process.exit(1);
368
+ }
369
+ namedVersion = VERSION;
370
+ }
371
+
372
+ const downloadUrl = `${BASE_URL}/vikat-cli/${namedVersion}/${platformDir}/${archDir}/${binaryName}`;
373
+
374
+ // Install to ~/.vikat/bin/
375
+ const installDir = join(homedir(), ".vikat", "bin");
376
+ mkdirSync(installDir, { recursive: true });
377
+ const binaryPath = join(installDir, binaryName);
378
+
379
+ // Download to a temp file, verify, then atomically replace
380
+ const tempBinaryPath = `${binaryPath}.download-${process.pid}-${Date.now()}`;
381
+ try {
382
+ await downloadBinary(downloadUrl, tempBinaryPath);
383
+
384
+ const checksumUrl = `${BASE_URL}/vikat-cli/${namedVersion}/${platformDir}/${archDir}/${binaryName}.sha256`;
385
+ await verifyChecksum(tempBinaryPath, checksumUrl);
386
+ renameSync(tempBinaryPath, binaryPath);
387
+ } catch (err) {
388
+ try { unlinkSync(tempBinaryPath); } catch {}
389
+ throw err;
390
+ }
391
+ console.log(`✅ Installed vikat to ${binaryPath}`);
392
+
393
+ // Shell PATH setup
394
+ const shellConfig = getShellConfig();
395
+
396
+ if (!shellConfig) {
397
+ // Windows — print manual instructions
398
+ console.log(`\nTo complete installation, add the following directory to your PATH:`);
399
+ console.log(` ${installDir}`);
400
+ console.log(`\nYou can do this in System Settings > Environment Variables.`);
401
+ printStartMessage("The installer won't start Vikat automatically.");
402
+ return;
403
+ }
404
+
405
+ // Check if PATH is already configured
406
+ const allRcFiles = [shellConfig.rcFile, ...(shellConfig.extraRcFiles || [])];
407
+ const missingRcFiles = allRcFiles.filter((rcFile) => !hasPathLine(rcFile));
408
+
409
+ if (missingRcFiles.length === 0) {
410
+ console.log(`\n✅ PATH already configured for vikat.`);
411
+ printStartMessage("The installer won't start Vikat automatically.");
412
+ return;
413
+ }
414
+
415
+ // Prompt user to add PATH
416
+ const rcDisplayName = shellConfig.rcFile.startsWith(homedir()) ? shellConfig.rcFile.slice(homedir().length + 1) : shellConfig.rcFile.split("/").pop();
417
+ const shouldAdd = await promptYesNo(`\nAdd vikat to your PATH in ~/${rcDisplayName}? [Y/n] `);
418
+
419
+ if (!shouldAdd) {
420
+ console.log(`\nSkipped PATH setup. You can manually add this to your shell config:`);
421
+ console.log(` ${shellConfig.exportLine}`);
422
+ printStartMessage("After updating your PATH, start Vikat manually.");
423
+ return;
424
+ }
425
+
426
+ // Append the export line to RC file(s)
427
+ for (const rcFile of missingRcFiles) {
428
+ try {
429
+ mkdirSync(dirname(rcFile), { recursive: true });
430
+ appendFileSync(rcFile, `\n# Added by vikat installer\n${shellConfig.exportLine}\n`);
431
+ console.log(`✅ Added PATH to ${rcFile}`);
432
+ } catch (err) {
433
+ console.error(`⚠️ Failed to update ${rcFile}: ${err.message}`);
434
+ }
435
+ }
436
+
437
+ const rcRelative = shellConfig.rcFile.startsWith(homedir()) ? shellConfig.rcFile.slice(homedir().length + 1) : shellConfig.rcFile.split("/").pop();
438
+ printStartMessage(`Run 'source ~/${rcRelative}' or open a new terminal first.`);
439
+ }
440
+
441
+ main().catch((error) => {
442
+ console.error(`❌ Failed to install Vikat CLI: ${error.message}`);
443
+ process.exit(1);
444
+ });
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@vikat/vikat-gateway-cli",
3
+ "version": "1.0.1",
4
+ "description": "Vikat CLI - Use any harness + any models through Vikat gateway with full observability and governance",
5
+ "keywords": [
6
+ "ai",
7
+ "gateway",
8
+ "openai",
9
+ "anthropic",
10
+ "cli",
11
+ "vikat",
12
+ "ai-gateway"
13
+ ],
14
+ "homepage": "https://vikat.local",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/Vikat-AI/vikat-gateway.git",
18
+ "directory": "npx/vikat-cli"
19
+ },
20
+ "license": "Apache-2.0",
21
+ "author": "Vikat",
22
+ "engines": {
23
+ "node": ">=18.0.0"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "bin": {
29
+ "vikat": "bin.js"
30
+ },
31
+ "type": "module",
32
+ "dependencies": {},
33
+ "files": [
34
+ "bin.js"
35
+ ]
36
+ }