@conanmontreal/crush 0.0.0-stage → 0.98.1-fork.7

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 CHANGED
@@ -1,3 +1,56 @@
1
- # Temporary Holding Version
1
+ # @conanmontreal/crush
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+
3
+ // This file was generated by GoReleaser. DO NOT EDIT.
4
+
5
+ import { install } from "./lib.js";
6
+ install();
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
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "type": "module",
4
+ "version": "0.98.1-fork.7",
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.7_Linux_x86_64.tar.gz",
36
+ "url": "https://github.com/pbtrudel/crush/releases/download/v0.98.1-fork.7/crush_0.98.1-fork.7_Linux_x86_64.tar.gz",
37
+ "bins": [
38
+ "crush"
39
+ ],
40
+ "format": "tar.gz",
41
+ "checksum": {
42
+ "algorithm": "sha256",
43
+ "digest": "c999de5655ef9f9a2cf92b0e04e587f91ccf3c60ba6d9657aefc3a74e858e5d0"
44
+ },
45
+ "wrappedIn": "crush_0.98.1-fork.7_Linux_x86_64"
46
+ }
47
+ }
48
+ }
package/run-crush.js ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+
3
+ // This file was generated by GoReleaser. DO NOT EDIT.
4
+
5
+ import { run } from "./lib.js";
6
+ run("crush").then((code) => process.exit(code));