chainlesschain 0.37.7 → 0.37.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chainlesschain",
3
- "version": "0.37.7",
3
+ "version": "0.37.8",
4
4
  "description": "CLI for ChainlessChain - install, configure, and manage your personal AI management system",
5
5
  "type": "module",
6
6
  "bin": {
package/src/constants.js CHANGED
@@ -69,14 +69,7 @@ export const EDITIONS = {
69
69
  },
70
70
  };
71
71
 
72
- export const BINARY_NAMES = {
73
- win32: { x64: "chainlesschain-desktop-vue-{version}-win-x64.exe" },
74
- darwin: {
75
- x64: "chainlesschain-desktop-vue-{version}-mac-x64.dmg",
76
- arm64: "chainlesschain-desktop-vue-{version}-mac-arm64.dmg",
77
- },
78
- linux: { x64: "chainlesschain-desktop-vue-{version}-linux-x64.deb" },
79
- };
72
+ // Asset matching is now done by platform patterns in downloader.js
80
73
 
81
74
  export const CONFIG_DIR_NAME = ".chainlesschain";
82
75
 
@@ -1,30 +1,48 @@
1
- import { createWriteStream, existsSync, unlinkSync } from "node:fs";
1
+ import {
2
+ createWriteStream,
3
+ existsSync,
4
+ unlinkSync,
5
+ readdirSync,
6
+ chmodSync,
7
+ } from "node:fs";
2
8
  import { join } from "node:path";
3
9
  import { pipeline } from "node:stream/promises";
4
10
  import { Readable } from "node:stream";
11
+ import { execSync } from "node:child_process";
5
12
  import ora from "ora";
6
13
  import { GITHUB_RELEASES_URL } from "../constants.js";
7
14
  import { getBinDir, ensureDir } from "./paths.js";
8
- import { getBinaryName } from "./platform.js";
9
- import { verifySha256 } from "./checksum.js";
15
+ import { getPlatform, getArch } from "./platform.js";
10
16
  import logger from "./logger.js";
11
17
 
18
+ // Platform-specific asset matching patterns
19
+ const ASSET_PATTERNS = {
20
+ win32: { x64: /chainlesschain.*win32.*x64.*\.zip$/i },
21
+ darwin: {
22
+ x64: /chainlesschain.*darwin.*x64.*\.zip$/i,
23
+ arm64: /chainlesschain.*darwin.*arm64.*\.zip$/i,
24
+ },
25
+ linux: { x64: /chainlesschain.*linux.*x64.*\.zip$/i },
26
+ };
27
+
12
28
  export async function downloadRelease(version, options = {}) {
13
- const binaryName = getBinaryName(version);
14
29
  const binDir = ensureDir(getBinDir());
15
- const destPath = join(binDir, binaryName);
16
30
 
17
- if (existsSync(destPath) && !options.force) {
18
- logger.info(`Binary already exists: ${binaryName}`);
19
- return destPath;
31
+ // Check if we already have an extracted app
32
+ if (!options.force && hasExtractedApp(binDir)) {
33
+ logger.info("Application already installed");
34
+ return binDir;
20
35
  }
21
36
 
22
- const releaseUrl =
23
- options.url || (await resolveAssetUrl(version, binaryName));
24
- const spinner = ora(`Downloading ${binaryName}...`).start();
37
+ const { url: assetUrl, name: assetName } = options.url
38
+ ? { url: options.url, name: "download.zip" }
39
+ : await resolveAssetUrl(version);
40
+
41
+ const destPath = join(binDir, assetName);
42
+ const spinner = ora(`Downloading ${assetName}...`).start();
25
43
 
26
44
  try {
27
- const response = await fetch(releaseUrl, {
45
+ const response = await fetch(assetUrl, {
28
46
  headers: { Accept: "application/octet-stream" },
29
47
  redirect: "follow",
30
48
  });
@@ -52,7 +70,7 @@ export async function downloadRelease(version, options = {}) {
52
70
  downloadedBytes += value.byteLength;
53
71
  if (totalBytes > 0) {
54
72
  const pct = ((downloadedBytes / totalBytes) * 100).toFixed(1);
55
- spinner.text = `Downloading ${binaryName}... ${pct}% (${formatBytes(downloadedBytes)}/${formatBytes(totalBytes)})`;
73
+ spinner.text = `Downloading ${assetName}... ${pct}% (${formatBytes(downloadedBytes)}/${formatBytes(totalBytes)})`;
56
74
  }
57
75
  controller.enqueue(value);
58
76
  },
@@ -62,23 +80,23 @@ export async function downloadRelease(version, options = {}) {
62
80
  await pipeline(nodeStream, createWriteStream(destPath));
63
81
 
64
82
  spinner.succeed(
65
- `Downloaded ${binaryName} (${formatBytes(downloadedBytes)})`,
83
+ `Downloaded ${assetName} (${formatBytes(downloadedBytes)})`,
66
84
  );
67
85
 
68
- if (options.checksum) {
69
- const verifySpinner = ora("Verifying checksum...").start();
70
- const result = await verifySha256(destPath, options.checksum);
71
- if (!result.valid) {
86
+ // Extract zip
87
+ if (destPath.endsWith(".zip")) {
88
+ const extractSpinner = ora("Extracting...").start();
89
+ try {
90
+ extractZip(destPath, binDir);
72
91
  unlinkSync(destPath);
73
- verifySpinner.fail("Checksum verification failed");
74
- throw new Error(
75
- `SHA256 mismatch: expected ${result.expected}, got ${result.actual}`,
76
- );
92
+ extractSpinner.succeed("Extracted and ready");
93
+ } catch (err) {
94
+ extractSpinner.fail(`Extraction failed: ${err.message}`);
95
+ throw err;
77
96
  }
78
- verifySpinner.succeed("Checksum verified");
79
97
  }
80
98
 
81
- return destPath;
99
+ return binDir;
82
100
  } catch (err) {
83
101
  spinner.fail(`Download failed: ${err.message}`);
84
102
  if (existsSync(destPath)) {
@@ -88,7 +106,7 @@ export async function downloadRelease(version, options = {}) {
88
106
  }
89
107
  }
90
108
 
91
- async function resolveAssetUrl(version, binaryName) {
109
+ async function resolveAssetUrl(version) {
92
110
  // Try exact version first, then fall back to latest release
93
111
  const tagName = `v${version}`;
94
112
  let release = await fetchRelease(`${GITHUB_RELEASES_URL}/tags/${tagName}`);
@@ -99,24 +117,32 @@ async function resolveAssetUrl(version, binaryName) {
99
117
  }
100
118
 
101
119
  if (!release) {
102
- throw new Error(`No releases found for ${GITHUB_RELEASES_URL}`);
120
+ throw new Error("No releases found on GitHub");
103
121
  }
104
122
 
105
- // Re-resolve binary name with the actual release version
106
- const actualVersion = release.tag_name.replace(/^v/, "");
107
- const actualBinaryName = binaryName.replace(version, actualVersion);
123
+ logger.info(`Using release ${release.tag_name}`);
108
124
 
109
- const asset =
110
- release.assets.find((a) => a.name === actualBinaryName) ||
111
- release.assets.find((a) => a.name === binaryName);
125
+ // Match asset by platform/arch pattern
126
+ const p = getPlatform();
127
+ const a = getArch();
128
+ const patterns = ASSET_PATTERNS[p];
129
+ if (!patterns) {
130
+ throw new Error(`Unsupported platform: ${p}`);
131
+ }
132
+ const pattern = patterns[a];
133
+ if (!pattern) {
134
+ throw new Error(`Unsupported architecture: ${a} on ${p}`);
135
+ }
136
+
137
+ const asset = release.assets.find((ast) => pattern.test(ast.name));
112
138
  if (!asset) {
113
- const available = release.assets.map((a) => a.name).join(", ");
139
+ const available = release.assets.map((ast) => ast.name).join(", ");
114
140
  throw new Error(
115
- `No matching asset in release ${release.tag_name}. Available: ${available}`,
141
+ `No matching asset for ${p}/${a} in release ${release.tag_name}. Available: ${available}`,
116
142
  );
117
143
  }
118
144
 
119
- return asset.browser_download_url;
145
+ return { url: asset.browser_download_url, name: asset.name };
120
146
  }
121
147
 
122
148
  async function fetchRelease(url) {
@@ -127,6 +153,36 @@ async function fetchRelease(url) {
127
153
  return response.json();
128
154
  }
129
155
 
156
+ function extractZip(zipPath, destDir) {
157
+ const p = getPlatform();
158
+ if (p === "win32") {
159
+ // Use PowerShell on Windows
160
+ execSync(
161
+ `powershell -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force"`,
162
+ { stdio: "ignore" },
163
+ );
164
+ } else {
165
+ execSync(`unzip -o "${zipPath}" -d "${destDir}"`, { stdio: "ignore" });
166
+ }
167
+ }
168
+
169
+ function hasExtractedApp(binDir) {
170
+ if (!existsSync(binDir)) return false;
171
+ try {
172
+ const files = readdirSync(binDir, { recursive: true });
173
+ return files.some(
174
+ (f) =>
175
+ (typeof f === "string" ? f : f.toString())
176
+ .toLowerCase()
177
+ .includes("chainlesschain") &&
178
+ ((typeof f === "string" ? f : f.toString()).endsWith(".exe") ||
179
+ !(typeof f === "string" ? f : f.toString()).includes(".")),
180
+ );
181
+ } catch {
182
+ return false;
183
+ }
184
+ }
185
+
130
186
  function formatBytes(bytes) {
131
187
  if (bytes < 1024) return `${bytes}B`;
132
188
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
@@ -1,5 +1,4 @@
1
1
  import { platform, arch } from "node:os";
2
- import { BINARY_NAMES } from "../constants.js";
3
2
 
4
3
  export function getPlatform() {
5
4
  return platform();
@@ -12,34 +11,6 @@ export function getArch() {
12
11
  return a;
13
12
  }
14
13
 
15
- export function getBinaryName(version) {
16
- const p = getPlatform();
17
- const a = getArch();
18
- const platformBinaries = BINARY_NAMES[p];
19
- if (!platformBinaries) {
20
- throw new Error(`Unsupported platform: ${p}`);
21
- }
22
- const template = platformBinaries[a];
23
- if (!template) {
24
- throw new Error(`Unsupported architecture: ${a} on ${p}`);
25
- }
26
- return template.replace("{version}", version);
27
- }
28
-
29
- export function getBinaryExtension() {
30
- const p = getPlatform();
31
- switch (p) {
32
- case "win32":
33
- return ".exe";
34
- case "darwin":
35
- return ".dmg";
36
- case "linux":
37
- return ".deb";
38
- default:
39
- return "";
40
- }
41
- }
42
-
43
14
  export function isWindows() {
44
15
  return getPlatform() === "win32";
45
16
  }
@@ -110,19 +110,40 @@ export function getAppPid() {
110
110
  function findExecutable(binDir, extension) {
111
111
  if (!existsSync(binDir)) return null;
112
112
  try {
113
- const files = readdirSync(binDir);
114
- const match = files.find((f) => {
115
- const isChainless = f.toLowerCase().includes("chainlesschain");
116
- if (extension) return isChainless && f.endsWith(extension);
117
- return (
118
- isChainless &&
119
- !f.endsWith(".dmg") &&
120
- !f.endsWith(".deb") &&
121
- !f.endsWith(".sha256")
122
- );
123
- });
124
- return match ? join(binDir, match) : null;
113
+ // Search recursively for the executable
114
+ const results = [];
115
+ searchDir(binDir, extension, results);
116
+ return results.length > 0 ? results[0] : null;
125
117
  } catch {
126
118
  return null;
127
119
  }
128
120
  }
121
+
122
+ function searchDir(dir, extension, results) {
123
+ const entries = readdirSync(dir, { withFileTypes: true });
124
+ for (const entry of entries) {
125
+ const fullPath = join(dir, entry.name);
126
+ if (entry.isDirectory()) {
127
+ searchDir(fullPath, extension, results);
128
+ } else {
129
+ const name = entry.name.toLowerCase();
130
+ const isChainless = name.includes("chainlesschain");
131
+ if (!isChainless) continue;
132
+ if (extension && name.endsWith(extension)) {
133
+ results.push(fullPath);
134
+ } else if (
135
+ !extension &&
136
+ !name.endsWith(".dmg") &&
137
+ !name.endsWith(".deb") &&
138
+ !name.endsWith(".zip") &&
139
+ !name.endsWith(".sha256") &&
140
+ !name.endsWith(".dll") &&
141
+ !name.endsWith(".dat") &&
142
+ !name.endsWith(".pak") &&
143
+ !name.endsWith(".json")
144
+ ) {
145
+ results.push(fullPath);
146
+ }
147
+ }
148
+ }
149
+ }