@chro-ai/cli 0.1.0 → 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 CHANGED
@@ -1,26 +1,240 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
 
4
- const { spawn } = require("child_process");
5
- const path = require("path");
4
+ const { execSync, spawn } = require("child_process");
5
+ const crypto = require("crypto");
6
6
  const fs = require("fs");
7
- const { getBinaryName } = require("./platform");
7
+ const https = require("https");
8
+ const http = require("http");
9
+ const path = require("path");
10
+ const os = require("os");
11
+ const {
12
+ assertPublicUrlConfigured,
13
+ getPlatformDir,
14
+ getBinaryName,
15
+ R2_PUBLIC_URL,
16
+ } = require("./platform");
17
+
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";
22
+
23
+ function fetchJson(url) {
24
+ return new Promise((resolve, reject) => {
25
+ const client = url.startsWith("https") ? https : http;
26
+ client
27
+ .get(url, (res) => {
28
+ if (
29
+ res.statusCode >= 300 &&
30
+ res.statusCode < 400 &&
31
+ res.headers.location
32
+ ) {
33
+ return fetchJson(res.headers.location).then(resolve, reject);
34
+ }
35
+ if (res.statusCode !== 200) {
36
+ return reject(new Error(`HTTP ${res.statusCode} fetching ${url}`));
37
+ }
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
+ });
47
+ })
48
+ .on("error", reject);
49
+ });
50
+ }
51
+
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
+ }
137
+
138
+ assertPublicUrlConfigured();
139
+
140
+ const version = require("../package.json").version;
141
+ const tag = `v${version}`;
142
+ const cacheDir = path.join(CACHE_DIR, tag, platformDir);
143
+ const zipPath = path.join(cacheDir, `${binaryName}.zip`);
8
144
 
9
- const binName = getBinaryName("chro");
10
- const localBinary = path.resolve(__dirname, "..", "..", "target", "release", binName);
145
+ if (fs.existsSync(zipPath)) return zipPath;
11
146
 
12
- if (!fs.existsSync(localBinary)) {
13
- console.error("chro binary not found.");
14
- console.error("Build it first with: cd apps/cli && cargo build --release");
15
- console.error("This wrapper is scaffold-only; remote download is not wired yet.");
16
- process.exit(1);
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;
17
167
  }
18
168
 
169
+ function extractAndRun(zipPath, extractDir, baseName, args) {
170
+ const binName = getBinaryName(baseName);
171
+ const binPath = path.join(extractDir, binName);
172
+
173
+ if (fs.existsSync(binPath)) {
174
+ try {
175
+ fs.unlinkSync(binPath);
176
+ } catch (err) {
177
+ if (process.env.CHRO_DEBUG) {
178
+ console.warn(`Warning: Could not delete existing binary: ${err.message}`);
179
+ }
180
+ }
181
+ }
182
+
183
+ if (!fs.existsSync(zipPath)) {
184
+ console.error(`${baseName}.zip not found at: ${zipPath}`);
185
+ process.exit(1);
186
+ }
187
+
188
+ const unzipCmd =
189
+ process.platform === "win32"
190
+ ? `powershell -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${extractDir}' -Force"`
191
+ : `unzip -qq -o "${zipPath}" -d "${extractDir}"`;
192
+
193
+ try {
194
+ execSync(unzipCmd, { stdio: "inherit" });
195
+ } catch (err) {
196
+ console.error("Extraction failed:", err.message);
197
+ process.exit(1);
198
+ }
199
+
200
+ if (!fs.existsSync(binPath)) {
201
+ console.error(`Extracted binary not found at: ${binPath}`);
202
+ console.error("This usually indicates a corrupt download. Please reinstall.");
203
+ process.exit(1);
204
+ }
205
+
206
+ if (process.platform !== "win32") {
207
+ try {
208
+ fs.chmodSync(binPath, 0o755);
209
+ } catch {}
210
+ }
211
+
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
+ }
223
+ }
224
+
225
+ const cliVersion = require("../package.json").version;
226
+ console.log(`Starting chro v${cliVersion}...`);
227
+
228
+ const platformDir = getPlatformDir();
19
229
  const args = process.argv.slice(2);
20
- const proc = spawn(localBinary, args, { stdio: "inherit" });
21
230
 
22
- proc.on("exit", (code) => process.exit(code || 0));
23
- proc.on("error", (error) => {
24
- console.error("failed to launch chro:", error.message);
25
- process.exit(1);
26
- });
231
+ ensureBinaryZip(platformDir, "chro-server")
232
+ .then((zipPath) => {
233
+ const extractDir = path.dirname(zipPath);
234
+ extractAndRun(zipPath, extractDir, "chro-server", args);
235
+ })
236
+ .catch((error) => {
237
+ console.error(`Failed to obtain chro binary: ${error.message}`);
238
+ console.error("Please check your network connection and try again.");
239
+ process.exit(1);
240
+ });
package/bin/platform.js CHANGED
@@ -3,8 +3,16 @@
3
3
 
4
4
  const { execSync } = require("child_process");
5
5
 
6
- // Public binary host URL placeholder for future releases.
7
- const R2_PUBLIC_URL = "https://example.com/chro";
6
+ const packageJson = require("../package.json");
7
+
8
+ function normalizePublicUrl(value) {
9
+ if (typeof value !== "string") return "";
10
+ return value.trim().replace(/\/+$/, "");
11
+ }
12
+
13
+ const R2_PUBLIC_URL = normalizePublicUrl(
14
+ process.env.R2_PUBLIC_URL || packageJson.config?.r2PublicUrl,
15
+ );
8
16
 
9
17
  function getEffectiveArch() {
10
18
  const platform = process.platform;
@@ -59,4 +67,20 @@ function getBinaryName(base) {
59
67
  return process.platform === "win32" ? `${base}.exe` : base;
60
68
  }
61
69
 
62
- module.exports = { getEffectiveArch, getPlatformDir, getBinaryName, R2_PUBLIC_URL };
70
+ function assertPublicUrlConfigured() {
71
+ if (R2_PUBLIC_URL.length > 0) return;
72
+
73
+ console.error("chro: R2 public URL is not configured.");
74
+ console.error(
75
+ "Set R2_PUBLIC_URL or populate package.json config.r2PublicUrl before publishing.",
76
+ );
77
+ process.exit(1);
78
+ }
79
+
80
+ module.exports = {
81
+ assertPublicUrlConfigured,
82
+ getEffectiveArch,
83
+ getPlatformDir,
84
+ getBinaryName,
85
+ R2_PUBLIC_URL,
86
+ };
package/package.json CHANGED
@@ -1,25 +1,29 @@
1
1
  {
2
2
  "name": "@chro-ai/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "Chro CLI",
5
- "license": "MIT",
5
+ "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "https://github.com/n-asuy/chro"
9
9
  },
10
10
  "homepage": "https://chro-ai.com",
11
+ "config": {
12
+ "r2PublicUrl": "https://dl.chro-ai.com"
13
+ },
11
14
  "bin": {
12
15
  "chro": "bin/cli.js"
13
16
  },
14
- "scripts": {
15
- "postinstall": "node bin/postinstall.js"
16
- },
17
+ "scripts": {},
17
18
  "files": [
18
19
  "bin"
19
20
  ],
20
21
  "engines": {
21
22
  "node": ">=18"
22
23
  },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
23
27
  "keywords": [
24
28
  "cli",
25
29
  "skills",
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
-
4
- console.log("chro: scaffold package installed");
5
- console.log("chro: remote binary download is not wired yet");