@sponsoredai/cli 0.1.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 ADDED
@@ -0,0 +1,43 @@
1
+ # SAI npm launcher
2
+
3
+ This package installs the `sai` command by downloading the platform binary from
4
+ `https://downloads.sponsoredai.dev/sai`.
5
+
6
+ The npm package is intentionally small. It contains a Node.js launcher and the
7
+ installer script, not the Python source tree.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install -g @sponsoredai/cli
13
+ sai login
14
+ sai claude
15
+ ```
16
+
17
+ ## Binary layout
18
+
19
+ The installer expects release assets at:
20
+
21
+ ```text
22
+ https://downloads.sponsoredai.dev/sai/v0.1.0/sai-darwin-arm64
23
+ https://downloads.sponsoredai.dev/sai/v0.1.0/sai-darwin-arm64.sha256
24
+ ```
25
+
26
+ Supported names are:
27
+
28
+ ```text
29
+ sai-darwin-arm64
30
+ sai-linux-x64
31
+ sai-win32-x64.exe
32
+ ```
33
+
34
+ The `.sha256` file must contain the hex SHA-256 digest. Standard
35
+ `sha256sum`-style files are supported.
36
+
37
+ ## Overrides
38
+
39
+ ```bash
40
+ SAI_BINARY_BASE_URL=https://downloads.example.com/sai npm install -g @sponsoredai/cli
41
+ SAI_BINARY_VERSION=v0.1.0 npm install -g @sponsoredai/cli
42
+ SAI_SKIP_BINARY_DOWNLOAD=1 npm install -g @sponsoredai/cli
43
+ ```
package/bin/sai.js ADDED
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const { spawn } = require("child_process");
7
+
8
+ const binaryName = process.platform === "win32" ? "sai.exe" : "sai";
9
+ const binaryPath = path.join(__dirname, "..", "vendor", binaryName);
10
+
11
+ if (!fs.existsSync(binaryPath)) {
12
+ console.error("SAI binary is not installed.");
13
+ console.error("Reinstall the package, or run: npm rebuild @sponsoredai/cli");
14
+ process.exit(1);
15
+ }
16
+
17
+ const child = spawn(binaryPath, process.argv.slice(2), {
18
+ stdio: "inherit",
19
+ windowsHide: false
20
+ });
21
+
22
+ child.on("error", (error) => {
23
+ console.error(`Failed to start SAI: ${error.message}`);
24
+ process.exit(1);
25
+ });
26
+
27
+ child.on("close", (code, signal) => {
28
+ if (signal) {
29
+ console.error(`SAI exited from signal ${signal}`);
30
+ process.exit(1);
31
+ }
32
+ process.exit(code == null ? 1 : code);
33
+ });
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@sponsoredai/cli",
3
+ "version": "0.1.0",
4
+ "description": "SAI CLI launcher that downloads the platform binary.",
5
+ "license": "MIT",
6
+ "homepage": "https://sponsoredai.dev",
7
+ "bin": {
8
+ "sai": "bin/sai.js"
9
+ },
10
+ "scripts": {
11
+ "postinstall": "node scripts/install.js"
12
+ },
13
+ "files": [
14
+ "bin/",
15
+ "scripts/",
16
+ "README.md"
17
+ ],
18
+ "os": [
19
+ "darwin",
20
+ "linux",
21
+ "win32"
22
+ ],
23
+ "cpu": [
24
+ "x64",
25
+ "arm64"
26
+ ],
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "sai": {
34
+ "binaryBaseUrl": "https://downloads.sponsoredai.dev/sai"
35
+ }
36
+ }
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const crypto = require("crypto");
5
+ const fs = require("fs");
6
+ const http = require("http");
7
+ const https = require("https");
8
+ const path = require("path");
9
+
10
+ const { binaryFileName, targetTriple } = require("./platform");
11
+
12
+ const packageRoot = path.resolve(__dirname, "..");
13
+ const pkg = require(path.join(packageRoot, "package.json"));
14
+ const vendorDir = path.join(packageRoot, "vendor");
15
+ const installedName = process.platform === "win32" ? "sai.exe" : "sai";
16
+ const installedPath = path.join(vendorDir, installedName);
17
+
18
+ function info(message) {
19
+ console.log(`[sai] ${message}`);
20
+ }
21
+
22
+ function fail(message) {
23
+ console.error(`[sai] ${message}`);
24
+ process.exit(1);
25
+ }
26
+
27
+ function request(url, redirectCount = 0) {
28
+ return new Promise((resolve, reject) => {
29
+ const client = url.startsWith("https:") ? https : http;
30
+ const req = client.get(url, (res) => {
31
+ const status = res.statusCode || 0;
32
+ if (status >= 300 && status < 400 && res.headers.location) {
33
+ if (redirectCount >= 5) {
34
+ reject(new Error(`too many redirects while downloading ${url}`));
35
+ return;
36
+ }
37
+ const nextUrl = new URL(res.headers.location, url).toString();
38
+ res.resume();
39
+ resolve(request(nextUrl, redirectCount + 1));
40
+ return;
41
+ }
42
+ if (status !== 200) {
43
+ res.resume();
44
+ reject(new Error(`download failed with HTTP ${status}: ${url}`));
45
+ return;
46
+ }
47
+
48
+ const chunks = [];
49
+ res.on("data", (chunk) => chunks.push(chunk));
50
+ res.on("end", () => resolve(Buffer.concat(chunks)));
51
+ });
52
+ req.on("error", reject);
53
+ });
54
+ }
55
+
56
+ function parseChecksum(text, fileName) {
57
+ const lines = text.split(/\r?\n/).filter(Boolean);
58
+ for (const line of lines) {
59
+ const parts = line.trim().split(/\s+/);
60
+ if (parts.length >= 1 && /^[a-f0-9]{64}$/i.test(parts[0])) {
61
+ if (parts.length === 1 || parts.some((part) => part.endsWith(fileName))) {
62
+ return parts[0].toLowerCase();
63
+ }
64
+ }
65
+ }
66
+ fail(`invalid checksum file for ${fileName}`);
67
+ }
68
+
69
+ async function main() {
70
+ if (process.env.SAI_SKIP_BINARY_DOWNLOAD === "1") {
71
+ info("Skipping binary download because SAI_SKIP_BINARY_DOWNLOAD=1");
72
+ return;
73
+ }
74
+
75
+ const target = targetTriple();
76
+ if (!target) {
77
+ fail(`unsupported platform ${process.platform}/${process.arch}`);
78
+ }
79
+
80
+ const fileName = binaryFileName(target);
81
+ const baseUrl = (process.env.SAI_BINARY_BASE_URL || pkg.sai.binaryBaseUrl).replace(/\/+$/, "");
82
+ const version = process.env.SAI_BINARY_VERSION || `v${pkg.version}`;
83
+ const binaryUrl = `${baseUrl}/${version}/${fileName}`;
84
+ const checksumUrl = `${binaryUrl}.sha256`;
85
+
86
+ info(`Downloading ${fileName}`);
87
+ const [binary, checksumBuffer] = await Promise.all([
88
+ request(binaryUrl),
89
+ request(checksumUrl)
90
+ ]);
91
+
92
+ const expected = parseChecksum(checksumBuffer.toString("utf8"), fileName);
93
+ const actual = crypto.createHash("sha256").update(binary).digest("hex");
94
+ if (actual !== expected) {
95
+ fail(`checksum mismatch for ${fileName}`);
96
+ }
97
+
98
+ fs.mkdirSync(vendorDir, { recursive: true });
99
+ fs.writeFileSync(installedPath, binary);
100
+ if (process.platform !== "win32") {
101
+ fs.chmodSync(installedPath, 0o755);
102
+ }
103
+ info(`Installed ${installedName}`);
104
+ }
105
+
106
+ main().catch((error) => {
107
+ fail(error.message);
108
+ });
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+
3
+ const TARGETS = new Set([
4
+ "darwin-arm64",
5
+ "linux-x64",
6
+ "win32-x64"
7
+ ]);
8
+
9
+ function targetTriple(platform = process.platform, arch = process.arch) {
10
+ if (!TARGETS.has(`${platform}-${arch}`)) {
11
+ return null;
12
+ }
13
+ return { platform, arch };
14
+ }
15
+
16
+ function binaryFileName(target) {
17
+ const ext = target.platform === "win32" ? ".exe" : "";
18
+ return `sai-${target.platform}-${target.arch}${ext}`;
19
+ }
20
+
21
+ module.exports = {
22
+ binaryFileName,
23
+ targetTriple
24
+ };