better-coder 0.1.0
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 +27 -0
- package/bin/cli.js +180 -0
- package/bin/download.js +137 -0
- package/dist/manifest.json +4 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# better-coder
|
|
2
|
+
|
|
3
|
+
Prompt AI agents against local git repositories with zero friction.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx better-coder
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
First run downloads the app (~50MB). Subsequent runs launch instantly from cache.
|
|
12
|
+
|
|
13
|
+
## Supported Platforms
|
|
14
|
+
|
|
15
|
+
- macOS (Apple Silicon and Intel)
|
|
16
|
+
- Linux (x64 and ARM64)
|
|
17
|
+
- Windows (x64 and ARM64)
|
|
18
|
+
|
|
19
|
+
## Cache Location
|
|
20
|
+
|
|
21
|
+
Binaries are cached at:
|
|
22
|
+
- macOS/Linux: ~/.better-coder/bin/
|
|
23
|
+
- Windows: %USERPROFILE%\.better-coder\bin\
|
|
24
|
+
|
|
25
|
+
## Local Development
|
|
26
|
+
|
|
27
|
+
Set BETTER_CODER_LOCAL=1 to use binaries from npx-cli/dist/ instead of CDN.
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { execSync, spawn } = require("child_process");
|
|
4
|
+
const AdmZip = require("adm-zip");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const { ensureBinary, BINARY_TAG, CACHE_DIR, LOCAL_DEV_MODE, LOCAL_DIST_DIR, R2_BASE_URL, getLatestVersion } = require("./download");
|
|
8
|
+
|
|
9
|
+
const CLI_VERSION = require("../package.json").version;
|
|
10
|
+
|
|
11
|
+
// Resolve effective arch for our published 64-bit binaries only.
|
|
12
|
+
// Any ARM → arm64; anything else → x64. On macOS, handle Rosetta.
|
|
13
|
+
function getEffectiveArch() {
|
|
14
|
+
const platform = process.platform;
|
|
15
|
+
const nodeArch = process.arch;
|
|
16
|
+
|
|
17
|
+
if (platform === "darwin") {
|
|
18
|
+
// If Node itself is arm64, we're natively on Apple silicon
|
|
19
|
+
if (nodeArch === "arm64") return "arm64";
|
|
20
|
+
|
|
21
|
+
// Otherwise check for Rosetta translation
|
|
22
|
+
try {
|
|
23
|
+
const translated = execSync("sysctl -in sysctl.proc_translated", {
|
|
24
|
+
encoding: "utf8",
|
|
25
|
+
}).trim();
|
|
26
|
+
if (translated === "1") return "arm64";
|
|
27
|
+
} catch {
|
|
28
|
+
// sysctl key not present → assume true Intel
|
|
29
|
+
}
|
|
30
|
+
return "x64";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Non-macOS: coerce to broad families we support
|
|
34
|
+
if (/arm/i.test(nodeArch)) return "arm64";
|
|
35
|
+
|
|
36
|
+
// On Windows with 32-bit Node (ia32), detect OS arch via env
|
|
37
|
+
if (platform === "win32") {
|
|
38
|
+
const pa = process.env.PROCESSOR_ARCHITECTURE || "";
|
|
39
|
+
const paw = process.env.PROCESSOR_ARCHITEW6432 || "";
|
|
40
|
+
if (/arm/i.test(pa) || /arm/i.test(paw)) return "arm64";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return "x64";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const platform = process.platform;
|
|
47
|
+
const arch = getEffectiveArch();
|
|
48
|
+
|
|
49
|
+
// Map to our build target names
|
|
50
|
+
function getPlatformDir() {
|
|
51
|
+
if (platform === "linux" && arch === "x64") return "linux-x64";
|
|
52
|
+
if (platform === "linux" && arch === "arm64") return "linux-arm64";
|
|
53
|
+
if (platform === "win32" && arch === "x64") return "windows-x64";
|
|
54
|
+
if (platform === "win32" && arch === "arm64") return "windows-arm64";
|
|
55
|
+
if (platform === "darwin" && arch === "x64") return "macos-x64";
|
|
56
|
+
if (platform === "darwin" && arch === "arm64") return "macos-arm64";
|
|
57
|
+
|
|
58
|
+
console.error(`Unsupported platform: ${platform}-${arch}`);
|
|
59
|
+
console.error("Supported platforms:");
|
|
60
|
+
console.error(" - Linux x64");
|
|
61
|
+
console.error(" - Linux ARM64");
|
|
62
|
+
console.error(" - Windows x64");
|
|
63
|
+
console.error(" - Windows ARM64");
|
|
64
|
+
console.error(" - macOS x64 (Intel)");
|
|
65
|
+
console.error(" - macOS ARM64 (Apple Silicon)");
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getBinaryName(base) {
|
|
70
|
+
return platform === "win32" ? `${base}.exe` : base;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const platformDir = getPlatformDir();
|
|
74
|
+
// In local dev mode, extract directly to dist directory; otherwise use global cache
|
|
75
|
+
const versionCacheDir = LOCAL_DEV_MODE
|
|
76
|
+
? path.join(LOCAL_DIST_DIR, platformDir)
|
|
77
|
+
: path.join(CACHE_DIR, BINARY_TAG, platformDir);
|
|
78
|
+
|
|
79
|
+
function showProgress(downloaded, total) {
|
|
80
|
+
const percent = total ? Math.round((downloaded / total) * 100) : 0;
|
|
81
|
+
const mb = (downloaded / (1024 * 1024)).toFixed(1);
|
|
82
|
+
const totalMb = total ? (total / (1024 * 1024)).toFixed(1) : "?";
|
|
83
|
+
process.stderr.write(`\r Downloading: ${mb}MB / ${totalMb}MB (${percent}%)`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function extractAndRun(baseName, launch) {
|
|
87
|
+
const binName = getBinaryName(baseName);
|
|
88
|
+
const binPath = path.join(versionCacheDir, binName);
|
|
89
|
+
const zipPath = path.join(versionCacheDir, `${baseName}.zip`);
|
|
90
|
+
|
|
91
|
+
// Clean old binary if exists
|
|
92
|
+
try {
|
|
93
|
+
if (fs.existsSync(binPath)) {
|
|
94
|
+
fs.unlinkSync(binPath);
|
|
95
|
+
}
|
|
96
|
+
} catch (err) {
|
|
97
|
+
if (process.env.BETTER_CODER_DEBUG) {
|
|
98
|
+
console.warn(`Warning: Could not delete existing binary: ${err.message}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Download if not cached
|
|
103
|
+
if (!fs.existsSync(zipPath)) {
|
|
104
|
+
console.error(`Downloading ${baseName}...`);
|
|
105
|
+
try {
|
|
106
|
+
await ensureBinary(platformDir, baseName, showProgress);
|
|
107
|
+
console.error(""); // newline after progress
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.error(`\nDownload failed: ${err.message}`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Extract
|
|
115
|
+
if (!fs.existsSync(binPath)) {
|
|
116
|
+
try {
|
|
117
|
+
const zip = new AdmZip(zipPath);
|
|
118
|
+
zip.extractAllTo(versionCacheDir, true);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
console.error("Extraction failed:", err.message);
|
|
121
|
+
try {
|
|
122
|
+
fs.unlinkSync(zipPath);
|
|
123
|
+
} catch {}
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!fs.existsSync(binPath)) {
|
|
129
|
+
console.error(`Extracted binary not found at: ${binPath}`);
|
|
130
|
+
console.error("This usually indicates a corrupt download. Please try again.");
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Set permissions (non-Windows)
|
|
135
|
+
if (platform !== "win32") {
|
|
136
|
+
try {
|
|
137
|
+
fs.chmodSync(binPath, 0o755);
|
|
138
|
+
} catch {}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return launch(binPath);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function main() {
|
|
145
|
+
fs.mkdirSync(versionCacheDir, { recursive: true });
|
|
146
|
+
|
|
147
|
+
// Non-blocking update check (skip in local dev mode and when R2 URL not configured)
|
|
148
|
+
const hasValidR2Url = !R2_BASE_URL.startsWith("__");
|
|
149
|
+
if (!LOCAL_DEV_MODE && hasValidR2Url) {
|
|
150
|
+
getLatestVersion()
|
|
151
|
+
.then((latest) => {
|
|
152
|
+
if (latest && latest !== CLI_VERSION) {
|
|
153
|
+
setTimeout(() => {
|
|
154
|
+
console.log(`\nUpdate available: ${CLI_VERSION} -> ${latest}`);
|
|
155
|
+
console.log(`Run: npx better-coder@latest`);
|
|
156
|
+
}, 2000);
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
.catch(() => {});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const modeLabel = LOCAL_DEV_MODE ? " (local dev)" : "";
|
|
163
|
+
console.log(`Starting better-coder v${CLI_VERSION}${modeLabel}...`);
|
|
164
|
+
|
|
165
|
+
await extractAndRun("better-coder", (bin) => {
|
|
166
|
+
if (platform === "win32") {
|
|
167
|
+
execSync(`"${bin}"`, { stdio: "inherit" });
|
|
168
|
+
} else {
|
|
169
|
+
execSync(`"${bin}"`, { stdio: "inherit" });
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
main().catch((err) => {
|
|
175
|
+
console.error("Fatal error:", err.message);
|
|
176
|
+
if (process.env.BETTER_CODER_DEBUG) {
|
|
177
|
+
console.error(err.stack);
|
|
178
|
+
}
|
|
179
|
+
process.exit(1);
|
|
180
|
+
});
|
package/bin/download.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
const https = require("https");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const crypto = require("crypto");
|
|
5
|
+
|
|
6
|
+
// Replaced during npm pack by workflow
|
|
7
|
+
const R2_BASE_URL = "https://pub-705b64c8394843c58988d5d3978fac1d.r2.dev";
|
|
8
|
+
const BINARY_TAG = "v0.1.0-20260122005408"; // e.g., v0.1.0-20260121120000
|
|
9
|
+
const CACHE_DIR = path.join(require("os").homedir(), ".better-coder", "bin");
|
|
10
|
+
|
|
11
|
+
// Local development mode: use binaries from npx-cli/dist/ instead of R2
|
|
12
|
+
// Only activate if dist/ exists (i.e., running from source after local build)
|
|
13
|
+
const LOCAL_DIST_DIR = path.join(__dirname, "..", "dist");
|
|
14
|
+
const LOCAL_DEV_MODE = fs.existsSync(LOCAL_DIST_DIR) || process.env.BETTER_CODER_LOCAL === "1";
|
|
15
|
+
|
|
16
|
+
async function fetchJson(url) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
https.get(url, (res) => {
|
|
19
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
20
|
+
return fetchJson(res.headers.location).then(resolve).catch(reject);
|
|
21
|
+
}
|
|
22
|
+
if (res.statusCode !== 200) {
|
|
23
|
+
return reject(new Error(`HTTP ${res.statusCode} fetching ${url}`));
|
|
24
|
+
}
|
|
25
|
+
let data = "";
|
|
26
|
+
res.on("data", (chunk) => (data += chunk));
|
|
27
|
+
res.on("end", () => {
|
|
28
|
+
try {
|
|
29
|
+
resolve(JSON.parse(data));
|
|
30
|
+
} catch (e) {
|
|
31
|
+
reject(new Error(`Failed to parse JSON from ${url}`));
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}).on("error", reject);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function downloadFile(url, destPath, expectedSha256, onProgress) {
|
|
39
|
+
const tempPath = destPath + ".tmp";
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const file = fs.createWriteStream(tempPath);
|
|
42
|
+
const hash = crypto.createHash("sha256");
|
|
43
|
+
|
|
44
|
+
const cleanup = () => {
|
|
45
|
+
try {
|
|
46
|
+
fs.unlinkSync(tempPath);
|
|
47
|
+
} catch {}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
https.get(url, (res) => {
|
|
51
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
52
|
+
file.close();
|
|
53
|
+
cleanup();
|
|
54
|
+
return downloadFile(res.headers.location, destPath, expectedSha256, onProgress)
|
|
55
|
+
.then(resolve)
|
|
56
|
+
.catch(reject);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (res.statusCode !== 200) {
|
|
60
|
+
file.close();
|
|
61
|
+
cleanup();
|
|
62
|
+
return reject(new Error(`HTTP ${res.statusCode} downloading ${url}`));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const totalSize = parseInt(res.headers["content-length"], 10);
|
|
66
|
+
let downloadedSize = 0;
|
|
67
|
+
|
|
68
|
+
res.on("data", (chunk) => {
|
|
69
|
+
downloadedSize += chunk.length;
|
|
70
|
+
hash.update(chunk);
|
|
71
|
+
if (onProgress) onProgress(downloadedSize, totalSize);
|
|
72
|
+
});
|
|
73
|
+
res.pipe(file);
|
|
74
|
+
|
|
75
|
+
file.on("finish", () => {
|
|
76
|
+
file.close();
|
|
77
|
+
const actualSha256 = hash.digest("hex");
|
|
78
|
+
if (expectedSha256 && actualSha256 !== expectedSha256) {
|
|
79
|
+
cleanup();
|
|
80
|
+
reject(new Error(`Checksum mismatch: expected ${expectedSha256}, got ${actualSha256}`));
|
|
81
|
+
} else {
|
|
82
|
+
try {
|
|
83
|
+
fs.renameSync(tempPath, destPath);
|
|
84
|
+
resolve(destPath);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
cleanup();
|
|
87
|
+
reject(err);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}).on("error", (err) => {
|
|
92
|
+
file.close();
|
|
93
|
+
cleanup();
|
|
94
|
+
reject(err);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function ensureBinary(platform, binaryName, onProgress) {
|
|
100
|
+
// In local dev mode, use binaries directly from npx-cli/dist/
|
|
101
|
+
if (LOCAL_DEV_MODE) {
|
|
102
|
+
const localZipPath = path.join(LOCAL_DIST_DIR, platform, `${binaryName}.zip`);
|
|
103
|
+
if (fs.existsSync(localZipPath)) {
|
|
104
|
+
return localZipPath;
|
|
105
|
+
}
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Local binary not found: ${localZipPath}\n` +
|
|
108
|
+
`Run local build script first to build the binaries.`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const cacheDir = path.join(CACHE_DIR, BINARY_TAG, platform);
|
|
113
|
+
const zipPath = path.join(cacheDir, `${binaryName}.zip`);
|
|
114
|
+
|
|
115
|
+
if (fs.existsSync(zipPath)) return zipPath;
|
|
116
|
+
|
|
117
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
118
|
+
|
|
119
|
+
const manifest = await fetchJson(`${R2_BASE_URL}/binaries/${BINARY_TAG}/manifest.json`);
|
|
120
|
+
const binaryInfo = manifest.platforms?.[platform]?.[binaryName];
|
|
121
|
+
|
|
122
|
+
if (!binaryInfo) {
|
|
123
|
+
throw new Error(`Binary ${binaryName} not available for ${platform}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const url = `${R2_BASE_URL}/binaries/${BINARY_TAG}/${platform}/${binaryName}.zip`;
|
|
127
|
+
await downloadFile(url, zipPath, binaryInfo.sha256, onProgress);
|
|
128
|
+
|
|
129
|
+
return zipPath;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function getLatestVersion() {
|
|
133
|
+
const manifest = await fetchJson(`${R2_BASE_URL}/binaries/manifest.json`);
|
|
134
|
+
return manifest.latest;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = { R2_BASE_URL, BINARY_TAG, CACHE_DIR, LOCAL_DEV_MODE, LOCAL_DIST_DIR, ensureBinary, getLatestVersion };
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "better-coder",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "NPX wrapper for better-coder desktop app - prompt AI agents against local git repos",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"better-coder": "bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"adm-zip": "^0.5.16"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"tauri",
|
|
18
|
+
"desktop",
|
|
19
|
+
"agent",
|
|
20
|
+
"mcp",
|
|
21
|
+
"ai",
|
|
22
|
+
"coding"
|
|
23
|
+
],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/user/better-coder"
|
|
28
|
+
}
|
|
29
|
+
}
|