From 29d202c6dac359705077a12db2432073023f8543 Mon Sep 17 00:00:00 2001 From: wbtiger <28288271@qq.com> Date: Tue, 19 May 2026 16:21:05 +0800 Subject: [PATCH] =?UTF-8?q?fix(npm):=20Windows=20=E5=AE=89=E8=A3=85?= =?UTF-8?q?=E5=90=8E=E4=BA=8C=E8=BF=9B=E5=88=B6=E7=BC=BA=E5=A4=B1=EF=BC=8C?= =?UTF-8?q?=E6=89=80=E6=9C=89=E5=91=BD=E4=BB=A4=E9=9D=99=E9=BB=98=E9=80=80?= =?UTF-8?q?=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:Windows 用户 npm install 后 gitlink-cli 无任何输出。 根因: 1. release.yml 不构建 Windows 二进制(只有 darwin/linux) 2. workflow 内联了旧版 install.js(从 github.com/ccfos 下载、只处理 .tar.gz) 3. cli.js 不检查二进制是否存在,execFileSync 失败时静默退出 修复: - release.yml: 加入 windows amd64/arm64 构建,输出 .zip; 删除内联 fallback 脚本,始终使用 npm/ 目录下的文件 - npm/bin/cli.js: 加 fs.existsSync 检查,缺失时输出明确错误和修复指引 - npm/scripts/install.js: 加 GitHub fallback 下载源 - scripts/install.js: 加 Windows 支持(.exe 后缀、.zip 解压、GitHub fallback) Closes #16 --- .github/workflows/release.yml | 146 ++++++---------------------------- npm/bin/cli.js | 18 ++++- npm/scripts/install.js | 14 ++-- scripts/install.js | 82 ++++++++++++++----- 4 files changed, 109 insertions(+), 151 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8be3183..854ac9a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,141 +27,45 @@ jobs: run: | mkdir -p dist VERSION=${GITHUB_REF#refs/tags/v} - - for pair in "darwin amd64" "darwin arm64" "linux amd64" "linux arm64"; do + + for pair in "darwin amd64" "darwin arm64" "linux amd64" "linux arm64" "windows amd64" "windows arm64"; do GOOS=$(echo $pair | cut -d' ' -f1) GOARCH=$(echo $pair | cut -d' ' -f2) echo "Building ${GOOS}-${GOARCH}..." - GOOS=$GOOS GOARCH=$GOARCH go build -ldflags "-s -w -X 'github.com/gitlink-org/gitlink-cli/cmd.Version=${VERSION}'" -o dist/gitlink-cli . + + EXT="" + if [ "$GOOS" = "windows" ]; then EXT=".exe"; fi + GOOS=$GOOS GOARCH=$GOARCH go build -ldflags "-s -w -X 'github.com/gitlink-org/gitlink-cli/cmd.Version=${VERSION}'" -o "dist/gitlink-cli${EXT}" . + cd dist - tar -czf "gitlink-cli_${VERSION}_${GOOS}_${GOARCH}.tar.gz" gitlink-cli - rm gitlink-cli + if [ "$GOOS" = "windows" ]; then + zip "gitlink-cli_${VERSION}_${GOOS}_${GOARCH}.zip" "gitlink-cli${EXT}" + else + tar -czf "gitlink-cli_${VERSION}_${GOOS}_${GOARCH}.tar.gz" "gitlink-cli${EXT}" + fi + rm "gitlink-cli${EXT}" cd .. done - name: Create Release uses: softprops/action-gh-release@v2 with: - files: dist/*.tar.gz + files: | + dist/*.tar.gz + dist/*.zip generate_release_notes: true - - name: Build npm package + - name: Publish npm package run: | VERSION=${GITHUB_REF#refs/tags/v} mkdir -p npm-pkg/bin npm-pkg/scripts npm-pkg/skills - - # Copy skills + cp -r skills/* npm-pkg/skills/ - - # Copy bin wrappers - cp bin/cli.js npm-pkg/bin/ 2>/dev/null || cat > npm-pkg/bin/cli.js << 'NODEEOF' - #!/usr/bin/env node - const {execFileSync} = require("child_process"); - const path = require("path"); - const bin = path.join(__dirname, "..", "bin", "gitlink-cli"); - try { process.exit(execFileSync(bin, process.argv.slice(2), {stdio:"inherit"}).status); } - catch(e) { process.exit(e.status || 1); } - NODEEOF - - cat > npm-pkg/bin/install-skills.js << 'NODEEOF' - #!/usr/bin/env node - const {execSync} = require("child_process"); - const path = require("path"); - const skillsDir = path.join(__dirname, "..", "skills"); - try { - execSync(`npx skills add "${skillsDir}" -y -g`, {stdio:"inherit"}); - } catch(e) { - console.error("Failed to install skills:", e.message); - process.exit(1); - } - NODEEOF - - # Copy install.js - cp scripts/install.js npm-pkg/scripts/ 2>/dev/null || cat > npm-pkg/scripts/install.js << 'NODEEOF' - #!/usr/bin/env node - "use strict"; - const os = require("os"); - const path = require("path"); - const fs = require("fs"); - const https = require("https"); - const http = require("http"); - const {execSync} = require("child_process"); - - const PACKAGE = require("../package.json"); - const VERSION = PACKAGE.version; - const BINARY_NAME = "gitlink-cli"; - const RELEASE_BASE = "https://github.com"; - const REPO_OWNER = "ccfos"; - const REPO_NAME = "gitlink-cli"; - - function getPlatformInfo() { - const platform = os.platform(); - const arch = os.arch(); - const pmap = {darwin:"darwin",linux:"linux",win32:"windows"}; - const amap = {x64:"amd64",arm64:"arm64"}; - const p=pmap[platform], a=amap[arch]; - if(!p||!a) throw new Error(`Unsupported: ${platform}-${arch}`); - return {platform:p,arch:a}; - } - - function fetch(url) { - return new Promise((resolve,reject) => { - const mod=url.startsWith("https")?https:http; - let count=0; - function req(u) { - if(++count>5) return reject(new Error("Too many redirects")); - mod.get(u,(res) => { - if([301,302,307,308].includes(res.statusCode)&&res.headers.location){ - let loc=res.headers.location; - if(loc.startsWith("/")){const p=new URL(u);loc=p.protocol+"//"+p.host+loc} - return req(loc); - } - if(res.statusCode!==200) return reject(new Error(`HTTP ${res.statusCode}`)); - const c=[];res.on("data",d=>c.push(d));res.on("end",()=>resolve(Buffer.concat(c))); - }).on("error",reject); - } - req(url); - }); - } - - async function main() { - try { - const {platform,arch} = getPlatformInfo(); - const binDir = path.join(__dirname,"..","bin"); - if(!fs.existsSync(binDir)) fs.mkdirSync(binDir,{recursive:true}); - const binaryPath = path.join(binDir,BINARY_NAME); - - if(fs.existsSync(binaryPath)) { - try { - const out = execSync(`"${binaryPath}" version`,{encoding:"utf-8",timeout:5000}); - if(out.includes(VERSION)) { console.log(`${BINARY_NAME} v${VERSION} already installed.`); return; } - } catch(e) {} - fs.unlinkSync(binaryPath); - } - - const assetName = `gitlink-cli_${VERSION}_${platform}_${arch}.tar.gz`; - const url = `${RELEASE_BASE}/${REPO_OWNER}/${REPO_NAME}/releases/download/v${VERSION}/${assetName}`; - console.log(`Downloading ${url}...`); - const data = await fetch(url); - const tmp = path.join(binDir,"dl.tar.gz"); - fs.writeFileSync(tmp,data); - execSync(`tar -xzf "${tmp}" -C "${binDir}"`,{stdio:"pipe"}); - fs.unlinkSync(tmp); - fs.chmodSync(binaryPath,0o755); - console.log(`${BINARY_NAME} v${VERSION} installed.`); - } catch(err) { - console.warn(`⚠ Binary download failed: ${err.message}`); - console.warn(`Skills are installed. Install binary manually:`); - console.warn(`npm run postinstall`); - } - } - main(); - NODEEOF - - # Create README + cp npm/bin/cli.js npm-pkg/bin/ + cp npm/bin/install-skills.js npm-pkg/bin/ + cp npm/scripts/install.js npm-pkg/scripts/ cp README.md npm-pkg/ - - # Create package.json + cat > npm-pkg/package.json << PKGEOF { "name": "@gitlink-ai/cli", @@ -184,14 +88,14 @@ jobs: ], "repository": { "type": "git", - "url": "https://github.com/ccfos/gitlink-cli.git" + "url": "https://www.gitlink.org.cn/Gitlink/gitlink-cli.git" }, "keywords": ["gitlink", "cli", "ai-agent", "skills"], - "author": "", + "author": "GitLink ", "license": "MulanPSL-2.0" } PKGEOF - + cd npm-pkg npm publish --access public env: diff --git a/npm/bin/cli.js b/npm/bin/cli.js index 474d69d..2d1d633 100755 --- a/npm/bin/cli.js +++ b/npm/bin/cli.js @@ -2,12 +2,27 @@ "use strict"; +const fs = require("fs"); const path = require("path"); const { execFileSync } = require("child_process"); const ext = process.platform === "win32" ? ".exe" : ""; const binaryPath = path.join(__dirname, "gitlink-cli" + ext); +if (!fs.existsSync(binaryPath)) { + console.error( + `Error: gitlink-cli binary not found at ${binaryPath}\n\n` + + `The binary was not downloaded during installation.\n` + + `This usually happens when the postinstall script failed (e.g. network issues).\n\n` + + `To fix, try one of:\n` + + ` 1. Reinstall: npm install -g @gitlink-ai/cli\n` + + ` 2. Manual download from:\n` + + ` https://www.gitlink.org.cn/Gitlink/gitlink-cli/releases\n` + + ` Then place the binary at: ${binaryPath}\n` + ); + process.exit(1); +} + try { execFileSync(binaryPath, process.argv.slice(2), { stdio: "inherit" }); } catch (err) { @@ -15,8 +30,5 @@ try { process.exit(err.status); } console.error(`Failed to run gitlink-cli: ${err.message}`); - console.error( - "Binary may not be installed. Try reinstalling: npm install -g @gitlink-ai/cli" - ); process.exit(1); } diff --git a/npm/scripts/install.js b/npm/scripts/install.js index ef82bdc..b0b80d2 100644 --- a/npm/scripts/install.js +++ b/npm/scripts/install.js @@ -13,13 +13,16 @@ const PACKAGE = require("../package.json"); const VERSION = PACKAGE.version; const BINARY_NAME = "gitlink-cli"; -// GitLink release download base URL -// Format: https://www.gitlink.org.cn/Gitlink/gitlink-cli/releases -// Attachment download: https://www.gitlink.org.cn/api/attachments/{attachment_id} +// GitLink release download (primary) const RELEASE_BASE = "https://www.gitlink.org.cn"; const REPO_OWNER = "Gitlink"; const REPO_NAME = "gitlink-cli"; +// GitHub release download (fallback for users who cannot reach GitLink CDN) +const GITHUB_RELEASE_BASE = "https://github.com"; +const GITHUB_REPO_OWNER = "ccfos"; +const GITHUB_REPO_NAME = "gitlink-cli"; + function getPlatformInfo() { const platform = os.platform(); const arch = os.arch(); @@ -180,8 +183,9 @@ async function findReleaseAsset(platform, arch) { console.log(`Warning: Could not fetch release info: ${e.message}`); } - // Fallback: try direct download URL pattern - return `${RELEASE_BASE}/api/${REPO_OWNER}/${REPO_NAME}/releases/${tagName}/assets/${archiveName}`; + // Fallback: download from GitHub releases + const tagName = `v${VERSION}`; + return `${GITHUB_RELEASE_BASE}/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}/releases/download/${tagName}/${archiveName}`; } async function downloadAndExtract(url, destDir, platform) { diff --git a/scripts/install.js b/scripts/install.js index ceeb24a..7045a3c 100644 --- a/scripts/install.js +++ b/scripts/install.js @@ -17,6 +17,10 @@ const RELEASE_BASE = "https://www.gitlink.org.cn"; const REPO_OWNER = "Gitlink"; const REPO_NAME = "gitlink-cli"; +const GITHUB_RELEASE_BASE = "https://github.com"; +const GITHUB_REPO_OWNER = "ccfos"; +const GITHUB_REPO_NAME = "gitlink-cli"; + function getPlatformInfo() { const platform = os.platform(); const arch = os.arch(); @@ -30,6 +34,18 @@ function getPlatformInfo() { return { platform: goPlatform, arch: goArch, isWindows: platform === "win32" }; } +function getBinaryName(isWindows) { + return isWindows ? BINARY_NAME + ".exe" : BINARY_NAME; +} + +function getArchiveExt(isWindows) { + return isWindows ? ".zip" : ".tar.gz"; +} + +function getArchiveName(platform, arch, isWindows) { + return `${BINARY_NAME}_${VERSION}_${platform}_${arch}${getArchiveExt(isWindows)}`; +} + function fetch(url, options = {}) { return new Promise((resolve, reject) => { const maxRedirects = options.maxRedirects || 5; @@ -65,14 +81,14 @@ function fetch(url, options = {}) { } }); req.on("error", reject); - req.setTimeout(30000, () => { req.destroy(); reject(new Error("Timeout")); }); + req.setTimeout(60000, () => { req.destroy(); reject(new Error("Timeout")); }); } doRequest(url); }); } -async function findReleaseAsset(platform, arch) { - const archiveName = `gitlink-cli_${VERSION}_${platform}_${arch}.tar.gz`; +async function findReleaseAsset(platform, arch, isWindows) { + const archiveName = getArchiveName(platform, arch, isWindows); const tagName = `v${VERSION}`; const apiUrl = `${RELEASE_BASE}/api/${REPO_OWNER}/${REPO_NAME}/releases.json`; @@ -85,7 +101,7 @@ async function findReleaseAsset(platform, arch) { if (release && release.attachments) { let asset = release.attachments.find(a => a.title === archiveName || a.filename === archiveName); if (!asset) { - const pattern = `_${platform}_${arch}.tar.gz`; + const pattern = `_${platform}_${arch}${getArchiveExt(isWindows)}`; asset = release.attachments.find(a => (a.title || a.filename || "").endsWith(pattern)); } if (asset) { @@ -94,36 +110,53 @@ async function findReleaseAsset(platform, arch) { return url; } } - } catch (e) {} + } catch (e) { + console.log(`Warning: GitLink API failed: ${e.message}, trying GitHub...`); + } - return `${RELEASE_BASE}/api/${REPO_OWNER}/${REPO_NAME}/releases/${tagName}/assets/${archiveName}`; + return `${GITHUB_RELEASE_BASE}/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}/releases/download/${tagName}/${archiveName}`; } -async function downloadAndExtract(url, destDir, platform) { +async function downloadAndExtract(url, destDir, isWindows) { + console.log(`Downloading ${BINARY_NAME} from ${url}...`); + const data = await fetch(url); - const archivePath = path.join(destDir, "download.tar.gz"); + console.log(`Downloaded ${(data.length / 1024 / 1024).toFixed(1)} MB`); + + const archivePath = path.join(destDir, "download" + getArchiveExt(isWindows)); fs.writeFileSync(archivePath, data); - execSync(`tar -xzf "${archivePath}" -C "${destDir}"`, { stdio: "pipe" }); + + if (isWindows) { + execSync( + `powershell -NoProfile -Command "Expand-Archive -Force -Path '${archivePath}' -DestinationPath '${destDir}'"`, + { stdio: "pipe" } + ); + } else { + execSync(`tar -xzf "${archivePath}" -C "${destDir}"`, { stdio: "pipe" }); + } fs.unlinkSync(archivePath); - const binaryPath = path.join(destDir, BINARY_NAME); + const binaryName = getBinaryName(isWindows); + const binaryPath = path.join(destDir, binaryName); if (!fs.existsSync(binaryPath)) { const files = fs.readdirSync(destDir); for (const file of files) { - const subPath = path.join(destDir, file, BINARY_NAME); + const subPath = path.join(destDir, file, binaryName); if (fs.existsSync(subPath)) { fs.renameSync(subPath, binaryPath); break; } } } - if (!fs.existsSync(binaryPath)) throw new Error("Binary not found after extraction"); - fs.chmodSync(binaryPath, 0o755); + if (!fs.existsSync(binaryPath)) throw new Error(`Binary "${binaryName}" not found after extraction`); + if (!isWindows) fs.chmodSync(binaryPath, 0o755); } async function main() { - const { platform, arch } = getPlatformInfo(); + const { platform, arch, isWindows } = getPlatformInfo(); + console.log(`Platform: ${platform}-${arch}`); + const binDir = path.join(__dirname, "..", "bin"); if (!fs.existsSync(binDir)) { fs.mkdirSync(binDir, { recursive: true }); } - const binaryPath = path.join(binDir, BINARY_NAME); + const binaryPath = path.join(binDir, getBinaryName(isWindows)); if (fs.existsSync(binaryPath)) { try { const output = execSync(`"${binaryPath}" version`, { encoding: "utf-8", stdio: "pipe", timeout: 5000 }); @@ -131,19 +164,24 @@ async function main() { console.log(`${BINARY_NAME} v${VERSION} already installed.`); return; } + console.log(`Version mismatch (got: ${output.trim()}, want: ${VERSION}), updating...`); } catch (e) {} fs.unlinkSync(binaryPath); } try { - const downloadUrl = await findReleaseAsset(platform, arch); - await downloadAndExtract(downloadUrl, binDir, platform); - console.log(`${BINARY_NAME} v${VERSION} installed.`); + const downloadUrl = await findReleaseAsset(platform, arch, isWindows); + await downloadAndExtract(downloadUrl, binDir, isWindows); + console.log(`${BINARY_NAME} v${VERSION} installed successfully.`); } catch (err) { - // Don't fail npm install — binary can be installed later - console.warn(`⚠ ${BINARY_NAME} binary download failed: ${err.message}`); - console.warn(` Skills are installed. You can install the binary manually later:`); - console.warn(` npm run postinstall`); + console.error(`\n Failed to install ${BINARY_NAME}: ${err.message}`); + console.error( + `\n You can install manually:\n` + + ` 1. Download from https://www.gitlink.org.cn/${REPO_OWNER}/${REPO_NAME}/releases\n` + + ` 2. Extract and place the binary in: ${binDir}\n` + + ` 3. Or retry: npm run postinstall\n` + ); + process.exit(1); } } -- 2.34.1