@chro-ai/cli 0.1.1 → 0.1.4

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 CHANGED
@@ -2,10 +2,12 @@
2
2
  "use strict";
3
3
 
4
4
  const { execSync, spawn } = require("child_process");
5
- const path = require("path");
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 platformDir = getPlatformDir();
17
- const extractDir = path.join(__dirname, "..", "dist", platformDir);
18
-
19
- fs.mkdirSync(extractDir, { recursive: true });
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 download(url) {
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 download(res.headers.location).then(resolve, reject);
33
+ return fetchJson(res.headers.location).then(resolve, reject);
32
34
  }
33
35
  if (res.statusCode !== 200) {
34
- reject(new Error(`HTTP ${res.statusCode} for ${url}`));
35
- res.resume();
36
- return;
36
+ return reject(new Error(`HTTP ${res.statusCode} fetching ${url}`));
37
37
  }
38
- resolve(res);
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
- async function ensureBinaryZip(baseName) {
45
- const zipPath = path.join(extractDir, `${baseName}.zip`);
46
- if (fs.existsSync(zipPath)) return;
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 url = `${R2_PUBLIC_URL}/releases/v${version}/${platformDir}/${baseName}.zip`;
52
- console.log("Binary not found locally. Downloading from R2...");
53
-
54
- const res = await download(url);
55
- const file = fs.createWriteStream(zipPath);
56
- await new Promise((resolve, reject) => {
57
- res.pipe(file);
58
- file.on("finish", () => file.close(resolve));
59
- file.on("error", (err) => {
60
- fs.unlink(zipPath, () => {});
61
- reject(err);
62
- });
63
- });
64
- console.log("Download complete.");
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, launch) {
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
- return launch(binPath);
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
- extractAndRun("chro-server", (binPath) => {
123
- const proc = spawn(binPath, args, { stdio: "inherit" });
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 (hasConfiguredPublicUrl()) return;
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.1",
3
+ "version": "0.1.4",
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
  ],
@@ -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();