@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 +43 -0
- package/bin/sai.js +33 -0
- package/package.json +36 -0
- package/scripts/install.js +108 -0
- package/scripts/platform.js +24 -0
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
|
+
};
|