codewhale 0.8.41

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.
@@ -0,0 +1,135 @@
1
+ const fs = require("fs");
2
+ const { execFileSync } = require("child_process");
3
+
4
+ const GLIBC_VERSION_RE = /GLIBC_(\d+)\.(\d+)(?:\.(\d+))?/g;
5
+
6
+ function isLinux() {
7
+ return process.platform === "linux";
8
+ }
9
+
10
+ function parseVersion(text) {
11
+ const match = String(text || "").match(/(\d+)\.(\d+)(?:\.(\d+))?/);
12
+ if (!match) return null;
13
+ return [Number(match[1]), Number(match[2]), Number(match[3] || 0)];
14
+ }
15
+
16
+ function compareVersion(a, b) {
17
+ for (let i = 0; i < 3; i += 1) {
18
+ if (a[i] !== b[i]) return a[i] - b[i];
19
+ }
20
+ return 0;
21
+ }
22
+
23
+ function formatVersion(version) {
24
+ return version[2] ? `${version[0]}.${version[1]}.${version[2]}` : `${version[0]}.${version[1]}`;
25
+ }
26
+
27
+ function detectHostGlibc() {
28
+ try {
29
+ const out = execFileSync("getconf", ["GNU_LIBC_VERSION"], {
30
+ encoding: "utf8",
31
+ stdio: ["ignore", "pipe", "ignore"],
32
+ });
33
+ const version = parseVersion(out);
34
+ if (version) return version;
35
+ } catch {
36
+ // fall through to ldd
37
+ }
38
+ try {
39
+ const out = execFileSync("ldd", ["--version"], {
40
+ encoding: "utf8",
41
+ stdio: ["ignore", "pipe", "ignore"],
42
+ });
43
+ const firstLine = out.split("\n", 1)[0];
44
+ const version = parseVersion(firstLine);
45
+ if (version) return version;
46
+ } catch {
47
+ // glibc not present (e.g. musl / Alpine)
48
+ }
49
+ return null;
50
+ }
51
+
52
+ function detectBinaryRequiredGlibc(filePath) {
53
+ const buf = fs.readFileSync(filePath);
54
+ const text = buf.toString("latin1");
55
+ let highest = null;
56
+ GLIBC_VERSION_RE.lastIndex = 0;
57
+ let match;
58
+ while ((match = GLIBC_VERSION_RE.exec(text)) !== null) {
59
+ const version = [Number(match[1]), Number(match[2]), Number(match[3] || 0)];
60
+ if (!highest || compareVersion(version, highest) > 0) {
61
+ highest = version;
62
+ }
63
+ }
64
+ return highest;
65
+ }
66
+
67
+ function buildFromSourceHint() {
68
+ return [
69
+ "You can still run codewhale by building from source with Cargo:",
70
+ "",
71
+ " # Requires Rust 1.88+ (https://rustup.rs)",
72
+ " cargo install codewhale-cli --locked # provides `codewhale`",
73
+ " cargo install codewhale-tui --locked # provides `codewhale-tui`",
74
+ "",
75
+ "Or build from a checkout:",
76
+ "",
77
+ " git clone https://github.com/Hmbown/CodeWhale.git",
78
+ " cd CodeWhale",
79
+ " cargo install --path crates/cli --locked",
80
+ " cargo install --path crates/tui --locked",
81
+ "",
82
+ "See https://github.com/Hmbown/CodeWhale/blob/main/docs/INSTALL.md",
83
+ ].join("\n");
84
+ }
85
+
86
+ function preflightGlibc(filePath) {
87
+ if (!isLinux()) return;
88
+ if (
89
+ process.env.DEEPSEEK_TUI_SKIP_GLIBC_CHECK === "1" ||
90
+ process.env.DEEPSEEK_SKIP_GLIBC_CHECK === "1"
91
+ ) {
92
+ return;
93
+ }
94
+
95
+ const required = detectBinaryRequiredGlibc(filePath);
96
+ if (!required) {
97
+ // Statically linked / musl binary, or no GLIBC_* version dependencies present.
98
+ return;
99
+ }
100
+
101
+ const host = detectHostGlibc();
102
+ if (!host) {
103
+ throw new Error(
104
+ [
105
+ `The prebuilt binary requires GLIBC_${formatVersion(required)}, but no GNU libc was detected on this host.`,
106
+ "This usually means you're on a musl-based distro such as Alpine.",
107
+ "",
108
+ buildFromSourceHint(),
109
+ "",
110
+ "Set DEEPSEEK_TUI_SKIP_GLIBC_CHECK=1 to bypass this check at your own risk.",
111
+ ].join("\n"),
112
+ );
113
+ }
114
+
115
+ if (compareVersion(host, required) < 0) {
116
+ throw new Error(
117
+ [
118
+ `Prebuilt DeepSeek TUI binary requires GLIBC_${formatVersion(required)} but this system has glibc ${formatVersion(host)}.`,
119
+ "Older distros (CentOS 7/8, RHEL 7/8, Debian 10, etc.) ship an older glibc that is not compatible with the prebuilt artifact.",
120
+ "",
121
+ buildFromSourceHint(),
122
+ "",
123
+ "Set DEEPSEEK_TUI_SKIP_GLIBC_CHECK=1 to bypass this check at your own risk.",
124
+ ].join("\n"),
125
+ );
126
+ }
127
+ }
128
+
129
+ module.exports = {
130
+ preflightGlibc,
131
+ detectHostGlibc,
132
+ detectBinaryRequiredGlibc,
133
+ // exported for tests
134
+ _internal: { parseVersion, compareVersion, formatVersion },
135
+ };
package/scripts/run.js ADDED
@@ -0,0 +1,59 @@
1
+ const { spawnSync } = require("child_process");
2
+ const { getBinaryPath } = require("./install");
3
+
4
+ const pkg = require("../package.json");
5
+
6
+ function isVersionFlag(args = process.argv.slice(2)) {
7
+ return args.includes("--version") || args.includes("-V");
8
+ }
9
+
10
+ function handleVersionFallback(binaryName) {
11
+ if (isVersionFlag()) {
12
+ const binVersion =
13
+ pkg.codewhaleBinaryVersion || pkg.deepseekBinaryVersion || pkg.version;
14
+ console.log(`${binaryName} (npm wrapper) v${pkg.version}`);
15
+ console.log(`binary version: v${binVersion}`);
16
+ console.log(`repo: ${pkg.repository?.url || "N/A"}`);
17
+ process.exit(0);
18
+ }
19
+ }
20
+
21
+ async function run(binaryName) {
22
+ // Intercept --version before attempting binary download/launch
23
+ handleVersionFallback(binaryName);
24
+
25
+ const binaryPath = await getBinaryPath(binaryName);
26
+ const result = spawnSync(binaryPath, process.argv.slice(2), {
27
+ stdio: "inherit",
28
+ });
29
+ if (result.error) {
30
+ // If binary fails and user asked for --version, show npm version instead
31
+ handleVersionFallback(binaryName);
32
+ throw result.error;
33
+ }
34
+ process.exit(result.status ?? 1);
35
+ }
36
+
37
+ async function runCodeWhale() {
38
+ await run("codewhale");
39
+ }
40
+
41
+ async function runCodeWhaleTui() {
42
+ await run("codewhale-tui");
43
+ }
44
+
45
+ module.exports = {
46
+ run,
47
+ runCodeWhale,
48
+ runCodeWhaleTui,
49
+ _internal: { isVersionFlag },
50
+ };
51
+
52
+ if (require.main === module) {
53
+ const command = process.argv[1] || "";
54
+ if (command.includes("tui")) {
55
+ runCodeWhaleTui();
56
+ } else {
57
+ runCodeWhale();
58
+ }
59
+ }
@@ -0,0 +1,140 @@
1
+ const https = require("https");
2
+ const http = require("http");
3
+ const {
4
+ allAssetNames,
5
+ allReleaseAssetNames,
6
+ checksumManifestUrl,
7
+ releaseAssetUrl,
8
+ } = require("./artifacts");
9
+
10
+ const pkg = require("../package.json");
11
+
12
+ function resolveBinaryVersion() {
13
+ const configuredVersion =
14
+ process.env.DEEPSEEK_TUI_VERSION ||
15
+ process.env.DEEPSEEK_VERSION ||
16
+ pkg.codewhaleBinaryVersion || pkg.deepseekBinaryVersion ||
17
+ pkg.version;
18
+ return String(configuredVersion).trim();
19
+ }
20
+
21
+ function resolveRepo() {
22
+ return process.env.DEEPSEEK_TUI_GITHUB_REPO || process.env.DEEPSEEK_GITHUB_REPO || "Hmbown/CodeWhale";
23
+ }
24
+
25
+ function requestStatus(url, method = "HEAD", redirects = 0) {
26
+ if (redirects > 10) {
27
+ throw new Error(`Too many redirects while checking ${url}`);
28
+ }
29
+ const client = url.startsWith("https:") ? https : http;
30
+ return new Promise((resolve, reject) => {
31
+ const req = client.request(
32
+ url,
33
+ {
34
+ method,
35
+ headers: {
36
+ "User-Agent": "codewhale-npm-release-check",
37
+ },
38
+ },
39
+ (res) => {
40
+ const status = res.statusCode || 0;
41
+ const location = res.headers.location;
42
+ res.resume();
43
+ if (status >= 300 && status < 400 && location) {
44
+ const next = new URL(location, url).toString();
45
+ resolve(requestStatus(next, method, redirects + 1));
46
+ return;
47
+ }
48
+ resolve(status);
49
+ },
50
+ );
51
+ req.on("error", reject);
52
+ req.end();
53
+ });
54
+ }
55
+
56
+ async function verifyAsset(url, label) {
57
+ let status = await requestStatus(url, "HEAD");
58
+ if (status === 403 || status === 405) {
59
+ status = await requestStatus(url, "GET");
60
+ }
61
+ if (status < 200 || status >= 400) {
62
+ throw new Error(`${label} returned HTTP ${status} (${url})`);
63
+ }
64
+ }
65
+
66
+ async function downloadText(url) {
67
+ const client = url.startsWith("https:") ? https : http;
68
+ return new Promise((resolve, reject) => {
69
+ client
70
+ .get(
71
+ url,
72
+ {
73
+ headers: {
74
+ "User-Agent": "codewhale-npm-release-check",
75
+ },
76
+ },
77
+ (res) => {
78
+ const status = res.statusCode || 0;
79
+ if (status >= 300 && status < 400 && res.headers.location) {
80
+ const next = new URL(res.headers.location, url).toString();
81
+ resolve(downloadText(next));
82
+ return;
83
+ }
84
+ if (status !== 200) {
85
+ reject(new Error(`Request failed with status ${status}: ${url}`));
86
+ res.resume();
87
+ return;
88
+ }
89
+ const chunks = [];
90
+ res.setEncoding("utf8");
91
+ res.on("data", (chunk) => chunks.push(chunk));
92
+ res.on("end", () => resolve(chunks.join("")));
93
+ },
94
+ )
95
+ .on("error", reject);
96
+ });
97
+ }
98
+
99
+ function parseChecksumManifest(text) {
100
+ const checksums = new Map();
101
+ for (const line of text.split(/\r?\n/)) {
102
+ const trimmed = line.trim();
103
+ if (!trimmed) {
104
+ continue;
105
+ }
106
+ const match = trimmed.match(/^([a-fA-F0-9]{64})\s+\*?(.+)$/);
107
+ if (!match) {
108
+ throw new Error(`Invalid checksum manifest line: ${trimmed}`);
109
+ }
110
+ checksums.set(match[2], match[1].toLowerCase());
111
+ }
112
+ return checksums;
113
+ }
114
+
115
+ async function run() {
116
+ const version = resolveBinaryVersion();
117
+ const repo = resolveRepo();
118
+ const assets = allReleaseAssetNames();
119
+
120
+ console.log(`Verifying ${assets.length} release assets for ${repo}@v${version}...`);
121
+ for (const asset of assets) {
122
+ const url = releaseAssetUrl(asset, version, repo);
123
+ await verifyAsset(url, asset);
124
+ console.log(` ok ${asset}`);
125
+ }
126
+ const checksums = parseChecksumManifest(
127
+ await downloadText(checksumManifestUrl(version, repo)),
128
+ );
129
+ for (const asset of allAssetNames()) {
130
+ if (!checksums.has(asset)) {
131
+ throw new Error(`Checksum manifest is missing ${asset}`);
132
+ }
133
+ }
134
+ console.log("Release assets verified.");
135
+ }
136
+
137
+ run().catch((error) => {
138
+ console.error("Release asset verification failed:", error.message);
139
+ process.exit(1);
140
+ });
@@ -0,0 +1,66 @@
1
+ const assert = require("node:assert/strict");
2
+ const path = require("node:path");
3
+ const test = require("node:test");
4
+ const os = require("os");
5
+
6
+ const ARTIFACTS_PATH = path.join(__dirname, "..", "scripts", "artifacts.js");
7
+
8
+ function withMockedOs(platform, arch, fn) {
9
+ const origPlatform = os.platform;
10
+ const origArch = os.arch;
11
+ os.platform = () => platform;
12
+ os.arch = () => arch;
13
+ delete require.cache[ARTIFACTS_PATH];
14
+ try {
15
+ return fn();
16
+ } finally {
17
+ os.platform = origPlatform;
18
+ os.arch = origArch;
19
+ delete require.cache[ARTIFACTS_PATH];
20
+ }
21
+ }
22
+
23
+ test("openharmony x64 resolves to linux x64 binaries", () => {
24
+ withMockedOs("openharmony", "x64", () => {
25
+ const { detectBinaryNames } = require(ARTIFACTS_PATH);
26
+ const result = detectBinaryNames();
27
+ assert.equal(result.codewhale, "codewhale-linux-x64");
28
+ assert.equal(result.tui, "codewhale-tui-linux-x64");
29
+ });
30
+ });
31
+
32
+ test("openharmony arm64 resolves to linux arm64 binaries", () => {
33
+ withMockedOs("openharmony", "arm64", () => {
34
+ const { detectBinaryNames } = require(ARTIFACTS_PATH);
35
+ const result = detectBinaryNames();
36
+ assert.equal(result.codewhale, "codewhale-linux-arm64");
37
+ assert.equal(result.tui, "codewhale-tui-linux-arm64");
38
+ });
39
+ });
40
+
41
+ test("genuinely unsupported platform throws with raw platform name", () => {
42
+ withMockedOs("freebsd", "x64", () => {
43
+ const { detectBinaryNames } = require(ARTIFACTS_PATH);
44
+ assert.throws(
45
+ () => detectBinaryNames(),
46
+ (err) => {
47
+ assert.match(err.message, /Unsupported platform: freebsd/);
48
+ return true;
49
+ },
50
+ );
51
+ });
52
+ });
53
+
54
+ test("known platforms are unaffected by alias map", () => {
55
+ for (const [platform, arch, expectedCodeWhale] of [
56
+ ["linux", "x64", "codewhale-linux-x64"],
57
+ ["darwin", "arm64", "codewhale-macos-arm64"],
58
+ ["win32", "x64", "codewhale-windows-x64.exe"],
59
+ ]) {
60
+ withMockedOs(platform, arch, () => {
61
+ const { detectBinaryNames } = require(ARTIFACTS_PATH);
62
+ const result = detectBinaryNames();
63
+ assert.equal(result.codewhale, expectedCodeWhale);
64
+ });
65
+ }
66
+ });
@@ -0,0 +1,180 @@
1
+ const assert = require("node:assert/strict");
2
+ const crypto = require("node:crypto");
3
+ const fs = require("node:fs");
4
+ const os = require("node:os");
5
+ const path = require("node:path");
6
+ const test = require("node:test");
7
+
8
+ const installScript = fs.readFileSync(
9
+ path.join(__dirname, "..", "scripts", "install.js"),
10
+ "utf8",
11
+ );
12
+ const { installFailureHint, _internal } = require("../scripts/install");
13
+
14
+ function sha256(content) {
15
+ return crypto.createHash("sha256").update(content).digest("hex");
16
+ }
17
+
18
+ async function makeTempDir(t) {
19
+ const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "codewhale-install-test-"));
20
+ t.after(() => fs.promises.rm(dir, { force: true, recursive: true }));
21
+ return dir;
22
+ }
23
+
24
+ async function exists(file) {
25
+ return fs.promises.access(file).then(
26
+ () => true,
27
+ () => false,
28
+ );
29
+ }
30
+
31
+ async function withoutForcedDownload(callback) {
32
+ const previousTui = process.env.DEEPSEEK_TUI_FORCE_DOWNLOAD;
33
+ const previousLegacy = process.env.DEEPSEEK_FORCE_DOWNLOAD;
34
+ delete process.env.DEEPSEEK_TUI_FORCE_DOWNLOAD;
35
+ delete process.env.DEEPSEEK_FORCE_DOWNLOAD;
36
+ try {
37
+ return await callback();
38
+ } finally {
39
+ if (previousTui === undefined) {
40
+ delete process.env.DEEPSEEK_TUI_FORCE_DOWNLOAD;
41
+ } else {
42
+ process.env.DEEPSEEK_TUI_FORCE_DOWNLOAD = previousTui;
43
+ }
44
+ if (previousLegacy === undefined) {
45
+ delete process.env.DEEPSEEK_FORCE_DOWNLOAD;
46
+ } else {
47
+ process.env.DEEPSEEK_FORCE_DOWNLOAD = previousLegacy;
48
+ }
49
+ }
50
+ }
51
+
52
+ test("install script checks Node support before loading helpers", () => {
53
+ const guardIndex = installScript.indexOf("assertSupportedNode();");
54
+ const firstRequireIndex = installScript.indexOf("require(");
55
+
56
+ assert.notEqual(guardIndex, -1);
57
+ assert.notEqual(firstRequireIndex, -1);
58
+ assert.ok(guardIndex < firstRequireIndex);
59
+ });
60
+
61
+ test("install script remains parseable before the Node support guard runs", () => {
62
+ assert.equal(installScript.includes("??"), false);
63
+ assert.equal(installScript.includes("?."), false);
64
+ });
65
+
66
+ test("install failure hint explains release base override for blocked GitHub downloads", () => {
67
+ const previous = process.env.DEEPSEEK_TUI_RELEASE_BASE_URL;
68
+ delete process.env.DEEPSEEK_TUI_RELEASE_BASE_URL;
69
+ try {
70
+ const error = Object.assign(
71
+ new Error(
72
+ "fetch https://github.com/Hmbown/CodeWhale/releases/download/v0.8.19/codewhale-artifacts-sha256.txt failed after 5 attempts:\ngetaddrinfo ENOTFOUND github.com",
73
+ ),
74
+ { code: "ENOTFOUND" },
75
+ );
76
+
77
+ const hint = installFailureHint(error);
78
+
79
+ assert.match(hint, /DEEPSEEK_TUI_RELEASE_BASE_URL/);
80
+ assert.match(hint, /codewhale-artifacts-sha256\.txt/);
81
+ assert.match(hint, /platform binaries/);
82
+ assert.match(hint, /#npm-binary-download-times-out/);
83
+ } finally {
84
+ if (previous === undefined) {
85
+ delete process.env.DEEPSEEK_TUI_RELEASE_BASE_URL;
86
+ } else {
87
+ process.env.DEEPSEEK_TUI_RELEASE_BASE_URL = previous;
88
+ }
89
+ }
90
+ });
91
+
92
+ test("install failure hint checks configured release base when override is already set", () => {
93
+ const previous = process.env.DEEPSEEK_TUI_RELEASE_BASE_URL;
94
+ process.env.DEEPSEEK_TUI_RELEASE_BASE_URL = "https://mirror.example/deepseek/";
95
+ try {
96
+ const error = Object.assign(new Error("download stalled"), {
97
+ code: "EDOWNLOADTIMEOUT",
98
+ });
99
+
100
+ const hint = installFailureHint(error);
101
+
102
+ assert.match(hint, /is set to https:\/\/mirror\.example\/deepseek\//);
103
+ assert.match(hint, /codewhale-artifacts-sha256\.txt/);
104
+ assert.doesNotMatch(hint, /If GitHub is unavailable/);
105
+ } finally {
106
+ if (previous === undefined) {
107
+ delete process.env.DEEPSEEK_TUI_RELEASE_BASE_URL;
108
+ } else {
109
+ process.env.DEEPSEEK_TUI_RELEASE_BASE_URL = previous;
110
+ }
111
+ }
112
+ });
113
+
114
+ test("ensureBinary adopts a manually placed target binary after checksum validation", async (t) => {
115
+ const dir = await makeTempDir(t);
116
+ const target = path.join(dir, process.platform === "win32" ? "codewhale.exe" : "codewhale");
117
+ const assetName = process.platform === "win32" ? "codewhale-windows-x64.exe" : "codewhale-linux-x64";
118
+ const version = "0.8.25";
119
+ const content = Buffer.from("manual codewhale binary");
120
+ let checksumLoads = 0;
121
+
122
+ await fs.promises.writeFile(target, content, { mode: 0o600 });
123
+ await fs.promises.writeFile(`${target}.version`, "0.8.24", "utf8");
124
+
125
+ const result = await withoutForcedDownload(() =>
126
+ _internal.ensureBinary(target, assetName, version, "Hmbown/CodeWhale", async () => {
127
+ checksumLoads += 1;
128
+ return new Map([[assetName, sha256(content)]]);
129
+ }),
130
+ );
131
+
132
+ assert.equal(result, target);
133
+ assert.equal(checksumLoads, 1);
134
+ assert.equal(await fs.promises.readFile(`${target}.version`, "utf8"), version);
135
+ if (process.platform !== "win32") {
136
+ assert.notEqual((await fs.promises.stat(target)).mode & 0o111, 0);
137
+ }
138
+ });
139
+
140
+ test("ensureBinary adopts an official release-named binary placed in downloads", async (t) => {
141
+ const dir = await makeTempDir(t);
142
+ const target = path.join(dir, process.platform === "win32" ? "codewhale.exe" : "codewhale");
143
+ const assetName = process.platform === "win32" ? "codewhale-windows-x64.exe" : "codewhale-linux-x64";
144
+ const assetPath = path.join(dir, assetName);
145
+ const version = "0.8.25";
146
+ const content = Buffer.from("official release binary");
147
+
148
+ await fs.promises.writeFile(assetPath, content);
149
+
150
+ const result = await withoutForcedDownload(() =>
151
+ _internal.ensureBinary(target, assetName, version, "Hmbown/CodeWhale", async () =>
152
+ new Map([[assetName, sha256(content)]]),
153
+ ),
154
+ );
155
+
156
+ assert.equal(result, target);
157
+ assert.equal(await exists(target), true);
158
+ assert.equal(await exists(assetPath), false);
159
+ assert.equal(await fs.promises.readFile(`${target}.version`, "utf8"), version);
160
+ });
161
+
162
+ test("manual binaries with mismatched checksums are not adopted", async (t) => {
163
+ const dir = await makeTempDir(t);
164
+ const target = path.join(dir, process.platform === "win32" ? "codewhale.exe" : "codewhale");
165
+ const assetName = process.platform === "win32" ? "codewhale-windows-x64.exe" : "codewhale-linux-x64";
166
+ const content = Buffer.from("wrong binary bytes");
167
+
168
+ await fs.promises.writeFile(target, content);
169
+
170
+ const adopted = await _internal.adoptExistingBinaryIfValid(
171
+ target,
172
+ assetName,
173
+ "0.8.25",
174
+ async () => new Map([[assetName, sha256(Buffer.from("different bytes"))]]),
175
+ `${target}.version`,
176
+ );
177
+
178
+ assert.equal(adopted, false);
179
+ assert.equal(await exists(`${target}.version`), false);
180
+ });