@conanmontreal/crush 0.0.0-stage → 0.98.1-fork.11
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 +55 -2
- package/install.js +6 -0
- package/lib.js +259 -0
- package/package.json +46 -4
- package/run-crush.js +6 -0
package/README.md
CHANGED
|
@@ -1,3 +1,56 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @conanmontreal/crush
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A fork of [Charm's Crush](https://github.com/charmbracelet/crush), a
|
|
4
|
+
terminal-based AI coding assistant, carrying local patches:
|
|
5
|
+
|
|
6
|
+
- Copilot Responses-API routing can be extended from a user config file
|
|
7
|
+
(`~/.config/crush/copilot-responses-models`) without rebuilding.
|
|
8
|
+
- Mistral: content-block streaming deltas are rewritten into standard
|
|
9
|
+
reasoning/text shapes, reasoning history is replayed in Mistral's thinking
|
|
10
|
+
block format, and requests are signed like the Mistral Vibe CLI so a
|
|
11
|
+
subscription-linked key draws on the Vibe allowance.
|
|
12
|
+
- Test-suite hardening: the binary is built from a tree where `go test` can
|
|
13
|
+
never launch a browser.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g @conanmontreal/crush
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The postinstall step downloads the patched binary for Linux x86_64 from
|
|
22
|
+
the fork's
|
|
23
|
+
[GitHub releases](https://github.com/pbtrudel/crush/releases) and verifies
|
|
24
|
+
its sha256 checksum. Fork builds are Linux-only; on other platforms install
|
|
25
|
+
upstream crush or build the fork from source.
|
|
26
|
+
|
|
27
|
+
Both this fork and upstream provide the same `crush` command, so uninstall the
|
|
28
|
+
upstream package first if it is installed:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm uninstall -g @charmland/crush
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The fork reads the same configuration as upstream (`~/.config/crush/`,
|
|
35
|
+
project `.crush/` directories), so no config changes are needed.
|
|
36
|
+
|
|
37
|
+
## Update
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install -g @conanmontreal/crush@latest
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Every merge to the fork's default branch produces a new version; the pipeline
|
|
44
|
+
stages it on npm and a maintainer approves it with 2FA.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
Maintainer note: `install.js`, `lib.js` and `run-crush.js` are the wrapper
|
|
49
|
+
scripts GoReleaser Pro generates for npm packages (copied verbatim from an
|
|
50
|
+
installed `@charmland/crush` 0.98.1). goreleaser's npm publisher is
|
|
51
|
+
Pro-only, so the `publish npm` workflow packages this directory itself:
|
|
52
|
+
`lib.js` reads the `archives` manifest in `package.json` at install time and
|
|
53
|
+
downloads the archive matching `${process.platform}-${process.arch}` from the
|
|
54
|
+
release URL, verifying the sha256 digest. To refresh the scripts, copy the
|
|
55
|
+
three files out of a newer installed `@charmland/crush` package directory and
|
|
56
|
+
re-run the local end-to-end install test.
|
package/install.js
ADDED
package/lib.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// This file was generated by GoReleaser. DO NOT EDIT.
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import crypto from "crypto";
|
|
4
|
+
import http from "http";
|
|
5
|
+
import https from "https";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import JSZip from "jszip";
|
|
8
|
+
import { x as tarExtract } from "tar";
|
|
9
|
+
import { ProxyAgent } from "proxy-agent";
|
|
10
|
+
import { spawnSync } from "child_process";
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
12
|
+
|
|
13
|
+
const { archives, name, version } = JSON.parse(
|
|
14
|
+
fs.readFileSync(new URL("./package.json", import.meta.url), "utf8"),
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const agent = new ProxyAgent();
|
|
18
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
|
|
20
|
+
const getArchive = () => {
|
|
21
|
+
let target = `${process.platform}-${process.arch}`;
|
|
22
|
+
const archive = archives[target];
|
|
23
|
+
if (!archive) {
|
|
24
|
+
throw new Error(`No archive available for ${target}`);
|
|
25
|
+
}
|
|
26
|
+
return archive;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const binDir = path.join(__dirname, "bin");
|
|
30
|
+
|
|
31
|
+
async function extractTar(tarPath, binaries, dir, wrappedIn) {
|
|
32
|
+
try {
|
|
33
|
+
const filesToExtract = wrappedIn
|
|
34
|
+
? binaries.map((bin) =>
|
|
35
|
+
path.join(wrappedIn, bin).replace(/\\/g, "/"),
|
|
36
|
+
)
|
|
37
|
+
: binaries;
|
|
38
|
+
|
|
39
|
+
await tarExtract({
|
|
40
|
+
file: tarPath,
|
|
41
|
+
cwd: dir,
|
|
42
|
+
filter: (path) => filesToExtract.includes(path),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// If wrapped, move files from wrapped directory to bin directory
|
|
46
|
+
if (wrappedIn) {
|
|
47
|
+
const wrappedDir = path.join(dir, wrappedIn);
|
|
48
|
+
for (const binary of binaries) {
|
|
49
|
+
const srcPath = path.join(wrappedDir, binary);
|
|
50
|
+
const destPath = path.join(dir, binary);
|
|
51
|
+
if (fs.existsSync(srcPath)) {
|
|
52
|
+
fs.renameSync(srcPath, destPath);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Clean up empty wrapped directory
|
|
56
|
+
try {
|
|
57
|
+
fs.rmSync(wrappedDir, { recursive: true, force: true });
|
|
58
|
+
} catch (err) {
|
|
59
|
+
// Ignore cleanup errors
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log(`Successfully extracted ${binaries} to "${dir}"`);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
throw new Error(`Extraction failed: ${err.message}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function extractZip(zipPath, binaries, dir, wrappedIn) {
|
|
70
|
+
try {
|
|
71
|
+
const zipData = fs.readFileSync(zipPath);
|
|
72
|
+
const zip = await JSZip.loadAsync(zipData);
|
|
73
|
+
|
|
74
|
+
for (const binary of binaries) {
|
|
75
|
+
const binaryPath = wrappedIn
|
|
76
|
+
? path.join(wrappedIn, binary).replace(/\\/g, "/")
|
|
77
|
+
: binary;
|
|
78
|
+
|
|
79
|
+
if (!zip.files[binaryPath]) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Error: ${binaryPath} does not exist in ${zipPath}`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const content = await zip.files[binaryPath].async("nodebuffer");
|
|
86
|
+
if (!fs.existsSync(dir)) {
|
|
87
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
88
|
+
}
|
|
89
|
+
const file = path.join(dir, binary);
|
|
90
|
+
fs.writeFileSync(file, content);
|
|
91
|
+
fs.chmodSync(file, "755");
|
|
92
|
+
console.log(`Successfully extracted "${binary}" to "${dir}"`);
|
|
93
|
+
}
|
|
94
|
+
} catch (err) {
|
|
95
|
+
throw new Error(`Extraction failed: ${err.message}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const run = async (bin) => {
|
|
100
|
+
await install();
|
|
101
|
+
if (process.platform === "win32") {
|
|
102
|
+
bin += ".exe";
|
|
103
|
+
}
|
|
104
|
+
const [, , ...args] = process.argv;
|
|
105
|
+
let result = spawnSync(path.join(binDir, bin), args, {
|
|
106
|
+
cwd: process.cwd(),
|
|
107
|
+
stdio: "inherit",
|
|
108
|
+
});
|
|
109
|
+
if (result.error) {
|
|
110
|
+
console.error(result.error);
|
|
111
|
+
}
|
|
112
|
+
return result.status;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const install = async () => {
|
|
116
|
+
try {
|
|
117
|
+
let archive = getArchive();
|
|
118
|
+
if (await exists(archive)) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
let tmp = fs.mkdtempSync("archive-");
|
|
122
|
+
let archivePath = path.join(tmp, archive.name);
|
|
123
|
+
await download(archive.url, archivePath);
|
|
124
|
+
verify(archivePath, archive.checksum);
|
|
125
|
+
|
|
126
|
+
if (!fs.existsSync(binDir)) {
|
|
127
|
+
fs.mkdirSync(binDir);
|
|
128
|
+
}
|
|
129
|
+
switch (archive.format) {
|
|
130
|
+
case "binary":
|
|
131
|
+
const bin = path.join(binDir, archive.bins[0]);
|
|
132
|
+
fs.copyFileSync(archivePath, bin);
|
|
133
|
+
fs.chmodSync(bin, 0o755);
|
|
134
|
+
break;
|
|
135
|
+
case "zip":
|
|
136
|
+
await extractZip(
|
|
137
|
+
archivePath,
|
|
138
|
+
archive.bins,
|
|
139
|
+
binDir,
|
|
140
|
+
archive.wrappedIn,
|
|
141
|
+
);
|
|
142
|
+
break;
|
|
143
|
+
case "tar":
|
|
144
|
+
case "tar.gz":
|
|
145
|
+
case "tgz":
|
|
146
|
+
await extractTar(
|
|
147
|
+
archivePath,
|
|
148
|
+
archive.bins,
|
|
149
|
+
binDir,
|
|
150
|
+
archive.wrappedIn,
|
|
151
|
+
);
|
|
152
|
+
break;
|
|
153
|
+
case "tar.zst":
|
|
154
|
+
case "tzst":
|
|
155
|
+
case "tar.xz":
|
|
156
|
+
case "txz":
|
|
157
|
+
default:
|
|
158
|
+
throw new Error(`unsupported format: ${archive.format}`);
|
|
159
|
+
}
|
|
160
|
+
console.log(`Installed ${name} ${version} to ${binDir}`);
|
|
161
|
+
} catch (err) {
|
|
162
|
+
throw new Error(`Installation failed: ${err.message}`);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const verify = (filename, checksum) => {
|
|
167
|
+
if (checksum.algorithm == "" || checksum.digest == "") {
|
|
168
|
+
console.warn("Warning: No checksum provided for verification");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
let digest = crypto
|
|
172
|
+
.createHash(checksum.algorithm)
|
|
173
|
+
.update(fs.readFileSync(filename))
|
|
174
|
+
.digest("hex");
|
|
175
|
+
if (digest != checksum.digest) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`${filename}: ${checksum.algorithm} does not match, expected ${checksum.digest}, got ${digest}`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const download = async (url, filename, maxRedirects = 10) => {
|
|
183
|
+
try {
|
|
184
|
+
console.log(`Downloading ${url} to ${filename}...`);
|
|
185
|
+
const dir = path.dirname(filename);
|
|
186
|
+
if (!fs.existsSync(dir)) {
|
|
187
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return new Promise((resolve, reject) => {
|
|
191
|
+
const parsedUrl = new URL(url);
|
|
192
|
+
const mod = parsedUrl.protocol === "https:" ? https : http;
|
|
193
|
+
|
|
194
|
+
const request = mod.get(url, { agent }, (response) => {
|
|
195
|
+
if (
|
|
196
|
+
response.statusCode >= 300 &&
|
|
197
|
+
response.statusCode < 400 &&
|
|
198
|
+
response.headers.location
|
|
199
|
+
) {
|
|
200
|
+
if (maxRedirects <= 0) {
|
|
201
|
+
reject(new Error("Too many redirects"));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
download(response.headers.location, filename, maxRedirects - 1)
|
|
205
|
+
.then(resolve)
|
|
206
|
+
.catch(reject);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (response.statusCode !== 200) {
|
|
211
|
+
reject(
|
|
212
|
+
new Error(
|
|
213
|
+
`HTTP ${response.statusCode}: ${response.statusMessage}`,
|
|
214
|
+
),
|
|
215
|
+
);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const writer = fs.createWriteStream(filename);
|
|
220
|
+
response.pipe(writer);
|
|
221
|
+
|
|
222
|
+
writer.on("finish", () => {
|
|
223
|
+
console.log(`Download complete: ${filename}`);
|
|
224
|
+
resolve(dir);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
writer.on("error", (err) => {
|
|
228
|
+
console.error(`Error writing file: ${err.message}`);
|
|
229
|
+
reject(err);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
request.on("error", (err) => {
|
|
234
|
+
reject(new Error(`Request failed: ${err.message}`));
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
request.setTimeout(300000, () => {
|
|
238
|
+
request.destroy();
|
|
239
|
+
reject(new Error("Request timed out"));
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
} catch (err) {
|
|
243
|
+
throw new Error(`Download failed: ${err.message}`);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
function exists(archive) {
|
|
248
|
+
if (!fs.existsSync(binDir)) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
return archive.bins.every((bin) => fs.existsSync(path.join(binDir, bin)));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export {
|
|
255
|
+
install,
|
|
256
|
+
run,
|
|
257
|
+
getArchive,
|
|
258
|
+
download,
|
|
259
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@conanmontreal/crush",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.98.1-fork.11",
|
|
5
|
+
"description": "A powerful terminal-based AI assistant for developers, providing intelligent coding assistance directly in your terminal. (pbtrudel fork with local patches)",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"postinstall": "node install.js",
|
|
8
|
+
"run": "node run-crush.js"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/pbtrudel/crush.git"
|
|
13
|
+
},
|
|
14
|
+
"license": "FSL-1.1-MIT",
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/pbtrudel/crush/issues"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/pbtrudel/crush#readme",
|
|
22
|
+
"bin": {
|
|
23
|
+
"crush": "run-crush.js"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"*.js"
|
|
27
|
+
],
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"jszip": "3.10.2",
|
|
30
|
+
"proxy-agent": "8.0.2",
|
|
31
|
+
"tar": "7.5.22"
|
|
32
|
+
},
|
|
33
|
+
"archives": {
|
|
34
|
+
"linux-x64": {
|
|
35
|
+
"name": "crush_0.98.1-fork.11_Linux_x86_64.tar.gz",
|
|
36
|
+
"url": "https://github.com/pbtrudel/crush/releases/download/v0.98.1-fork.11/crush_0.98.1-fork.11_Linux_x86_64.tar.gz",
|
|
37
|
+
"bins": [
|
|
38
|
+
"crush"
|
|
39
|
+
],
|
|
40
|
+
"format": "tar.gz",
|
|
41
|
+
"checksum": {
|
|
42
|
+
"algorithm": "sha256",
|
|
43
|
+
"digest": "104f2a5b500d92b5761836f924fe25fd857a40bc9e53678c60ce740713447be0"
|
|
44
|
+
},
|
|
45
|
+
"wrappedIn": "crush_0.98.1-fork.11_Linux_x86_64"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|