@chro-ai/cli 0.1.1 → 0.1.3
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/cli.js +147 -47
- package/bin/platform.js +1 -6
- package/package.json +2 -4
- package/bin/postinstall.js +0 -81
package/bin/cli.js
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
const { execSync, spawn } = require("child_process");
|
|
5
|
-
const
|
|
5
|
+
const crypto = require("crypto");
|
|
6
6
|
const fs = require("fs");
|
|
7
7
|
const https = require("https");
|
|
8
8
|
const http = require("http");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const os = require("os");
|
|
9
11
|
const {
|
|
10
12
|
assertPublicUrlConfigured,
|
|
11
13
|
getPlatformDir,
|
|
@@ -13,12 +15,12 @@ const {
|
|
|
13
15
|
R2_PUBLIC_URL,
|
|
14
16
|
} = require("./platform");
|
|
15
17
|
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
fs.
|
|
18
|
+
const CACHE_DIR = path.join(os.homedir(), ".chro", "bin");
|
|
19
|
+
const LOCAL_DIST_DIR = path.join(__dirname, "..", "dist");
|
|
20
|
+
const LOCAL_DEV_MODE =
|
|
21
|
+
fs.existsSync(LOCAL_DIST_DIR) || process.env.CHRO_LOCAL === "1";
|
|
20
22
|
|
|
21
|
-
function
|
|
23
|
+
function fetchJson(url) {
|
|
22
24
|
return new Promise((resolve, reject) => {
|
|
23
25
|
const client = url.startsWith("https") ? https : http;
|
|
24
26
|
client
|
|
@@ -28,46 +30,145 @@ function download(url) {
|
|
|
28
30
|
res.statusCode < 400 &&
|
|
29
31
|
res.headers.location
|
|
30
32
|
) {
|
|
31
|
-
return
|
|
33
|
+
return fetchJson(res.headers.location).then(resolve, reject);
|
|
32
34
|
}
|
|
33
35
|
if (res.statusCode !== 200) {
|
|
34
|
-
reject(new Error(`HTTP ${res.statusCode}
|
|
35
|
-
res.resume();
|
|
36
|
-
return;
|
|
36
|
+
return reject(new Error(`HTTP ${res.statusCode} fetching ${url}`));
|
|
37
37
|
}
|
|
38
|
-
|
|
38
|
+
let data = "";
|
|
39
|
+
res.on("data", (chunk) => (data += chunk));
|
|
40
|
+
res.on("end", () => {
|
|
41
|
+
try {
|
|
42
|
+
resolve(JSON.parse(data));
|
|
43
|
+
} catch {
|
|
44
|
+
reject(new Error(`Failed to parse JSON from ${url}`));
|
|
45
|
+
}
|
|
46
|
+
});
|
|
39
47
|
})
|
|
40
48
|
.on("error", reject);
|
|
41
49
|
});
|
|
42
50
|
}
|
|
43
51
|
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
52
|
+
function downloadFile(url, destPath, expectedSha256) {
|
|
53
|
+
const tempPath = destPath + ".tmp";
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const file = fs.createWriteStream(tempPath);
|
|
56
|
+
const hash = crypto.createHash("sha256");
|
|
57
|
+
|
|
58
|
+
const cleanup = () => {
|
|
59
|
+
try {
|
|
60
|
+
fs.unlinkSync(tempPath);
|
|
61
|
+
} catch {}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const client = url.startsWith("https") ? https : http;
|
|
65
|
+
client
|
|
66
|
+
.get(url, (res) => {
|
|
67
|
+
if (
|
|
68
|
+
res.statusCode >= 300 &&
|
|
69
|
+
res.statusCode < 400 &&
|
|
70
|
+
res.headers.location
|
|
71
|
+
) {
|
|
72
|
+
file.close();
|
|
73
|
+
cleanup();
|
|
74
|
+
return downloadFile(res.headers.location, destPath, expectedSha256)
|
|
75
|
+
.then(resolve)
|
|
76
|
+
.catch(reject);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (res.statusCode !== 200) {
|
|
80
|
+
file.close();
|
|
81
|
+
cleanup();
|
|
82
|
+
return reject(
|
|
83
|
+
new Error(`HTTP ${res.statusCode} downloading ${url}`),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
res.on("data", (chunk) => {
|
|
88
|
+
hash.update(chunk);
|
|
89
|
+
});
|
|
90
|
+
res.pipe(file);
|
|
91
|
+
|
|
92
|
+
file.on("finish", () => {
|
|
93
|
+
file.close();
|
|
94
|
+
const actualSha256 = hash.digest("hex");
|
|
95
|
+
if (expectedSha256 && actualSha256 !== expectedSha256) {
|
|
96
|
+
cleanup();
|
|
97
|
+
reject(
|
|
98
|
+
new Error(
|
|
99
|
+
`Checksum mismatch: expected ${expectedSha256}, got ${actualSha256}`,
|
|
100
|
+
),
|
|
101
|
+
);
|
|
102
|
+
} else {
|
|
103
|
+
try {
|
|
104
|
+
fs.renameSync(tempPath, destPath);
|
|
105
|
+
resolve(destPath);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
cleanup();
|
|
108
|
+
reject(err);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
})
|
|
113
|
+
.on("error", (err) => {
|
|
114
|
+
file.close();
|
|
115
|
+
cleanup();
|
|
116
|
+
reject(err);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function ensureBinaryZip(platformDir, binaryName) {
|
|
122
|
+
// Local dev mode: use binaries from npx-cli/dist/
|
|
123
|
+
if (LOCAL_DEV_MODE) {
|
|
124
|
+
const localZipPath = path.join(
|
|
125
|
+
LOCAL_DIST_DIR,
|
|
126
|
+
platformDir,
|
|
127
|
+
`${binaryName}.zip`,
|
|
128
|
+
);
|
|
129
|
+
if (fs.existsSync(localZipPath)) {
|
|
130
|
+
return localZipPath;
|
|
131
|
+
}
|
|
132
|
+
throw new Error(
|
|
133
|
+
`Local binary not found: ${localZipPath}\n` +
|
|
134
|
+
"Run ./local-build.sh first to build the binaries.",
|
|
135
|
+
);
|
|
136
|
+
}
|
|
47
137
|
|
|
48
138
|
assertPublicUrlConfigured();
|
|
49
139
|
|
|
50
140
|
const version = require("../package.json").version;
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
141
|
+
const tag = `v${version}`;
|
|
142
|
+
const cacheDir = path.join(CACHE_DIR, tag, platformDir);
|
|
143
|
+
const zipPath = path.join(cacheDir, `${binaryName}.zip`);
|
|
144
|
+
|
|
145
|
+
if (fs.existsSync(zipPath)) return zipPath;
|
|
146
|
+
|
|
147
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
148
|
+
|
|
149
|
+
console.log("Fetching binary manifest...");
|
|
150
|
+
const manifest = await fetchJson(
|
|
151
|
+
`${R2_PUBLIC_URL}/releases/${tag}/manifest.json`,
|
|
152
|
+
);
|
|
153
|
+
const binaryInfo = manifest.platforms?.[platformDir]?.[binaryName];
|
|
154
|
+
|
|
155
|
+
if (!binaryInfo) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`Binary ${binaryName} not available for ${platformDir}`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const url = `${R2_PUBLIC_URL}/releases/${tag}/${platformDir}/${binaryName}.zip`;
|
|
162
|
+
console.log(`Downloading binary for ${platformDir}...`);
|
|
163
|
+
await downloadFile(url, zipPath, binaryInfo.sha256);
|
|
164
|
+
console.log("Download complete. Checksum verified.");
|
|
165
|
+
|
|
166
|
+
return zipPath;
|
|
65
167
|
}
|
|
66
168
|
|
|
67
|
-
function extractAndRun(baseName,
|
|
169
|
+
function extractAndRun(zipPath, extractDir, baseName, args) {
|
|
68
170
|
const binName = getBinaryName(baseName);
|
|
69
171
|
const binPath = path.join(extractDir, binName);
|
|
70
|
-
const zipPath = path.join(extractDir, `${baseName}.zip`);
|
|
71
172
|
|
|
72
173
|
if (fs.existsSync(binPath)) {
|
|
73
174
|
try {
|
|
@@ -81,7 +182,6 @@ function extractAndRun(baseName, launch) {
|
|
|
81
182
|
|
|
82
183
|
if (!fs.existsSync(zipPath)) {
|
|
83
184
|
console.error(`${baseName}.zip not found at: ${zipPath}`);
|
|
84
|
-
console.error(`Current platform: ${platformDir}`);
|
|
85
185
|
process.exit(1);
|
|
86
186
|
}
|
|
87
187
|
|
|
@@ -109,29 +209,29 @@ function extractAndRun(baseName, launch) {
|
|
|
109
209
|
} catch {}
|
|
110
210
|
}
|
|
111
211
|
|
|
112
|
-
|
|
212
|
+
const proc = spawn(binPath, args, { stdio: "inherit" });
|
|
213
|
+
proc.on("exit", (code) => process.exit(code || 0));
|
|
214
|
+
proc.on("error", (error) => {
|
|
215
|
+
console.error("Failed to launch chro-server:", error.message);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
if (process.platform !== "win32") {
|
|
220
|
+
process.on("SIGINT", () => proc.kill("SIGINT"));
|
|
221
|
+
process.on("SIGTERM", () => proc.kill("SIGTERM"));
|
|
222
|
+
}
|
|
113
223
|
}
|
|
114
224
|
|
|
115
225
|
const cliVersion = require("../package.json").version;
|
|
116
226
|
console.log(`Starting chro v${cliVersion}...`);
|
|
117
227
|
|
|
228
|
+
const platformDir = getPlatformDir();
|
|
118
229
|
const args = process.argv.slice(2);
|
|
119
230
|
|
|
120
|
-
ensureBinaryZip("chro-server")
|
|
121
|
-
.then(() => {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
proc.on("exit", (code) => process.exit(code || 0));
|
|
125
|
-
proc.on("error", (error) => {
|
|
126
|
-
console.error("failed to launch chro-server:", error.message);
|
|
127
|
-
process.exit(1);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
if (process.platform !== "win32") {
|
|
131
|
-
process.on("SIGINT", () => proc.kill("SIGINT"));
|
|
132
|
-
process.on("SIGTERM", () => proc.kill("SIGTERM"));
|
|
133
|
-
}
|
|
134
|
-
});
|
|
231
|
+
ensureBinaryZip(platformDir, "chro-server")
|
|
232
|
+
.then((zipPath) => {
|
|
233
|
+
const extractDir = path.dirname(zipPath);
|
|
234
|
+
extractAndRun(zipPath, extractDir, "chro-server", args);
|
|
135
235
|
})
|
|
136
236
|
.catch((error) => {
|
|
137
237
|
console.error(`Failed to obtain chro binary: ${error.message}`);
|
package/bin/platform.js
CHANGED
|
@@ -67,12 +67,8 @@ function getBinaryName(base) {
|
|
|
67
67
|
return process.platform === "win32" ? `${base}.exe` : base;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
function hasConfiguredPublicUrl() {
|
|
71
|
-
return R2_PUBLIC_URL.length > 0;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
70
|
function assertPublicUrlConfigured() {
|
|
75
|
-
if (
|
|
71
|
+
if (R2_PUBLIC_URL.length > 0) return;
|
|
76
72
|
|
|
77
73
|
console.error("chro: R2 public URL is not configured.");
|
|
78
74
|
console.error(
|
|
@@ -86,6 +82,5 @@ module.exports = {
|
|
|
86
82
|
getEffectiveArch,
|
|
87
83
|
getPlatformDir,
|
|
88
84
|
getBinaryName,
|
|
89
|
-
hasConfiguredPublicUrl,
|
|
90
85
|
R2_PUBLIC_URL,
|
|
91
86
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chro-ai/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Chro CLI",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -14,9 +14,7 @@
|
|
|
14
14
|
"bin": {
|
|
15
15
|
"chro": "bin/cli.js"
|
|
16
16
|
},
|
|
17
|
-
"scripts": {
|
|
18
|
-
"postinstall": "node bin/postinstall.js"
|
|
19
|
-
},
|
|
17
|
+
"scripts": {},
|
|
20
18
|
"files": [
|
|
21
19
|
"bin"
|
|
22
20
|
],
|
package/bin/postinstall.js
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
|
-
|
|
4
|
-
const https = require("https");
|
|
5
|
-
const http = require("http");
|
|
6
|
-
const fs = require("fs");
|
|
7
|
-
const path = require("path");
|
|
8
|
-
const {
|
|
9
|
-
getPlatformDir,
|
|
10
|
-
hasConfiguredPublicUrl,
|
|
11
|
-
R2_PUBLIC_URL,
|
|
12
|
-
} = require("./platform");
|
|
13
|
-
|
|
14
|
-
const version = require("../package.json").version;
|
|
15
|
-
const platformDir = getPlatformDir();
|
|
16
|
-
const distDir = path.join(__dirname, "..", "dist", platformDir);
|
|
17
|
-
const zipPath = path.join(distDir, "chro-server.zip");
|
|
18
|
-
|
|
19
|
-
function download(url) {
|
|
20
|
-
return new Promise((resolve, reject) => {
|
|
21
|
-
const client = url.startsWith("https") ? https : http;
|
|
22
|
-
client
|
|
23
|
-
.get(url, (res) => {
|
|
24
|
-
if (
|
|
25
|
-
res.statusCode >= 300 &&
|
|
26
|
-
res.statusCode < 400 &&
|
|
27
|
-
res.headers.location
|
|
28
|
-
) {
|
|
29
|
-
return download(res.headers.location).then(resolve, reject);
|
|
30
|
-
}
|
|
31
|
-
if (res.statusCode !== 200) {
|
|
32
|
-
reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
|
33
|
-
res.resume();
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
resolve(res);
|
|
37
|
-
})
|
|
38
|
-
.on("error", reject);
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async function main() {
|
|
43
|
-
if (!hasConfiguredPublicUrl()) {
|
|
44
|
-
console.warn("chro: R2 public URL is not configured");
|
|
45
|
-
console.warn("chro: binary will be downloaded on first run if R2_PUBLIC_URL is set");
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (fs.existsSync(zipPath)) {
|
|
50
|
-
console.log(`chro: binary already present at ${zipPath}, skipping download`);
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const url = `${R2_PUBLIC_URL}/releases/v${version}/${platformDir}/chro-server.zip`;
|
|
55
|
-
console.log(`chro: downloading binary for ${platformDir}...`);
|
|
56
|
-
|
|
57
|
-
try {
|
|
58
|
-
fs.mkdirSync(distDir, { recursive: true });
|
|
59
|
-
const res = await download(url);
|
|
60
|
-
const file = fs.createWriteStream(zipPath);
|
|
61
|
-
|
|
62
|
-
await new Promise((resolve, reject) => {
|
|
63
|
-
res.pipe(file);
|
|
64
|
-
file.on("finish", () => file.close(resolve));
|
|
65
|
-
file.on("error", (err) => {
|
|
66
|
-
fs.unlink(zipPath, () => {});
|
|
67
|
-
reject(err);
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
console.log("chro: binary downloaded successfully");
|
|
72
|
-
} catch (err) {
|
|
73
|
-
console.warn(`chro: postinstall download failed: ${err.message}`);
|
|
74
|
-
console.warn("chro: binary will be downloaded on first run");
|
|
75
|
-
try {
|
|
76
|
-
fs.unlinkSync(zipPath);
|
|
77
|
-
} catch {}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
main();
|