diff --git a/.gitignore b/.gitignore index 72b9aa7..1d0eabe 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,9 @@ Thumbs.db # Go vendor/ + +# npm packaging +npm/bin/gitlink-cli +npm/*.tgz +npm/README.md +dist/ diff --git a/README.md b/README.md index ed6214f..6c428f9 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,111 @@ # gitlink-cli -[![GitLink](https://img.shields.io/badge/GitLink-wbtiger%2Fgitlink--cli-green)](https://www.gitlink.org.cn/wbtiger/gitlink-cli) +[![GitLink](https://img.shields.io/badge/GitLink-Gitlink%2Fgitlink--cli-green)](https://www.gitlink.org.cn/Gitlink/gitlink-cli) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) [![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) -**gitlink-cli** 是 [GitLink(确实开源)](https://www.gitlink.org.cn) 平台的官方命令行工具,提供高效的代码托管、协作开发和自动化能力。 +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/). -## 核心特性 +[Install](#installation--quick-start) · [AI Agent Skills](#ai-agent-skills) · [Auth](#首次使用) · [Commands](#使用示例) · [Contributing](#相关项目) -- 🚀 **三层命令体系**:Shortcuts(快捷命令)→ Raw API(全量接口)→ Config(配置管理) -- 🔐 **安全认证**:OS Keychain 存储,支持 Token 和用户名密码登录 -- 📦 **智能上下文**:自动从 git remote 解析 owner/repo,无需重复指定 -- 📊 **多格式输出**:JSON / Table / YAML,标准 Envelope 结构 -- 🤖 **AI 自动化**:11 个 Claude Code Agent Skills,支持 Issue 分类、PR Review、Release Notes 等工作流 -- 🌐 **双向同步**:支持 GitHub ↔ GitLink 代码同步(通过 gitlink-bisync) +## Why gitlink-cli? -## 快速开始 +- **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 +- **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 +- **Three-Layer Architecture** — Shortcuts (human & AI friendly) → Raw API (full coverage) → Config (configuration management) -### 安装 +## Features + +| Category | Capabilities | +|----------|-------------| +| 📦 Repo | List, create, fork, delete repositories, view repo info | +| 🐛 Issue | Create, update, close, comment on issues | +| 🔀 PR | Create, merge, review pull requests, view changed files | +| 🌿 Branch | Create, delete, protect branches | +| 🏷️ Release | Create, view, delete releases | +| 🏢 Org | Manage organizations, members, teams | +| 🔧 CI | View builds, logs, CI/CD operations | +| 🔍 Search | Search repositories, users | +| 👤 User | View user profiles and info | +| 🤖 Workflow | AI-powered issue triage, PR review, release notes | + +## Installation & Quick Start + +### Requirements + +Before you start, make sure you have: + +- Node.js 14+ (`npm`/`npx`) — for npm installation +- Go 1.21+ — only required for building from source + +### Quick Start (Human Users) + +> **Note for AI assistants:** If you are an AI Agent helping the user with installation, jump directly to [Quick Start (AI Agent)](#quick-start-ai-agent), which contains all the steps you need to complete. + +#### Install + +Choose **one** of the following methods: + +**Option 1 — From npm (recommended):** ```bash -# 从源码构建 -git clone https://www.gitlink.org.cn/wbtiger/gitlink-cli.git -cd gitlink-cli -make build +npm install -g @gitlink-ai/cli +``` -# 或安装到 PATH +**Option 2 — From source:** + +Requires Go 1.21+. + +```bash +git clone https://www.gitlink.org.cn/Gitlink/gitlink-cli.git +cd gitlink-cli make install ``` -**要求**:Go 1.21+ - -### 首次使用 +#### Configure & Use ```bash -# 1. 初始化配置 +# 1. Configure (one-time, interactive guided setup) gitlink-cli config init -# 2. 登录 +# 2. Log in gitlink-cli auth login -# 3. 验证登录 -gitlink-cli user +me +# 3. Start using +gitlink-cli repo +list +``` -# 4. 查看仓库 -gitlink-cli repo +info --owner Gitlink --repo forgeplus +### Quick Start (AI Agent) + +> The following steps are for AI Agents. Some steps require the user to complete actions in a browser. + +**Step 1 — Install** + +```bash +npm install -g @gitlink-ai/cli +``` + +**Step 2 — Configure** + +```bash +gitlink-cli config init +``` + +**Step 3 — Login** + +```bash +gitlink-cli auth login +``` + +**Step 4 — Verify** + +```bash +gitlink-cli user +me ``` ## 使用示例 diff --git a/npm/bin/cli.js b/npm/bin/cli.js new file mode 100755 index 0000000..a7c4b4c --- /dev/null +++ b/npm/bin/cli.js @@ -0,0 +1,21 @@ +#!/usr/bin/env node + +"use strict"; + +const path = require("path"); +const { execFileSync } = require("child_process"); + +const binaryPath = path.join(__dirname, "gitlink-cli"); + +try { + execFileSync(binaryPath, process.argv.slice(2), { stdio: "inherit" }); +} catch (err) { + if (err.status !== undefined) { + 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/package.json b/npm/package.json new file mode 100644 index 0000000..c588572 --- /dev/null +++ b/npm/package.json @@ -0,0 +1,41 @@ +{ + "name": "@gitlink-ai/cli", + "version": "0.1.0", + "description": "GitLink 平台官方命令行工具 — 代码托管、协作开发和自动化", + "bin": { + "gitlink-cli": "bin/cli.js" + }, + "scripts": { + "postinstall": "node scripts/install.js" + }, + "keywords": [ + "gitlink", + "cli", + "git", + "devops", + "code-hosting" + ], + "author": "GitLink ", + "license": "Apache-2.0", + "homepage": "https://www.gitlink.org.cn/Gitlink/gitlink-cli", + "repository": { + "type": "git", + "url": "https://www.gitlink.org.cn/Gitlink/gitlink-cli.git" + }, + "os": [ + "darwin", + "linux" + ], + "cpu": [ + "x64", + "arm64" + ], + "files": [ + "bin/", + "scripts/", + "README.md" + ], + "engines": { + "node": ">=14" + } +} diff --git a/npm/scripts/install.js b/npm/scripts/install.js new file mode 100644 index 0000000..0301520 --- /dev/null +++ b/npm/scripts/install.js @@ -0,0 +1,242 @@ +#!/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"; + +// 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} +const RELEASE_BASE = "https://www.gitlink.org.cn"; +const REPO_OWNER = "Gitlink"; +const REPO_NAME = "gitlink-cli"; + +function getPlatformInfo() { + const platform = os.platform(); + const arch = os.arch(); + + const platformMap = { + darwin: "darwin", + linux: "linux", + }; + + const archMap = { + x64: "amd64", + arm64: "arm64", + }; + + const goPlatform = platformMap[platform]; + const goArch = archMap[arch]; + + if (!goPlatform || !goArch) { + throw new Error( + `Unsupported platform: ${platform}-${arch}. ` + + `Supported: darwin-x64, darwin-arm64, linux-x64, linux-arm64` + ); + } + + return { platform: goPlatform, arch: goArch }; +} + +function getBinaryName(platform) { + return BINARY_NAME; +} + +function getArchiveName(platform, arch) { + return `${BINARY_NAME}_${VERSION}_${platform}_${arch}.tar.gz`; +} + +function fetch(url, options = {}) { + return new Promise((resolve, reject) => { + const maxRedirects = options.maxRedirects || 5; + let redirectCount = 0; + + function doRequest(currentUrl) { + const mod = currentUrl.startsWith("https") ? https : http; + const req = mod.get(currentUrl, (res) => { + // Follow redirects + if ( + (res.statusCode === 301 || + res.statusCode === 302 || + res.statusCode === 307 || + res.statusCode === 308) && + res.headers.location + ) { + redirectCount++; + if (redirectCount > maxRedirects) { + reject(new Error(`Too many redirects (max ${maxRedirects})`)); + return; + } + let redirectUrl = res.headers.location; + if (redirectUrl.startsWith("/")) { + const parsed = new URL(currentUrl); + redirectUrl = `${parsed.protocol}//${parsed.host}${redirectUrl}`; + } + doRequest(redirectUrl); + return; + } + + if (res.statusCode !== 200) { + reject( + new Error(`HTTP ${res.statusCode} when downloading ${currentUrl}`) + ); + return; + } + + if (options.json) { + let body = ""; + res.on("data", (chunk) => (body += chunk)); + res.on("end", () => { + try { + resolve(JSON.parse(body)); + } catch (e) { + reject(new Error(`Failed to parse JSON: ${e.message}`)); + } + }); + } else { + const chunks = []; + res.on("data", (chunk) => chunks.push(chunk)); + res.on("end", () => resolve(Buffer.concat(chunks))); + } + }); + req.on("error", reject); + req.setTimeout(60000, () => { + req.destroy(); + reject(new Error("Request timed out")); + }); + } + + doRequest(url); + }); +} + +async function findReleaseAsset(platform, arch) { + const archiveName = getArchiveName(platform, arch); + + // Try fetching release info from GitLink API + const apiUrl = `${RELEASE_BASE}/api/${REPO_OWNER}/${REPO_NAME}/releases.json`; + console.log(`Fetching release info from ${apiUrl}`); + + try { + const releases = await fetch(apiUrl, { json: true }); + + // Find the release matching our version + let release = null; + const tagName = `v${VERSION}`; + + if (Array.isArray(releases)) { + release = releases.find( + (r) => r.tag_name === tagName || r.tag_name === VERSION + ); + if (!release && releases.length > 0) { + // Fall back to latest release + release = releases[0]; + } + } else if (releases && releases.releases) { + const list = releases.releases; + release = list.find( + (r) => r.tag_name === tagName || r.tag_name === VERSION + ); + if (!release && list.length > 0) { + release = list[0]; + } + } + + if (release && release.attachments) { + const asset = release.attachments.find( + (a) => a.title === archiveName || a.filename === archiveName + ); + if (asset) { + // Return the download URL for this attachment + const downloadUrl = asset.url || `${RELEASE_BASE}/api/attachments/${asset.id}`; + return downloadUrl; + } + } + } catch (e) { + 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}`; +} + +async function downloadAndExtract(url, destDir) { + console.log(`Downloading ${BINARY_NAME} from ${url}...`); + + const data = await fetch(url); + const tarballPath = path.join(destDir, "download.tar.gz"); + + fs.writeFileSync(tarballPath, 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); + + // Find the binary in extracted files + const binaryName = getBinaryName(); + const binaryPath = path.join(destDir, binaryName); + + if (!fs.existsSync(binaryPath)) { + // It might be in a subdirectory + const files = fs.readdirSync(destDir); + for (const file of files) { + const subPath = path.join(destDir, file, binaryName); + if (fs.existsSync(subPath)) { + fs.renameSync(subPath, binaryPath); + break; + } + } + } + + if (!fs.existsSync(binaryPath)) { + throw new Error(`Binary "${binaryName}" not found after extraction`); + } + + // Make executable + fs.chmodSync(binaryPath, 0o755); + console.log(`Installed ${BINARY_NAME} to ${binaryPath}`); +} + +async function main() { + try { + const { platform, arch } = 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, getBinaryName()); + + // If binary already exists (pre-packed), skip download + if (fs.existsSync(binaryPath)) { + console.log(`${BINARY_NAME} binary already exists, skipping download.`); + return; + } + + const downloadUrl = await findReleaseAsset(platform, arch); + await downloadAndExtract(downloadUrl, binDir); + } catch (err) { + console.error(`\nFailed to install ${BINARY_NAME}: ${err.message}`); + console.error( + `\nYou 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 your PATH\n` + + ` 3. Or build from source: git clone && make build\n` + ); + process.exit(1); + } +} + +main(); diff --git a/scripts/build-npm.sh b/scripts/build-npm.sh new file mode 100755 index 0000000..e7a7266 --- /dev/null +++ b/scripts/build-npm.sh @@ -0,0 +1,78 @@ +#!/bin/bash +# Build multi-platform binaries and package for npm +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +NPM_DIR="$PROJECT_DIR/npm" +DIST_DIR="$PROJECT_DIR/dist" + +# Read version from npm/package.json +VERSION=$(node -p "require('$NPM_DIR/package.json').version") +MODULE="github.com/gitlink-org/gitlink-cli" +LDFLAGS="-s -w -X '${MODULE}/cmd.Version=${VERSION}'" +BINARY="gitlink-cli" + +echo "=== Building gitlink-cli v${VERSION} ===" + +# Clean +rm -rf "$DIST_DIR" +mkdir -p "$DIST_DIR" + +# Platforms to build: GOOS/GOARCH +PLATFORMS=( + "darwin/amd64" + "darwin/arm64" + "linux/amd64" + "linux/arm64" +) + +cd "$PROJECT_DIR" + +for PLATFORM in "${PLATFORMS[@]}"; do + GOOS="${PLATFORM%/*}" + GOARCH="${PLATFORM#*/}" + OUTPUT_NAME="${BINARY}" + + echo "Building ${GOOS}/${GOARCH}..." + + ARCHIVE_DIR="${DIST_DIR}/${BINARY}_${VERSION}_${GOOS}_${GOARCH}" + mkdir -p "$ARCHIVE_DIR" + + 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") + + echo " -> dist/${ARCHIVE_NAME}" + rm -rf "$ARCHIVE_DIR" +done + +echo "" +echo "=== Build complete ===" +echo "Archives in dist/:" +ls -lh "$DIST_DIR"/*.tar.gz + +echo "" +echo "=== Packaging npm ===" + +# Copy README to npm dir +cp "$PROJECT_DIR/README.md" "$NPM_DIR/README.md" + +# Ensure bin dir exists and wrapper is executable +chmod +x "$NPM_DIR/bin/gitlink-cli" + +echo "" +echo "=== Done ===" +echo "" +echo "Next steps:" +echo " 1. Upload dist/*.tar.gz to GitLink Release v${VERSION}" +echo " URL: https://www.gitlink.org.cn/Gitlink/gitlink-cli/releases" +echo "" +echo " 2. Publish npm package:" +echo " cd npm && npm publish --access public" +echo "" +echo " Or for local-packed npm (includes binary for current platform):" +echo " ./scripts/pack-local.sh" diff --git a/scripts/pack-local.sh b/scripts/pack-local.sh new file mode 100755 index 0000000..80febff --- /dev/null +++ b/scripts/pack-local.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# Pack npm package with the binary for the current platform pre-included. +# This way npm install does NOT need to download anything. +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +NPM_DIR="$PROJECT_DIR/npm" + +MODULE="github.com/gitlink-org/gitlink-cli" +VERSION=$(node -p "require('$NPM_DIR/package.json').version") +LDFLAGS="-s -w -X '${MODULE}/cmd.Version=${VERSION}'" +BINARY="gitlink-cli" + +echo "=== Building gitlink-cli for current platform ===" + +cd "$PROJECT_DIR" + +# Build for current platform +CGO_ENABLED=0 go build -ldflags "$LDFLAGS" -o "$NPM_DIR/bin/${BINARY}" . + +chmod +x "$NPM_DIR/bin/${BINARY}" +cp "$PROJECT_DIR/README.md" "$NPM_DIR/README.md" + +echo "Binary size: $(du -h "$NPM_DIR/bin/${BINARY}" | cut -f1)" + +echo "" +echo "=== Creating npm tarball ===" +cd "$NPM_DIR" +npm pack + +echo "" +echo "=== Done ===" +echo "To install locally: npm install -g gitlink-ai-cli-${VERSION}.tgz" +echo "To publish: cd npm && npm publish --access public"