diff --git a/README.md b/README.md index 28b7a00..def5859 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Go Version](https://img.shields.io/badge/Go-1.21%2B-blue.svg)](https://golang.org) [![npm version](https://img.shields.io/npm/v/@gitlink-ai/cli.svg)](https://www.npmjs.com/package/@gitlink-ai/cli) -The official [GitLink(确实开源)](https://www.gitlink.org.cn) CLI tool — built for humans and AI Agents. Covers repository management, issue tracking, pull requests, CI/CD, and AI-powered workflows, with 40+ commands and 11 AI Agent [Skills](./skills/). +The official [GitLink(确实开源)](https://www.gitlink.org.cn) CLI tool — built for humans and AI Agents. Supports **macOS, Linux, and Windows**. Covers repository management, issue tracking, pull requests, CI/CD, and AI-powered workflows, with 40+ commands and 11 AI Agent [Skills](./skills/). [Install](#installation--quick-start) · [AI Agent Skills](#ai-agent-skills) · [Auth](#首次使用) · [Commands](#使用示例) · [Contributing](#相关项目) @@ -14,6 +14,7 @@ The official [GitLink(确实开源)](https://www.gitlink.org.cn) CLI tool - **Agent-Native Design** — 11 structured [Skills](./skills/) out of the box, compatible with Claude Code — Agents can operate GitLink with zero extra setup - **Wide Coverage** — Repository, Issue, PR, Branch, Release, CI, Org, Search, User — all core domains covered - **AI-Friendly & Optimized** — Every command is tested with real Agents, featuring concise parameters, smart defaults, and structured output +- **Cross-Platform** — Runs on macOS, Linux, and Windows (x64/arm64), install via `npm` in one command - **Open Source, Zero Barriers** — Apache 2.0 license, ready to use, just `npm install` - **Up and Running in 3 Minutes** — Interactive login, from install to first API call in just 3 steps - **Secure & Controllable** — OS-native keychain credential storage, auto git remote context resolution @@ -41,6 +42,7 @@ The official [GitLink(确实开源)](https://www.gitlink.org.cn) CLI tool Before you start, make sure you have: - Node.js 14+ (`npm`/`npx`) — for npm installation +- Supported platforms: macOS, Linux, Windows (x64/arm64) - Go 1.21+ — only required for building from source ### Quick Start (Human Users) @@ -74,6 +76,8 @@ make install npx skills add ./skills -y -g ``` +> **Windows 用户注意:** 请在 PowerShell 或 CMD 中运行 `npm install -g @gitlink-ai/cli`。从源码构建请使用 `go install .` 代替 `make install`。 + #### Configure & Use ```bash @@ -343,6 +347,10 @@ A: 重新登录: gitlink-cli auth login ``` +### Q: Windows 上凭证存储在哪里? + +A: gitlink-cli 使用 Windows Credential Manager 安全存储 Token。如果 Credential Manager 不可用,会自动降级到文件存储 (`~/.config/gitlink-cli/credentials`)。 + ### Q: 如何查看完整的 API 参考? A: 查看 [skills/gitlink-shared/REFERENCE.md](skills/gitlink-shared/REFERENCE.md) diff --git a/internal/auth/token_store.go b/internal/auth/token_store.go index e8df8e8..35b260c 100644 --- a/internal/auth/token_store.go +++ b/internal/auth/token_store.go @@ -2,6 +2,7 @@ package auth import ( "os" + "path/filepath" "github.com/zalando/go-keyring" ) @@ -41,15 +42,15 @@ func DeleteToken() error { func credentialPath() string { home, _ := os.UserHomeDir() - return home + "/.config/gitlink-cli/credentials" + return filepath.Join(home, ".config", "gitlink-cli", "credentials") } func storeTokenFile(token string) error { - dir := credentialPath() - if err := os.MkdirAll(dir[:len(dir)-len("/credentials")], 0700); err != nil { + p := credentialPath() + if err := os.MkdirAll(filepath.Dir(p), 0700); err != nil { return err } - return os.WriteFile(dir, []byte(token), 0600) + return os.WriteFile(p, []byte(token), 0600) } func loadTokenFile() (string, error) { diff --git a/npm/bin/cli.js b/npm/bin/cli.js index a7c4b4c..474d69d 100755 --- a/npm/bin/cli.js +++ b/npm/bin/cli.js @@ -5,7 +5,8 @@ const path = require("path"); const { execFileSync } = require("child_process"); -const binaryPath = path.join(__dirname, "gitlink-cli"); +const ext = process.platform === "win32" ? ".exe" : ""; +const binaryPath = path.join(__dirname, "gitlink-cli" + ext); try { execFileSync(binaryPath, process.argv.slice(2), { stdio: "inherit" }); diff --git a/npm/package.json b/npm/package.json index 8e153df..aba6543 100644 --- a/npm/package.json +++ b/npm/package.json @@ -1,6 +1,6 @@ { "name": "@gitlink-ai/cli", - "version": "0.1.1", + "version": "0.1.2", "description": "GitLink 平台官方命令行工具 — 代码托管、协作开发和自动化", "bin": { "gitlink-cli": "bin/cli.js" @@ -24,7 +24,8 @@ }, "os": [ "darwin", - "linux" + "linux", + "win32" ], "cpu": [ "x64", diff --git a/npm/scripts/install.js b/npm/scripts/install.js index 0301520..23137d3 100644 --- a/npm/scripts/install.js +++ b/npm/scripts/install.js @@ -27,6 +27,7 @@ function getPlatformInfo() { const platformMap = { darwin: "darwin", linux: "linux", + win32: "windows", }; const archMap = { @@ -40,19 +41,20 @@ function getPlatformInfo() { if (!goPlatform || !goArch) { throw new Error( `Unsupported platform: ${platform}-${arch}. ` + - `Supported: darwin-x64, darwin-arm64, linux-x64, linux-arm64` + `Supported: darwin-x64, darwin-arm64, linux-x64, linux-arm64, win32-x64, win32-arm64` ); } - return { platform: goPlatform, arch: goArch }; + return { platform: goPlatform, arch: goArch, isWindows: platform === "win32" }; } function getBinaryName(platform) { - return BINARY_NAME; + return platform === "windows" ? BINARY_NAME + ".exe" : BINARY_NAME; } function getArchiveName(platform, arch) { - return `${BINARY_NAME}_${VERSION}_${platform}_${arch}.tar.gz`; + const ext = platform === "windows" ? ".zip" : ".tar.gz"; + return `${BINARY_NAME}_${VERSION}_${platform}_${arch}${ext}`; } function fetch(url, options = {}) { @@ -169,21 +171,30 @@ async function findReleaseAsset(platform, arch) { return `${RELEASE_BASE}/api/${REPO_OWNER}/${REPO_NAME}/releases/${tagName}/assets/${archiveName}`; } -async function downloadAndExtract(url, destDir) { +async function downloadAndExtract(url, destDir, platform) { console.log(`Downloading ${BINARY_NAME} from ${url}...`); const data = await fetch(url); - const tarballPath = path.join(destDir, "download.tar.gz"); + const isWindows = platform === "windows"; + const archiveExt = isWindows ? "download.zip" : "download.tar.gz"; + const archivePath = path.join(destDir, archiveExt); - fs.writeFileSync(tarballPath, data); + fs.writeFileSync(archivePath, data); console.log(`Downloaded ${(data.length / 1024 / 1024).toFixed(1)} MB`); - // Extract using tar - execSync(`tar -xzf "${tarballPath}" -C "${destDir}"`, { stdio: "pipe" }); - fs.unlinkSync(tarballPath); + // Extract + 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); // Find the binary in extracted files - const binaryName = getBinaryName(); + const binaryName = getBinaryName(platform); const binaryPath = path.join(destDir, binaryName); if (!fs.existsSync(binaryPath)) { @@ -202,8 +213,10 @@ async function downloadAndExtract(url, destDir) { throw new Error(`Binary "${binaryName}" not found after extraction`); } - // Make executable - fs.chmodSync(binaryPath, 0o755); + // Make executable (not needed on Windows) + if (!isWindows) { + fs.chmodSync(binaryPath, 0o755); + } console.log(`Installed ${BINARY_NAME} to ${binaryPath}`); } @@ -217,7 +230,7 @@ async function main() { fs.mkdirSync(binDir, { recursive: true }); } - const binaryPath = path.join(binDir, getBinaryName()); + const binaryPath = path.join(binDir, getBinaryName(platform)); // If binary already exists (pre-packed), skip download if (fs.existsSync(binaryPath)) { @@ -226,7 +239,7 @@ async function main() { } const downloadUrl = await findReleaseAsset(platform, arch); - await downloadAndExtract(downloadUrl, binDir); + await downloadAndExtract(downloadUrl, binDir, platform); } catch (err) { console.error(`\nFailed to install ${BINARY_NAME}: ${err.message}`); console.error( diff --git a/scripts/build-npm.sh b/scripts/build-npm.sh index a516b78..d2ea8f2 100755 --- a/scripts/build-npm.sh +++ b/scripts/build-npm.sh @@ -25,6 +25,8 @@ PLATFORMS=( "darwin/arm64" "linux/amd64" "linux/arm64" + "windows/amd64" + "windows/arm64" ) cd "$PROJECT_DIR" @@ -33,6 +35,9 @@ for PLATFORM in "${PLATFORMS[@]}"; do GOOS="${PLATFORM%/*}" GOARCH="${PLATFORM#*/}" OUTPUT_NAME="${BINARY}" + if [ "$GOOS" = "windows" ]; then + OUTPUT_NAME="${BINARY}.exe" + fi echo "Building ${GOOS}/${GOARCH}..." @@ -42,9 +47,14 @@ for PLATFORM in "${PLATFORMS[@]}"; do CGO_ENABLED=0 GOOS="$GOOS" GOARCH="$GOARCH" \ go build -ldflags "$LDFLAGS" -o "${ARCHIVE_DIR}/${OUTPUT_NAME}" . - # Create tar.gz archive - ARCHIVE_NAME="${BINARY}_${VERSION}_${GOOS}_${GOARCH}.tar.gz" - (cd "$DIST_DIR" && tar -czf "$ARCHIVE_NAME" -C "${ARCHIVE_DIR}" "$OUTPUT_NAME") + # Create archive (zip for Windows, tar.gz for others) + if [ "$GOOS" = "windows" ]; then + ARCHIVE_NAME="${BINARY}_${VERSION}_${GOOS}_${GOARCH}.zip" + (cd "${ARCHIVE_DIR}" && zip -q "../${ARCHIVE_NAME}" "$OUTPUT_NAME") + else + ARCHIVE_NAME="${BINARY}_${VERSION}_${GOOS}_${GOARCH}.tar.gz" + (cd "$DIST_DIR" && tar -czf "$ARCHIVE_NAME" -C "${ARCHIVE_DIR}" "$OUTPUT_NAME") + fi echo " -> dist/${ARCHIVE_NAME}" rm -rf "$ARCHIVE_DIR" @@ -53,7 +63,7 @@ done echo "" echo "=== Build complete ===" echo "Archives in dist/:" -ls -lh "$DIST_DIR"/*.tar.gz +ls -lh "$DIST_DIR"/*.tar.gz "$DIST_DIR"/*.zip 2>/dev/null echo "" echo "=== Packaging npm ==="