初始化仓库
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
bin/
|
||||
node_modules/
|
||||
@@ -0,0 +1,26 @@
|
||||
# cc-connect
|
||||
|
||||
Bridge local AI coding agents (Claude Code, Cursor, Gemini CLI, Codex) to messaging platforms (Feishu/Lark, DingTalk, Slack, Telegram, Discord, LINE, WeChat Work).
|
||||
|
||||
Chat with your AI dev assistant from anywhere.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install -g cc-connect
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Create config
|
||||
cc-connect --version
|
||||
|
||||
# Edit config.toml, then run
|
||||
cc-connect
|
||||
cc-connect -config /path/to/config.toml
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
See full documentation at: https://github.com/chenhg5/cc-connect
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
"use strict";
|
||||
|
||||
const { execSync } = require("child_process");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const https = require("https");
|
||||
const http = require("http");
|
||||
const zlib = require("zlib");
|
||||
|
||||
const PACKAGE = require("./package.json");
|
||||
const VERSION = `v${PACKAGE.version}`;
|
||||
const NAME = "cc-connect";
|
||||
|
||||
const GITHUB_REPO = "chenhg5/cc-connect";
|
||||
const GITEE_REPO = "cg33/cc-connect";
|
||||
|
||||
const PLATFORM_MAP = {
|
||||
darwin: "darwin",
|
||||
linux: "linux",
|
||||
win32: "windows",
|
||||
};
|
||||
|
||||
const ARCH_MAP = {
|
||||
x64: "amd64",
|
||||
arm64: "arm64",
|
||||
};
|
||||
|
||||
function getPlatformInfo() {
|
||||
const platform = PLATFORM_MAP[process.platform];
|
||||
const arch = ARCH_MAP[process.arch];
|
||||
if (!platform || !arch) {
|
||||
throw new Error(
|
||||
`Unsupported platform: ${process.platform}/${process.arch}. ` +
|
||||
`Supported: linux/darwin/windows x64/arm64`
|
||||
);
|
||||
}
|
||||
const ext = platform === "windows" ? ".zip" : ".tar.gz";
|
||||
const filename = `${NAME}-${VERSION}-${platform}-${arch}${ext}`;
|
||||
return { platform, arch, ext, filename };
|
||||
}
|
||||
|
||||
function getDownloadURLs(filename) {
|
||||
return [
|
||||
`https://github.com/${GITHUB_REPO}/releases/download/${VERSION}/${filename}`,
|
||||
`https://gitee.com/${GITEE_REPO}/releases/download/${VERSION}/${filename}`,
|
||||
];
|
||||
}
|
||||
|
||||
function fetch(url, redirects = 5) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (redirects <= 0) return reject(new Error("Too many redirects"));
|
||||
const mod = url.startsWith("https") ? https : http;
|
||||
mod
|
||||
.get(url, { headers: { "User-Agent": "cc-connect-npm" } }, (res) => {
|
||||
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||
return resolve(fetch(res.headers.location, redirects - 1));
|
||||
}
|
||||
if (res.statusCode !== 200) {
|
||||
res.resume();
|
||||
return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
||||
}
|
||||
const chunks = [];
|
||||
res.on("data", (c) => chunks.push(c));
|
||||
res.on("end", () => resolve(Buffer.concat(chunks)));
|
||||
res.on("error", reject);
|
||||
})
|
||||
.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function download(urls) {
|
||||
for (const url of urls) {
|
||||
try {
|
||||
console.log(`[cc-connect] Downloading from ${url}`);
|
||||
const data = await fetch(url);
|
||||
console.log(`[cc-connect] Downloaded ${(data.length / 1024 / 1024).toFixed(1)} MB`);
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.warn(`[cc-connect] Failed: ${err.message}, trying next source...`);
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`[cc-connect] Could not download binary from any source.\n` +
|
||||
` Tried: ${urls.join(", ")}\n` +
|
||||
` You can download manually from https://github.com/${GITHUB_REPO}/releases`
|
||||
);
|
||||
}
|
||||
|
||||
function extractTarGz(buffer, destDir, binaryName) {
|
||||
const tmpFile = path.join(destDir, "_tmp.tar.gz");
|
||||
fs.writeFileSync(tmpFile, buffer);
|
||||
try {
|
||||
execSync(`tar xzf "${tmpFile}" -C "${destDir}"`, { stdio: "pipe" });
|
||||
} finally {
|
||||
fs.unlinkSync(tmpFile);
|
||||
}
|
||||
const extracted = fs.readdirSync(destDir).find((f) => f.startsWith(NAME) && !f.endsWith(".tar.gz"));
|
||||
if (extracted && extracted !== binaryName) {
|
||||
fs.renameSync(path.join(destDir, extracted), path.join(destDir, binaryName));
|
||||
}
|
||||
}
|
||||
|
||||
function extractZip(buffer, destDir, binaryName) {
|
||||
const tmpFile = path.join(destDir, "_tmp.zip");
|
||||
fs.writeFileSync(tmpFile, buffer);
|
||||
try {
|
||||
try {
|
||||
execSync(`unzip -o "${tmpFile}" -d "${destDir}"`, { stdio: "pipe" });
|
||||
} catch {
|
||||
execSync(`powershell -Command "Expand-Archive -Force '${tmpFile}' '${destDir}'"`, {
|
||||
stdio: "pipe",
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
try { fs.unlinkSync(tmpFile); } catch {}
|
||||
}
|
||||
const extracted = fs.readdirSync(destDir).find((f) => f.startsWith(NAME) && f.endsWith(".exe"));
|
||||
if (extracted && extracted !== binaryName) {
|
||||
fs.renameSync(path.join(destDir, extracted), path.join(destDir, binaryName));
|
||||
}
|
||||
}
|
||||
|
||||
// parseVersion splits "1.2.3-beta.1" into { nums: [1,2,3], preTag: "beta", preNum: 1 }
|
||||
function parseVersion(v) {
|
||||
v = v.replace(/^v/, "").trim();
|
||||
const [base, ...rest] = v.split("-");
|
||||
const nums = base.split(".").map(Number);
|
||||
const pre = rest.join("-");
|
||||
const m = pre.match(/^([a-zA-Z]+)\.?(\d+)?$/);
|
||||
return { nums, preTag: m ? m[1] : pre, preNum: m && m[2] ? parseInt(m[2], 10) : 0, hasPre: pre !== "" };
|
||||
}
|
||||
|
||||
// isNewerOrEqual returns true if installed >= expected
|
||||
function isNewerOrEqual(installed, expected) {
|
||||
const a = parseVersion(installed);
|
||||
const b = parseVersion(expected);
|
||||
const len = Math.max(a.nums.length, b.nums.length);
|
||||
for (let i = 0; i < len; i++) {
|
||||
const av = a.nums[i] || 0;
|
||||
const bv = b.nums[i] || 0;
|
||||
if (av > bv) return true;
|
||||
if (av < bv) return false;
|
||||
}
|
||||
if (!a.hasPre && b.hasPre) return true;
|
||||
if (a.hasPre && !b.hasPre) return false;
|
||||
if (!a.hasPre && !b.hasPre) return true;
|
||||
// Both pre-release: compare tag then number (rc > beta, beta.10 > beta.9)
|
||||
if (a.preTag !== b.preTag) return a.preTag > b.preTag;
|
||||
return a.preNum >= b.preNum;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { platform, arch, ext, filename } = getPlatformInfo();
|
||||
console.log(`[cc-connect] Platform: ${platform}/${arch}`);
|
||||
|
||||
const binDir = path.join(__dirname, "bin");
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
const binaryName = platform === "windows" ? `${NAME}.exe` : NAME;
|
||||
const binaryPath = path.join(binDir, binaryName);
|
||||
|
||||
if (fs.existsSync(binaryPath)) {
|
||||
try {
|
||||
const out = execSync(`"${binaryPath}" --version`, { encoding: "utf8", timeout: 5000 });
|
||||
const expectedVer = VERSION.slice(1); // remove leading "v"
|
||||
if (out.includes(expectedVer)) {
|
||||
console.log(`[cc-connect] Binary ${VERSION} already installed, skipping.`);
|
||||
return;
|
||||
}
|
||||
// Don't downgrade: if existing binary is newer, keep it
|
||||
const match = out.match(/(\d+\.\d+\.\d+[^\s]*)/);
|
||||
if (match && isNewerOrEqual(match[1], expectedVer)) {
|
||||
console.log(`[cc-connect] Binary ${match[1]} is newer than ${VERSION}, skipping.`);
|
||||
return;
|
||||
}
|
||||
console.log(`[cc-connect] Existing binary is outdated, upgrading to ${VERSION}...`);
|
||||
fs.unlinkSync(binaryPath);
|
||||
} catch {
|
||||
console.log(`[cc-connect] Replacing existing binary with ${VERSION}...`);
|
||||
fs.unlinkSync(binaryPath);
|
||||
}
|
||||
}
|
||||
|
||||
const urls = getDownloadURLs(filename);
|
||||
const data = await download(urls);
|
||||
|
||||
if (ext === ".tar.gz") {
|
||||
extractTarGz(data, binDir, binaryName);
|
||||
} else {
|
||||
extractZip(data, binDir, binaryName);
|
||||
}
|
||||
|
||||
if (platform !== "windows") {
|
||||
fs.chmodSync(binaryPath, 0o755);
|
||||
}
|
||||
|
||||
if (platform === "darwin") {
|
||||
try {
|
||||
execSync(`xattr -d com.apple.quarantine "${binaryPath}"`, { stdio: "pipe" });
|
||||
console.log(`[cc-connect] Removed macOS quarantine attribute`);
|
||||
} catch {
|
||||
// xattr fails if the attribute doesn't exist, which is fine
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[cc-connect] Installed to ${binaryPath}`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err.message);
|
||||
console.error(
|
||||
"[cc-connect] Installation failed. You can install manually:\n" +
|
||||
` https://github.com/${GITHUB_REPO}/releases/tag/${VERSION}`
|
||||
);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "cc-connect",
|
||||
"version": "1.3.3-beta.4",
|
||||
"description": "Bridge local AI coding agents (Claude Code, Cursor, Gemini CLI) to messaging platforms (Feishu, DingTalk, Slack, Telegram, Discord, LINE, WeChat Work)",
|
||||
"keywords": [
|
||||
"claude-code",
|
||||
"ai-coding",
|
||||
"feishu",
|
||||
"dingtalk",
|
||||
"slack",
|
||||
"telegram",
|
||||
"discord",
|
||||
"line",
|
||||
"wechat-work",
|
||||
"chatbot",
|
||||
"bridge"
|
||||
],
|
||||
"homepage": "https://github.com/chenhg5/cc-connect",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/chenhg5/cc-connect.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": "chenhg5",
|
||||
"bin": {
|
||||
"cc-connect": "run.js"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "node install.js"
|
||||
},
|
||||
"files": [
|
||||
"install.js",
|
||||
"run.js",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
"use strict";
|
||||
|
||||
const { execFileSync, execSync } = require("child_process");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
const PACKAGE = require("./package.json");
|
||||
const EXPECTED_VER = PACKAGE.version; // e.g. "1.1.0-beta.4"
|
||||
const NAME = "cc-connect";
|
||||
const binDir = path.join(__dirname, "bin");
|
||||
const ext = process.platform === "win32" ? ".exe" : "";
|
||||
const binaryPath = path.join(binDir, NAME + ext);
|
||||
|
||||
// parseVersion splits "1.2.3-beta.1" into { nums: [1,2,3], preTag: "beta", preNum: 1 }
|
||||
function parseVersion(v) {
|
||||
v = v.replace(/^v/, "").trim();
|
||||
const [base, ...rest] = v.split("-");
|
||||
const nums = base.split(".").map(Number);
|
||||
const pre = rest.join("-");
|
||||
const m = pre.match(/^([a-zA-Z]+)\.?(\d+)?$/);
|
||||
return { nums, preTag: m ? m[1] : pre, preNum: m && m[2] ? parseInt(m[2], 10) : 0, hasPre: pre !== "" };
|
||||
}
|
||||
|
||||
// isNewerOrEqual returns true if installed >= expected
|
||||
function isNewerOrEqual(installed, expected) {
|
||||
const a = parseVersion(installed);
|
||||
const b = parseVersion(expected);
|
||||
const len = Math.max(a.nums.length, b.nums.length);
|
||||
for (let i = 0; i < len; i++) {
|
||||
const av = a.nums[i] || 0;
|
||||
const bv = b.nums[i] || 0;
|
||||
if (av > bv) return true;
|
||||
if (av < bv) return false;
|
||||
}
|
||||
// Same base: no pre-release >= any pre-release (1.2.3 >= 1.2.3-beta.1)
|
||||
if (!a.hasPre && b.hasPre) return true;
|
||||
if (a.hasPre && !b.hasPre) return false;
|
||||
if (!a.hasPre && !b.hasPre) return true;
|
||||
// Both pre-release: compare tag then number (rc > beta, beta.10 > beta.9)
|
||||
if (a.preTag !== b.preTag) return a.preTag > b.preTag;
|
||||
return a.preNum >= b.preNum;
|
||||
}
|
||||
|
||||
function needsReinstall() {
|
||||
if (!fs.existsSync(binaryPath)) return true;
|
||||
try {
|
||||
const out = execFileSync(binaryPath, ["--version"], { encoding: "utf8", timeout: 5000 });
|
||||
if (out.includes(EXPECTED_VER)) return false;
|
||||
// Extract version from output (e.g. "cc-connect 1.2.2-beta.1" or "1.2.2-beta.1")
|
||||
const match = out.match(/(\d+\.\d+\.\d+[^\s]*)/);
|
||||
if (match && isNewerOrEqual(match[1], EXPECTED_VER)) return false;
|
||||
return true;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (needsReinstall()) {
|
||||
console.log(`[cc-connect] Binary missing or outdated, installing v${EXPECTED_VER}...`);
|
||||
try {
|
||||
execSync("node " + JSON.stringify(path.join(__dirname, "install.js")), {
|
||||
stdio: "inherit",
|
||||
cwd: __dirname,
|
||||
});
|
||||
} catch {
|
||||
console.error("[cc-connect] Auto-install failed. Run manually: npm uninstall -g cc-connect && npm install -g cc-connect@beta");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
execFileSync(binaryPath, process.argv.slice(2), { stdio: "inherit" });
|
||||
} catch (err) {
|
||||
process.exit(err.status || 1);
|
||||
}
|
||||
Reference in New Issue
Block a user