feat: initial implementation of gitee-cli
- auth: login/logout/status with Personal Access Token - repo: list/create/view/clone/delete - issue: list/create/view/close/comment - pr: list/create/view/merge/close - release: list/create - org: list - api: raw API call with --field/--query/--paginate Features: - TypeScript + ESM - commander.js CLI parsing - Node built-in fetch (no axios) - Auto-detect owner/repo from git remote (gitee.com) - GITEE_TOKEN env var priority over config file - --json flag for machine-readable output - --page/--per-page pagination support - Friendly error messages (401/403/404) - Config stored at ~/.config/gitee-cli/config.json
This commit is contained in:
commit
d44e6d049c
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
*.js.map
|
||||||
4
.npmignore
Normal file
4
.npmignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
src/
|
||||||
|
*.ts
|
||||||
|
tsconfig.json
|
||||||
|
.gitignore
|
||||||
177
README.md
Normal file
177
README.md
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
# gitee-cli
|
||||||
|
|
||||||
|
**Gitee (码云) 命令行工具** — 类似 `gh`,但面向 Gitee。
|
||||||
|
|
||||||
|
A command-line tool for Gitee (码云) — like `gh`, but for Gitee.
|
||||||
|
|
||||||
|
[](https://www.npmjs.com/package/gitee-cli)
|
||||||
|
[](https://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 安装 / Installation
|
||||||
|
|
||||||
|
### 全局安装 (Recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g gitee-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
### 从源码构建 / Build from source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/shazhou-ww/gitee-cli.git
|
||||||
|
cd gitee-cli
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
npm link
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 认证 / Authentication
|
||||||
|
|
||||||
|
gitee-cli 使用 Gitee Personal Access Token 认证。
|
||||||
|
|
||||||
|
1. 前往 https://gitee.com/profile/personal_access_tokens 创建 Token
|
||||||
|
2. 运行登录命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gitee auth login
|
||||||
|
```
|
||||||
|
|
||||||
|
### 环境变量 / Environment Variable
|
||||||
|
|
||||||
|
`GITEE_TOKEN` 优先级高于配置文件:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export GITEE_TOKEN=your_token_here
|
||||||
|
```
|
||||||
|
|
||||||
|
### 命令 / Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gitee auth login # 交互式登录
|
||||||
|
gitee auth logout # 清除认证
|
||||||
|
gitee auth status # 查看当前认证状态
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 命令列表 / Commands
|
||||||
|
|
||||||
|
### 仓库 / Repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gitee repo list # 列出我的仓库
|
||||||
|
gitee repo list --owner <user> # 列出指定用户的仓库
|
||||||
|
gitee repo list --type private # 只列出私有仓库
|
||||||
|
gitee repo create <name> # 创建仓库
|
||||||
|
gitee repo create <name> --private # 创建私有仓库
|
||||||
|
gitee repo create <name> --description "..."
|
||||||
|
gitee repo view # 查看当前仓库 (自动检测)
|
||||||
|
gitee repo view <owner/repo> # 查看指定仓库
|
||||||
|
gitee repo clone <owner/repo> # Clone 仓库
|
||||||
|
gitee repo delete <owner/repo> # 删除仓库 (需确认)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gitee issue list # 列出 issues (当前仓库)
|
||||||
|
gitee issue list --repo <owner/repo> # 指定仓库
|
||||||
|
gitee issue list --state closed # 列出已关闭的 issues
|
||||||
|
gitee issue create --title "Bug fix" # 创建 issue
|
||||||
|
gitee issue create --title "..." --body "..." --repo <owner/repo>
|
||||||
|
gitee issue view <number> # 查看 issue 详情
|
||||||
|
gitee issue close <number> # 关闭 issue
|
||||||
|
gitee issue comment <number> --body "..." # 评论
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pull Request
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gitee pr list # 列出 PRs (当前仓库)
|
||||||
|
gitee pr list --state merged # 已合并的 PRs
|
||||||
|
gitee pr create --title "feat: xxx" --head feature-branch
|
||||||
|
gitee pr create --title "..." --head <branch> --base master --body "..."
|
||||||
|
gitee pr view <number> # 查看 PR 详情
|
||||||
|
gitee pr merge <number> # 合并 PR
|
||||||
|
gitee pr merge <number> --method squash # Squash 合并
|
||||||
|
gitee pr close <number> # 关闭 PR
|
||||||
|
```
|
||||||
|
|
||||||
|
### Release
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gitee release list # 列出 releases
|
||||||
|
gitee release list --repo <owner/repo>
|
||||||
|
gitee release create --tag v1.0.0 --name "v1.0.0 Release"
|
||||||
|
gitee release create --tag v1.0.0 --name "..." --body "Release notes"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 组织 / Organization
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gitee org list # 列出我加入的组织
|
||||||
|
```
|
||||||
|
|
||||||
|
### 通用 API / Raw API
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gitee api GET /v5/emojis # 不需要认证
|
||||||
|
gitee api GET /v5/user # 获取当前用户
|
||||||
|
gitee api GET /v5/repos/owner/repo
|
||||||
|
gitee api POST /v5/user/repos --field name=myrepo --field private=true
|
||||||
|
gitee api GET /v5/repos/owner/repo/issues --query state=open
|
||||||
|
gitee api GET /v5/repos/owner/repo/issues --paginate # 自动翻页
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 通用选项 / Global Options
|
||||||
|
|
||||||
|
| 选项 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `--json` | 输出原始 JSON(方便脚本/AI 解析) |
|
||||||
|
| `--repo <owner/repo>` | 指定仓库(省略时自动检测 git remote) |
|
||||||
|
| `--page <n>` | 分页页码(默认 1) |
|
||||||
|
| `--per-page <n>` | 每页条数(默认 20,最大 100) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 自动检测仓库 / Auto-detect Repository
|
||||||
|
|
||||||
|
当你在一个 Gitee 仓库目录内运行命令时,`--repo` 参数可以省略,gitee-cli 会自动从 `git remote` 检测 `owner/repo`。
|
||||||
|
|
||||||
|
支持两种 remote 格式:
|
||||||
|
- `https://gitee.com/owner/repo.git`
|
||||||
|
- `git@gitee.com:owner/repo.git`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 配置文件 / Config File
|
||||||
|
|
||||||
|
Token 存储在 `~/.config/gitee-cli/config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "your_token",
|
||||||
|
"username": "your_username"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 技术栈 / Tech Stack
|
||||||
|
|
||||||
|
- Node.js 18+ (ESM)
|
||||||
|
- TypeScript
|
||||||
|
- commander.js
|
||||||
|
- Node 内置 `fetch` (无 axios 依赖)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
609
package-lock.json
generated
Normal file
609
package-lock.json
generated
Normal file
@ -0,0 +1,609 @@
|
|||||||
|
{
|
||||||
|
"name": "gitee-cli",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "gitee-cli",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "^12.1.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"gitee": "dist/cli.js"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.14.0",
|
||||||
|
"tsx": "^4.15.7",
|
||||||
|
"typescript": "^5.5.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"aix"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==",
|
||||||
|
"cpu": [
|
||||||
|
"mips64el"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openharmony-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openharmony"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"sunos"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "20.19.37",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz",
|
||||||
|
"integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/commander": {
|
||||||
|
"version": "12.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
|
||||||
|
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"esbuild": "bin/esbuild"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@esbuild/aix-ppc64": "0.27.4",
|
||||||
|
"@esbuild/android-arm": "0.27.4",
|
||||||
|
"@esbuild/android-arm64": "0.27.4",
|
||||||
|
"@esbuild/android-x64": "0.27.4",
|
||||||
|
"@esbuild/darwin-arm64": "0.27.4",
|
||||||
|
"@esbuild/darwin-x64": "0.27.4",
|
||||||
|
"@esbuild/freebsd-arm64": "0.27.4",
|
||||||
|
"@esbuild/freebsd-x64": "0.27.4",
|
||||||
|
"@esbuild/linux-arm": "0.27.4",
|
||||||
|
"@esbuild/linux-arm64": "0.27.4",
|
||||||
|
"@esbuild/linux-ia32": "0.27.4",
|
||||||
|
"@esbuild/linux-loong64": "0.27.4",
|
||||||
|
"@esbuild/linux-mips64el": "0.27.4",
|
||||||
|
"@esbuild/linux-ppc64": "0.27.4",
|
||||||
|
"@esbuild/linux-riscv64": "0.27.4",
|
||||||
|
"@esbuild/linux-s390x": "0.27.4",
|
||||||
|
"@esbuild/linux-x64": "0.27.4",
|
||||||
|
"@esbuild/netbsd-arm64": "0.27.4",
|
||||||
|
"@esbuild/netbsd-x64": "0.27.4",
|
||||||
|
"@esbuild/openbsd-arm64": "0.27.4",
|
||||||
|
"@esbuild/openbsd-x64": "0.27.4",
|
||||||
|
"@esbuild/openharmony-arm64": "0.27.4",
|
||||||
|
"@esbuild/sunos-x64": "0.27.4",
|
||||||
|
"@esbuild/win32-arm64": "0.27.4",
|
||||||
|
"@esbuild/win32-ia32": "0.27.4",
|
||||||
|
"@esbuild/win32-x64": "0.27.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-tsconfig": {
|
||||||
|
"version": "4.13.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz",
|
||||||
|
"integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"resolve-pkg-maps": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/resolve-pkg-maps": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx": {
|
||||||
|
"version": "4.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
||||||
|
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"esbuild": "~0.27.0",
|
||||||
|
"get-tsconfig": "^4.7.5"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"tsx": "dist/cli.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "6.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
package.json
Normal file
28
package.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "gitee-cli",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Gitee (码云) command-line tool — like gh, but for Gitee",
|
||||||
|
"type": "module",
|
||||||
|
"bin": {
|
||||||
|
"gitee": "./dist/cli.js"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"dev": "tsx src/cli.ts",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
|
"keywords": ["gitee", "cli", "git", "vcs"],
|
||||||
|
"author": "小橘",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "^12.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.14.0",
|
||||||
|
"tsx": "^4.15.7",
|
||||||
|
"typescript": "^5.5.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
141
src/api.ts
Normal file
141
src/api.ts
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import { execSync } from 'child_process';
|
||||||
|
|
||||||
|
const API_BASE = 'https://gitee.com/api/v5';
|
||||||
|
|
||||||
|
export interface RequestOptions {
|
||||||
|
method?: string;
|
||||||
|
params?: Record<string, string | number | boolean | undefined>;
|
||||||
|
body?: Record<string, unknown>;
|
||||||
|
token?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ApiError extends Error {
|
||||||
|
constructor(
|
||||||
|
public readonly status: number,
|
||||||
|
message: string
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'ApiError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function apiRequest<T = unknown>(
|
||||||
|
path: string,
|
||||||
|
options: RequestOptions = {}
|
||||||
|
): Promise<T> {
|
||||||
|
const { method = 'GET', params = {}, body, token } = options;
|
||||||
|
|
||||||
|
// Build URL
|
||||||
|
const url = new URL(path.startsWith('http') ? path : `${API_BASE}${path}`);
|
||||||
|
|
||||||
|
// Add token to params if available
|
||||||
|
if (token) {
|
||||||
|
url.searchParams.set('access_token', token);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add other query params
|
||||||
|
for (const [key, val] of Object.entries(params)) {
|
||||||
|
if (val !== undefined && val !== null) {
|
||||||
|
url.searchParams.set(key, String(val));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'User-Agent': 'gitee-cli/0.1.0',
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchOptions: RequestInit = {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (body && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) {
|
||||||
|
fetchOptions.body = JSON.stringify(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url.toString(), fetchOptions);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
let message = `HTTP ${response.status} ${response.statusText}`;
|
||||||
|
try {
|
||||||
|
const errBody = await response.json() as Record<string, unknown>;
|
||||||
|
if (errBody.message) {
|
||||||
|
message = String(errBody.message);
|
||||||
|
} else if (errBody.error) {
|
||||||
|
message = String(errBody.error);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
throw new ApiError(response.status, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 204 No Content
|
||||||
|
if (response.status === 204) {
|
||||||
|
return undefined as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json() as Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect owner/repo from current git remote (gitee.com)
|
||||||
|
*/
|
||||||
|
export function detectRepo(): string | undefined {
|
||||||
|
try {
|
||||||
|
const remote = execSync('git remote get-url origin 2>/dev/null', {
|
||||||
|
encoding: 'utf-8',
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
|
}).trim();
|
||||||
|
|
||||||
|
// Match: https://gitee.com/owner/repo or git@gitee.com:owner/repo.git
|
||||||
|
const httpsMatch = remote.match(/https:\/\/gitee\.com\/([^/]+\/[^/]+?)(?:\.git)?$/);
|
||||||
|
const sshMatch = remote.match(/git@gitee\.com:([^/]+\/[^/]+?)(?:\.git)?$/);
|
||||||
|
|
||||||
|
return (httpsMatch || sshMatch)?.[1];
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve owner/repo: explicit arg > auto-detect > error
|
||||||
|
*/
|
||||||
|
export function resolveRepo(explicit?: string): string {
|
||||||
|
if (explicit) return explicit;
|
||||||
|
const detected = detectRepo();
|
||||||
|
if (detected) {
|
||||||
|
return detected;
|
||||||
|
}
|
||||||
|
console.error('Error: Could not determine repository. Use --repo <owner/repo> or run inside a gitee.com git directory.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDate(dateStr: string): string {
|
||||||
|
const d = new Date(dateStr);
|
||||||
|
return d.toLocaleDateString('zh-CN', {
|
||||||
|
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||||
|
hour: '2-digit', minute: '2-digit',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleError(err: unknown): never {
|
||||||
|
if (err instanceof ApiError) {
|
||||||
|
if (err.status === 401) {
|
||||||
|
console.error('Error: Unauthorized. Please run `gitee auth login` first or set GITEE_TOKEN.');
|
||||||
|
} else if (err.status === 404) {
|
||||||
|
console.error('Error: Not found. Check repository name and your access permissions.');
|
||||||
|
} else if (err.status === 403) {
|
||||||
|
console.error('Error: Forbidden. You may not have permission for this operation.');
|
||||||
|
} else {
|
||||||
|
console.error(`Error: ${err.message}`);
|
||||||
|
}
|
||||||
|
} else if (err instanceof Error) {
|
||||||
|
console.error(`Error: ${err.message}`);
|
||||||
|
} else {
|
||||||
|
console.error('Unknown error occurred');
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
26
src/cli.ts
Normal file
26
src/cli.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { Command } from 'commander';
|
||||||
|
import { registerAuthCommands } from './commands/auth.js';
|
||||||
|
import { registerRepoCommands } from './commands/repo.js';
|
||||||
|
import { registerIssueCommands } from './commands/issue.js';
|
||||||
|
import { registerPrCommands } from './commands/pr.js';
|
||||||
|
import { registerReleaseCommands } from './commands/release.js';
|
||||||
|
import { registerOrgCommands } from './commands/org.js';
|
||||||
|
import { registerApiCommand } from './commands/api.js';
|
||||||
|
|
||||||
|
const program = new Command();
|
||||||
|
|
||||||
|
program
|
||||||
|
.name('gitee')
|
||||||
|
.description('Gitee (码云) command-line tool — like gh, but for Gitee')
|
||||||
|
.version('0.1.0');
|
||||||
|
|
||||||
|
registerAuthCommands(program);
|
||||||
|
registerRepoCommands(program);
|
||||||
|
registerIssueCommands(program);
|
||||||
|
registerPrCommands(program);
|
||||||
|
registerReleaseCommands(program);
|
||||||
|
registerOrgCommands(program);
|
||||||
|
registerApiCommand(program);
|
||||||
|
|
||||||
|
program.parse(process.argv);
|
||||||
99
src/commands/api.ts
Normal file
99
src/commands/api.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import { Command } from 'commander';
|
||||||
|
import { getToken } from '../config.js';
|
||||||
|
import { apiRequest, handleError } from '../api.js';
|
||||||
|
|
||||||
|
export function registerApiCommand(program: Command): void {
|
||||||
|
program
|
||||||
|
.command('api <method> <path>')
|
||||||
|
.description('Make a raw API call to Gitee API v5')
|
||||||
|
.option('--field <key=value>', 'Set request field (repeatable)', (val: string, prev: string[]) => {
|
||||||
|
prev.push(val);
|
||||||
|
return prev;
|
||||||
|
}, [] as string[])
|
||||||
|
.option('--query <key=value>', 'Set query parameter (repeatable)', (val: string, prev: string[]) => {
|
||||||
|
prev.push(val);
|
||||||
|
return prev;
|
||||||
|
}, [] as string[])
|
||||||
|
.option('--no-auth', 'Skip authentication token')
|
||||||
|
.option('--paginate', 'Fetch all pages and combine results')
|
||||||
|
.action(async (method: string, path: string, opts: {
|
||||||
|
field: string[];
|
||||||
|
query: string[];
|
||||||
|
auth: boolean;
|
||||||
|
paginate?: boolean;
|
||||||
|
}) => {
|
||||||
|
const token = opts.auth !== false ? getToken() : undefined;
|
||||||
|
|
||||||
|
// Parse fields into body
|
||||||
|
const body: Record<string, string> = {};
|
||||||
|
for (const f of opts.field) {
|
||||||
|
const idx = f.indexOf('=');
|
||||||
|
if (idx === -1) {
|
||||||
|
console.error(`Error: Invalid field format "${f}". Use key=value.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
body[f.slice(0, idx)] = f.slice(idx + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse query params
|
||||||
|
const params: Record<string, string> = {};
|
||||||
|
for (const q of opts.query) {
|
||||||
|
const idx = q.indexOf('=');
|
||||||
|
if (idx === -1) {
|
||||||
|
console.error(`Error: Invalid query format "${q}". Use key=value.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
params[q.slice(0, idx)] = q.slice(idx + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize path
|
||||||
|
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const requestOpts = {
|
||||||
|
method: method.toUpperCase(),
|
||||||
|
token,
|
||||||
|
params: Object.keys(params).length > 0 ? params : undefined,
|
||||||
|
body: ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase()) && Object.keys(body).length > 0 ? body : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Resolve the API path:
|
||||||
|
// - If user passes /v5/... → use as-is (strip leading /v5 since API_BASE already has /v5)
|
||||||
|
// - If user passes /repos/... or /user/... → prepend nothing (API_BASE handles it)
|
||||||
|
// - Raw absolute URL → pass directly
|
||||||
|
let apiPath: string;
|
||||||
|
if (normalizedPath.startsWith('/v5/')) {
|
||||||
|
// strip /v5 prefix since apiRequest prepends API_BASE which already ends with /v5
|
||||||
|
apiPath = normalizedPath.slice(3); // remove /v5
|
||||||
|
} else {
|
||||||
|
apiPath = normalizedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.paginate) {
|
||||||
|
const allResults: unknown[] = [];
|
||||||
|
let page = 1;
|
||||||
|
while (true) {
|
||||||
|
const pageParams = { ...params, page, per_page: 100 };
|
||||||
|
const result = await apiRequest<unknown>(apiPath, {
|
||||||
|
...requestOpts,
|
||||||
|
params: pageParams,
|
||||||
|
});
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
allResults.push(...result);
|
||||||
|
if (result.length < 100) break;
|
||||||
|
page++;
|
||||||
|
} else {
|
||||||
|
console.log(JSON.stringify(result, null, 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(JSON.stringify(allResults, null, 2));
|
||||||
|
} else {
|
||||||
|
const result = await apiRequest<unknown>(apiPath, requestOpts);
|
||||||
|
console.log(JSON.stringify(result, null, 2));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
141
src/commands/auth.ts
Normal file
141
src/commands/auth.ts
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import { Command } from 'commander';
|
||||||
|
import * as readline from 'readline';
|
||||||
|
import { getConfig, saveConfig, clearConfig, getToken } from '../config.js';
|
||||||
|
import { apiRequest, handleError, ApiError } from '../api.js';
|
||||||
|
|
||||||
|
interface GiteeUser {
|
||||||
|
login: string;
|
||||||
|
name: string;
|
||||||
|
email?: string;
|
||||||
|
avatar_url?: string;
|
||||||
|
bio?: string;
|
||||||
|
public_repos?: number;
|
||||||
|
followers?: number;
|
||||||
|
following?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prompt(question: string, hidden = false): Promise<string> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hidden) {
|
||||||
|
// Hide input for token
|
||||||
|
process.stdout.write(question);
|
||||||
|
let value = '';
|
||||||
|
const stdin = process.stdin;
|
||||||
|
stdin.setRawMode?.(true);
|
||||||
|
stdin.resume();
|
||||||
|
stdin.setEncoding('utf-8');
|
||||||
|
stdin.on('data', (char: string) => {
|
||||||
|
if (char === '\n' || char === '\r' || char === '\u0003') {
|
||||||
|
stdin.setRawMode?.(false);
|
||||||
|
stdin.pause();
|
||||||
|
process.stdout.write('\n');
|
||||||
|
rl.close();
|
||||||
|
resolve(value);
|
||||||
|
} else if (char === '\u007F') {
|
||||||
|
if (value.length > 0) {
|
||||||
|
value = value.slice(0, -1);
|
||||||
|
process.stdout.write('\b \b');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
value += char;
|
||||||
|
process.stdout.write('*');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
rl.question(question, (answer) => {
|
||||||
|
rl.close();
|
||||||
|
resolve(answer.trim());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerAuthCommands(program: Command): void {
|
||||||
|
const auth = program
|
||||||
|
.command('auth')
|
||||||
|
.description('Manage authentication');
|
||||||
|
|
||||||
|
auth
|
||||||
|
.command('login')
|
||||||
|
.description('Authenticate with a Gitee Personal Access Token')
|
||||||
|
.option('--with-token', 'Read token from stdin (non-interactive)')
|
||||||
|
.action(async (opts: { withToken?: boolean }) => {
|
||||||
|
let token: string;
|
||||||
|
|
||||||
|
if (opts.withToken) {
|
||||||
|
// Read from stdin
|
||||||
|
const chunks: string[] = [];
|
||||||
|
for await (const chunk of process.stdin) {
|
||||||
|
chunks.push(chunk);
|
||||||
|
}
|
||||||
|
token = chunks.join('').trim();
|
||||||
|
} else {
|
||||||
|
console.log('Gitee CLI Authentication');
|
||||||
|
console.log('Get your token at: https://gitee.com/profile/personal_access_tokens');
|
||||||
|
console.log('');
|
||||||
|
token = await prompt('Enter your Gitee Personal Access Token: ', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
console.error('Error: Token cannot be empty.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the token
|
||||||
|
console.log('Verifying token...');
|
||||||
|
try {
|
||||||
|
const user = await apiRequest<GiteeUser>('/user', { token });
|
||||||
|
saveConfig({ token, username: user.login });
|
||||||
|
console.log(`✓ Logged in as ${user.login} (${user.name || user.login})`);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof ApiError && err.status === 401) {
|
||||||
|
console.error('Error: Invalid token. Please check your Personal Access Token.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
handleError(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
auth
|
||||||
|
.command('logout')
|
||||||
|
.description('Clear stored authentication credentials')
|
||||||
|
.action(() => {
|
||||||
|
clearConfig();
|
||||||
|
console.log('✓ Logged out. Token cleared.');
|
||||||
|
});
|
||||||
|
|
||||||
|
auth
|
||||||
|
.command('status')
|
||||||
|
.description('Show current authentication status')
|
||||||
|
.action(async () => {
|
||||||
|
const token = getToken();
|
||||||
|
if (!token) {
|
||||||
|
console.log('Not logged in.');
|
||||||
|
console.log('Run `gitee auth login` to authenticate.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromEnv = !!process.env.GITEE_TOKEN;
|
||||||
|
const config = getConfig();
|
||||||
|
|
||||||
|
console.log(`Token source: ${fromEnv ? 'GITEE_TOKEN (env)' : 'config file'}`);
|
||||||
|
if (config.username) {
|
||||||
|
console.log(`Cached username: ${config.username}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Verifying token...');
|
||||||
|
try {
|
||||||
|
const user = await apiRequest<GiteeUser>('/user', { token });
|
||||||
|
console.log(`✓ Authenticated as ${user.login} (${user.name || user.login})`);
|
||||||
|
if (user.email) console.log(`Email: ${user.email}`);
|
||||||
|
if (user.public_repos !== undefined) console.log(`Public repos: ${user.public_repos}`);
|
||||||
|
} catch {
|
||||||
|
console.log('✗ Token is invalid or expired.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
215
src/commands/issue.ts
Normal file
215
src/commands/issue.ts
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
import { Command } from 'commander';
|
||||||
|
import { getToken } from '../config.js';
|
||||||
|
import { apiRequest, resolveRepo, formatDate, handleError } from '../api.js';
|
||||||
|
|
||||||
|
interface GiteeIssue {
|
||||||
|
id: number;
|
||||||
|
number: string;
|
||||||
|
title: string;
|
||||||
|
state: string;
|
||||||
|
body?: string;
|
||||||
|
user?: { login: string };
|
||||||
|
assignee?: { login: string };
|
||||||
|
labels?: Array<{ name: string; color: string }>;
|
||||||
|
comments?: number;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
html_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerIssueCommands(program: Command): void {
|
||||||
|
const issue = program
|
||||||
|
.command('issue')
|
||||||
|
.description('Manage issues');
|
||||||
|
|
||||||
|
issue
|
||||||
|
.command('list')
|
||||||
|
.description('List issues in a repository')
|
||||||
|
.option('--repo <owner/repo>', 'Repository (owner/repo)')
|
||||||
|
.option('--state <state>', 'State: open|closed|all (default: open)', 'open')
|
||||||
|
.option('--page <n>', 'Page number', '1')
|
||||||
|
.option('--per-page <n>', 'Results per page', '20')
|
||||||
|
.option('--json', 'Output raw JSON')
|
||||||
|
.action(async (opts: { repo?: string; state?: string; page?: string; perPage?: string; json?: boolean }) => {
|
||||||
|
const token = getToken();
|
||||||
|
const repoName = resolveRepo(opts.repo);
|
||||||
|
const [owner, repo] = repoName.split('/');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const issues = await apiRequest<GiteeIssue[]>(`/repos/${owner}/${repo}/issues`, {
|
||||||
|
token,
|
||||||
|
params: {
|
||||||
|
state: opts.state || 'open',
|
||||||
|
page: opts.page || 1,
|
||||||
|
per_page: opts.perPage || 20,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (opts.json) {
|
||||||
|
console.log(JSON.stringify(issues, null, 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!issues.length) {
|
||||||
|
console.log('No issues found.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Issues in ${repoName}:\n`);
|
||||||
|
for (const issue of issues) {
|
||||||
|
const labels = issue.labels?.map(l => `[${l.name}]`).join(' ') || '';
|
||||||
|
const comments = issue.comments ? `💬 ${issue.comments}` : '';
|
||||||
|
console.log(` #${issue.number} ${issue.title} ${labels}`);
|
||||||
|
console.log(` ${issue.state} · by ${issue.user?.login || 'unknown'} · ${formatDate(issue.created_at)} ${comments}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
issue
|
||||||
|
.command('create')
|
||||||
|
.description('Create an issue')
|
||||||
|
.option('--repo <owner/repo>', 'Repository (owner/repo)')
|
||||||
|
.requiredOption('--title <title>', 'Issue title')
|
||||||
|
.option('--body <body>', 'Issue body/description')
|
||||||
|
.option('--assignee <username>', 'Assign to user')
|
||||||
|
.option('--json', 'Output raw JSON')
|
||||||
|
.action(async (opts: { repo?: string; title: string; body?: string; assignee?: string; json?: boolean }) => {
|
||||||
|
const token = getToken();
|
||||||
|
if (!token) {
|
||||||
|
console.error('Error: Authentication required. Run `gitee auth login` or set GITEE_TOKEN.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const repoName = resolveRepo(opts.repo);
|
||||||
|
const [owner, repo] = repoName.split('/');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const created = await apiRequest<GiteeIssue>(`/repos/${owner}/${repo}/issues`, {
|
||||||
|
method: 'POST',
|
||||||
|
token,
|
||||||
|
body: {
|
||||||
|
title: opts.title,
|
||||||
|
body: opts.body || '',
|
||||||
|
assignee: opts.assignee,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (opts.json) {
|
||||||
|
console.log(JSON.stringify(created, null, 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✓ Created issue #${created.number}: ${created.title}`);
|
||||||
|
console.log(` URL: ${created.html_url}`);
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
issue
|
||||||
|
.command('view <number>')
|
||||||
|
.description('View issue details')
|
||||||
|
.option('--repo <owner/repo>', 'Repository (owner/repo)')
|
||||||
|
.option('--json', 'Output raw JSON')
|
||||||
|
.action(async (number: string, opts: { repo?: string; json?: boolean }) => {
|
||||||
|
const token = getToken();
|
||||||
|
const repoName = resolveRepo(opts.repo);
|
||||||
|
const [owner, repo] = repoName.split('/');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const iss = await apiRequest<GiteeIssue>(`/repos/${owner}/${repo}/issues/${number}`, { token });
|
||||||
|
|
||||||
|
if (opts.json) {
|
||||||
|
console.log(JSON.stringify(iss, null, 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`#${iss.number} ${iss.title}`);
|
||||||
|
console.log(`─────────────────────────────`);
|
||||||
|
console.log(`State: ${iss.state}`);
|
||||||
|
console.log(`Author: ${iss.user?.login || 'unknown'}`);
|
||||||
|
if (iss.assignee) console.log(`Assignee: ${iss.assignee.login}`);
|
||||||
|
if (iss.labels?.length) console.log(`Labels: ${iss.labels.map(l => l.name).join(', ')}`);
|
||||||
|
console.log(`Created: ${formatDate(iss.created_at)}`);
|
||||||
|
console.log(`Updated: ${formatDate(iss.updated_at)}`);
|
||||||
|
console.log(`URL: ${iss.html_url}`);
|
||||||
|
if (iss.body) {
|
||||||
|
console.log(`\n${iss.body}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
issue
|
||||||
|
.command('close <number>')
|
||||||
|
.description('Close an issue')
|
||||||
|
.option('--repo <owner/repo>', 'Repository (owner/repo)')
|
||||||
|
.option('--json', 'Output raw JSON')
|
||||||
|
.action(async (number: string, opts: { repo?: string; json?: boolean }) => {
|
||||||
|
const token = getToken();
|
||||||
|
if (!token) {
|
||||||
|
console.error('Error: Authentication required. Run `gitee auth login` or set GITEE_TOKEN.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const repoName = resolveRepo(opts.repo);
|
||||||
|
const [owner, repo] = repoName.split('/');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updated = await apiRequest<GiteeIssue>(`/repos/${owner}/${repo}/issues/${number}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
token,
|
||||||
|
body: { state: 'closed', repo },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (opts.json) {
|
||||||
|
console.log(JSON.stringify(updated, null, 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✓ Closed issue #${number}`);
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
issue
|
||||||
|
.command('comment <number>')
|
||||||
|
.description('Add a comment to an issue')
|
||||||
|
.option('--repo <owner/repo>', 'Repository (owner/repo)')
|
||||||
|
.requiredOption('--body <comment>', 'Comment body')
|
||||||
|
.option('--json', 'Output raw JSON')
|
||||||
|
.action(async (number: string, opts: { repo?: string; body: string; json?: boolean }) => {
|
||||||
|
const token = getToken();
|
||||||
|
if (!token) {
|
||||||
|
console.error('Error: Authentication required. Run `gitee auth login` or set GITEE_TOKEN.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const repoName = resolveRepo(opts.repo);
|
||||||
|
const [owner, repo] = repoName.split('/');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const comment = await apiRequest<{ id: number; body: string; created_at: string }>(
|
||||||
|
`/repos/${owner}/${repo}/issues/${number}/comments`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
token,
|
||||||
|
body: { body: opts.body },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (opts.json) {
|
||||||
|
console.log(JSON.stringify(comment, null, 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✓ Comment added (id: ${comment.id})`);
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
62
src/commands/org.ts
Normal file
62
src/commands/org.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { Command } from 'commander';
|
||||||
|
import { getToken } from '../config.js';
|
||||||
|
import { apiRequest, handleError } from '../api.js';
|
||||||
|
|
||||||
|
interface GiteeOrg {
|
||||||
|
id: number;
|
||||||
|
login: string;
|
||||||
|
name?: string;
|
||||||
|
avatar_url?: string;
|
||||||
|
description?: string;
|
||||||
|
html_url?: string;
|
||||||
|
public_repos?: number;
|
||||||
|
members_count?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerOrgCommands(program: Command): void {
|
||||||
|
const org = program
|
||||||
|
.command('org')
|
||||||
|
.description('Manage organizations');
|
||||||
|
|
||||||
|
org
|
||||||
|
.command('list')
|
||||||
|
.description('List organizations for the authenticated user')
|
||||||
|
.option('--page <n>', 'Page number', '1')
|
||||||
|
.option('--per-page <n>', 'Results per page', '20')
|
||||||
|
.option('--json', 'Output raw JSON')
|
||||||
|
.action(async (opts: { page?: string; perPage?: string; json?: boolean }) => {
|
||||||
|
const token = getToken();
|
||||||
|
if (!token) {
|
||||||
|
console.error('Error: Authentication required. Run `gitee auth login` or set GITEE_TOKEN.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const orgs = await apiRequest<GiteeOrg[]>('/user/orgs', {
|
||||||
|
token,
|
||||||
|
params: {
|
||||||
|
page: opts.page || 1,
|
||||||
|
per_page: opts.perPage || 20,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (opts.json) {
|
||||||
|
console.log(JSON.stringify(orgs, null, 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!orgs.length) {
|
||||||
|
console.log('No organizations found.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Organizations:\n`);
|
||||||
|
for (const o of orgs) {
|
||||||
|
console.log(` ${o.login}${o.name && o.name !== o.login ? ` (${o.name})` : ''}`);
|
||||||
|
if (o.description) console.log(` ${o.description}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
233
src/commands/pr.ts
Normal file
233
src/commands/pr.ts
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
import { Command } from 'commander';
|
||||||
|
import { getToken } from '../config.js';
|
||||||
|
import { apiRequest, resolveRepo, formatDate, handleError } from '../api.js';
|
||||||
|
|
||||||
|
interface GiteePR {
|
||||||
|
id: number;
|
||||||
|
number: number;
|
||||||
|
title: string;
|
||||||
|
state: string;
|
||||||
|
body?: string;
|
||||||
|
user?: { login: string };
|
||||||
|
head?: { label: string; sha: string };
|
||||||
|
base?: { label: string; sha: string };
|
||||||
|
merged?: boolean;
|
||||||
|
merged_at?: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
html_url: string;
|
||||||
|
comments?: number;
|
||||||
|
commits?: number;
|
||||||
|
changed_files?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerPrCommands(program: Command): void {
|
||||||
|
const pr = program
|
||||||
|
.command('pr')
|
||||||
|
.description('Manage pull requests');
|
||||||
|
|
||||||
|
pr
|
||||||
|
.command('list')
|
||||||
|
.description('List pull requests')
|
||||||
|
.option('--repo <owner/repo>', 'Repository (owner/repo)')
|
||||||
|
.option('--state <state>', 'State: open|closed|merged|all (default: open)', 'open')
|
||||||
|
.option('--page <n>', 'Page number', '1')
|
||||||
|
.option('--per-page <n>', 'Results per page', '20')
|
||||||
|
.option('--json', 'Output raw JSON')
|
||||||
|
.action(async (opts: { repo?: string; state?: string; page?: string; perPage?: string; json?: boolean }) => {
|
||||||
|
const token = getToken();
|
||||||
|
const repoName = resolveRepo(opts.repo);
|
||||||
|
const [owner, repo] = repoName.split('/');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const prs = await apiRequest<GiteePR[]>(`/repos/${owner}/${repo}/pulls`, {
|
||||||
|
token,
|
||||||
|
params: {
|
||||||
|
state: opts.state || 'open',
|
||||||
|
page: opts.page || 1,
|
||||||
|
per_page: opts.perPage || 20,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (opts.json) {
|
||||||
|
console.log(JSON.stringify(prs, null, 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!prs.length) {
|
||||||
|
console.log('No pull requests found.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Pull requests in ${repoName}:\n`);
|
||||||
|
for (const p of prs) {
|
||||||
|
const head = p.head?.label || 'unknown';
|
||||||
|
const base = p.base?.label || 'unknown';
|
||||||
|
console.log(` #${p.number} ${p.title}`);
|
||||||
|
console.log(` ${p.state} · ${head} → ${base} · by ${p.user?.login || 'unknown'} · ${formatDate(p.created_at)}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
pr
|
||||||
|
.command('create')
|
||||||
|
.description('Create a pull request')
|
||||||
|
.option('--repo <owner/repo>', 'Repository (owner/repo)')
|
||||||
|
.requiredOption('--title <title>', 'PR title')
|
||||||
|
.requiredOption('--head <branch>', 'Source branch (head)')
|
||||||
|
.option('--base <branch>', 'Target branch (base)', 'master')
|
||||||
|
.option('--body <desc>', 'PR description')
|
||||||
|
.option('--json', 'Output raw JSON')
|
||||||
|
.action(async (opts: { repo?: string; title: string; head: string; base?: string; body?: string; json?: boolean }) => {
|
||||||
|
const token = getToken();
|
||||||
|
if (!token) {
|
||||||
|
console.error('Error: Authentication required. Run `gitee auth login` or set GITEE_TOKEN.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const repoName = resolveRepo(opts.repo);
|
||||||
|
const [owner, repo] = repoName.split('/');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const created = await apiRequest<GiteePR>(`/repos/${owner}/${repo}/pulls`, {
|
||||||
|
method: 'POST',
|
||||||
|
token,
|
||||||
|
body: {
|
||||||
|
title: opts.title,
|
||||||
|
head: opts.head,
|
||||||
|
base: opts.base || 'master',
|
||||||
|
body: opts.body || '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (opts.json) {
|
||||||
|
console.log(JSON.stringify(created, null, 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✓ Created PR #${created.number}: ${created.title}`);
|
||||||
|
console.log(` URL: ${created.html_url}`);
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
pr
|
||||||
|
.command('view <number>')
|
||||||
|
.description('View pull request details')
|
||||||
|
.option('--repo <owner/repo>', 'Repository (owner/repo)')
|
||||||
|
.option('--json', 'Output raw JSON')
|
||||||
|
.action(async (number: string, opts: { repo?: string; json?: boolean }) => {
|
||||||
|
const token = getToken();
|
||||||
|
const repoName = resolveRepo(opts.repo);
|
||||||
|
const [owner, repo] = repoName.split('/');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const p = await apiRequest<GiteePR>(`/repos/${owner}/${repo}/pulls/${number}`, { token });
|
||||||
|
|
||||||
|
if (opts.json) {
|
||||||
|
console.log(JSON.stringify(p, null, 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`#${p.number} ${p.title}`);
|
||||||
|
console.log(`─────────────────────────────`);
|
||||||
|
console.log(`State: ${p.state}${p.merged ? ' (merged)' : ''}`);
|
||||||
|
console.log(`Author: ${p.user?.login || 'unknown'}`);
|
||||||
|
console.log(`Head: ${p.head?.label || 'unknown'}`);
|
||||||
|
console.log(`Base: ${p.base?.label || 'unknown'}`);
|
||||||
|
if (p.commits !== undefined) console.log(`Commits: ${p.commits}`);
|
||||||
|
if (p.changed_files !== undefined) console.log(`Changed: ${p.changed_files} files`);
|
||||||
|
console.log(`Created: ${formatDate(p.created_at)}`);
|
||||||
|
console.log(`Updated: ${formatDate(p.updated_at)}`);
|
||||||
|
if (p.merged_at) console.log(`Merged: ${formatDate(p.merged_at)}`);
|
||||||
|
console.log(`URL: ${p.html_url}`);
|
||||||
|
if (p.body) {
|
||||||
|
console.log(`\n${p.body}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
pr
|
||||||
|
.command('merge <number>')
|
||||||
|
.description('Merge a pull request')
|
||||||
|
.option('--repo <owner/repo>', 'Repository (owner/repo)')
|
||||||
|
.option('--method <method>', 'Merge method: merge|squash|rebase', 'merge')
|
||||||
|
.option('--message <msg>', 'Merge commit message')
|
||||||
|
.option('--json', 'Output raw JSON')
|
||||||
|
.action(async (number: string, opts: { repo?: string; method?: string; message?: string; json?: boolean }) => {
|
||||||
|
const token = getToken();
|
||||||
|
if (!token) {
|
||||||
|
console.error('Error: Authentication required. Run `gitee auth login` or set GITEE_TOKEN.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const repoName = resolveRepo(opts.repo);
|
||||||
|
const [owner, repo] = repoName.split('/');
|
||||||
|
|
||||||
|
const methodMap: Record<string, string> = {
|
||||||
|
merge: 'merge',
|
||||||
|
squash: 'squash',
|
||||||
|
rebase: 'rebase',
|
||||||
|
};
|
||||||
|
|
||||||
|
const mergeMethod = methodMap[opts.method || 'merge'] || 'merge';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await apiRequest(`/repos/${owner}/${repo}/pulls/${number}/merge`, {
|
||||||
|
method: 'PUT',
|
||||||
|
token,
|
||||||
|
body: {
|
||||||
|
merge_method: mergeMethod,
|
||||||
|
commit_message: opts.message || '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (opts.json) {
|
||||||
|
console.log(JSON.stringify({ merged: true, number, method: mergeMethod }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✓ Merged PR #${number} (${mergeMethod})`);
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
pr
|
||||||
|
.command('close <number>')
|
||||||
|
.description('Close a pull request')
|
||||||
|
.option('--repo <owner/repo>', 'Repository (owner/repo)')
|
||||||
|
.option('--json', 'Output raw JSON')
|
||||||
|
.action(async (number: string, opts: { repo?: string; json?: boolean }) => {
|
||||||
|
const token = getToken();
|
||||||
|
if (!token) {
|
||||||
|
console.error('Error: Authentication required. Run `gitee auth login` or set GITEE_TOKEN.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const repoName = resolveRepo(opts.repo);
|
||||||
|
const [owner, repo] = repoName.split('/');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updated = await apiRequest<GiteePR>(`/repos/${owner}/${repo}/pulls/${number}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
token,
|
||||||
|
body: { state: 'closed' },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (opts.json) {
|
||||||
|
console.log(JSON.stringify(updated, null, 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✓ Closed PR #${number}`);
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
121
src/commands/release.ts
Normal file
121
src/commands/release.ts
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import { Command } from 'commander';
|
||||||
|
import { getToken } from '../config.js';
|
||||||
|
import { apiRequest, resolveRepo, formatDate, handleError } from '../api.js';
|
||||||
|
|
||||||
|
interface GiteeRelease {
|
||||||
|
id: number;
|
||||||
|
tag_name: string;
|
||||||
|
name: string;
|
||||||
|
body?: string;
|
||||||
|
draft: boolean;
|
||||||
|
prerelease: boolean;
|
||||||
|
author?: { login: string };
|
||||||
|
created_at: string;
|
||||||
|
assets?: Array<{ id: number; name: string; size: number; download_count: number }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerReleaseCommands(program: Command): void {
|
||||||
|
const release = program
|
||||||
|
.command('release')
|
||||||
|
.description('Manage releases');
|
||||||
|
|
||||||
|
release
|
||||||
|
.command('list')
|
||||||
|
.description('List releases')
|
||||||
|
.option('--repo <owner/repo>', 'Repository (owner/repo)')
|
||||||
|
.option('--page <n>', 'Page number', '1')
|
||||||
|
.option('--per-page <n>', 'Results per page', '20')
|
||||||
|
.option('--json', 'Output raw JSON')
|
||||||
|
.action(async (opts: { repo?: string; page?: string; perPage?: string; json?: boolean }) => {
|
||||||
|
const token = getToken();
|
||||||
|
const repoName = resolveRepo(opts.repo);
|
||||||
|
const [owner, repo] = repoName.split('/');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const releases = await apiRequest<GiteeRelease[]>(`/repos/${owner}/${repo}/releases`, {
|
||||||
|
token,
|
||||||
|
params: {
|
||||||
|
page: opts.page || 1,
|
||||||
|
per_page: opts.perPage || 20,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (opts.json) {
|
||||||
|
console.log(JSON.stringify(releases, null, 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!releases.length) {
|
||||||
|
console.log('No releases found.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Releases in ${repoName}:\n`);
|
||||||
|
for (const r of releases) {
|
||||||
|
const flags: string[] = [];
|
||||||
|
if (r.draft) flags.push('draft');
|
||||||
|
if (r.prerelease) flags.push('pre-release');
|
||||||
|
const flagStr = flags.length ? ` [${flags.join(', ')}]` : '';
|
||||||
|
console.log(` ${r.tag_name} — ${r.name}${flagStr}`);
|
||||||
|
console.log(` by ${r.author?.login || 'unknown'} · ${formatDate(r.created_at)}`);
|
||||||
|
if (r.assets?.length) {
|
||||||
|
console.log(` Assets: ${r.assets.map(a => a.name).join(', ')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
release
|
||||||
|
.command('create')
|
||||||
|
.description('Create a release')
|
||||||
|
.option('--repo <owner/repo>', 'Repository (owner/repo)')
|
||||||
|
.requiredOption('--tag <tag>', 'Tag name (e.g. v1.0.0)')
|
||||||
|
.requiredOption('--name <name>', 'Release name/title')
|
||||||
|
.option('--body <desc>', 'Release description')
|
||||||
|
.option('--draft', 'Create as draft')
|
||||||
|
.option('--prerelease', 'Mark as pre-release')
|
||||||
|
.option('--json', 'Output raw JSON')
|
||||||
|
.action(async (opts: {
|
||||||
|
repo?: string;
|
||||||
|
tag: string;
|
||||||
|
name: string;
|
||||||
|
body?: string;
|
||||||
|
draft?: boolean;
|
||||||
|
prerelease?: boolean;
|
||||||
|
json?: boolean;
|
||||||
|
}) => {
|
||||||
|
const token = getToken();
|
||||||
|
if (!token) {
|
||||||
|
console.error('Error: Authentication required. Run `gitee auth login` or set GITEE_TOKEN.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const repoName = resolveRepo(opts.repo);
|
||||||
|
const [owner, repo] = repoName.split('/');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const created = await apiRequest<GiteeRelease>(`/repos/${owner}/${repo}/releases`, {
|
||||||
|
method: 'POST',
|
||||||
|
token,
|
||||||
|
body: {
|
||||||
|
tag_name: opts.tag,
|
||||||
|
name: opts.name,
|
||||||
|
body: opts.body || '',
|
||||||
|
draft: opts.draft || false,
|
||||||
|
prerelease: opts.prerelease || false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (opts.json) {
|
||||||
|
console.log(JSON.stringify(created, null, 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✓ Created release: ${created.name} (${created.tag_name})`);
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
224
src/commands/repo.ts
Normal file
224
src/commands/repo.ts
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
import { Command } from 'commander';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import * as readline from 'readline';
|
||||||
|
import { getToken } from '../config.js';
|
||||||
|
import { apiRequest, resolveRepo, formatDate, handleError } from '../api.js';
|
||||||
|
|
||||||
|
interface GiteeRepo {
|
||||||
|
id: number;
|
||||||
|
full_name: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
private: boolean;
|
||||||
|
fork: boolean;
|
||||||
|
html_url: string;
|
||||||
|
ssh_url?: string;
|
||||||
|
clone_url?: string;
|
||||||
|
homepage?: string;
|
||||||
|
language?: string;
|
||||||
|
forks_count?: number;
|
||||||
|
stargazers_count?: number;
|
||||||
|
watchers_count?: number;
|
||||||
|
open_issues_count?: number;
|
||||||
|
default_branch?: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
pushed_at?: string;
|
||||||
|
owner?: { login: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
function promptConfirm(question: string): Promise<boolean> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
||||||
|
rl.question(question, (answer) => {
|
||||||
|
rl.close();
|
||||||
|
resolve(answer.trim().toLowerCase() === 'y' || answer.trim().toLowerCase() === 'yes');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerRepoCommands(program: Command): void {
|
||||||
|
const repo = program
|
||||||
|
.command('repo')
|
||||||
|
.description('Manage repositories');
|
||||||
|
|
||||||
|
repo
|
||||||
|
.command('list')
|
||||||
|
.description('List repositories')
|
||||||
|
.option('--owner <user>', 'List repos for a specific user (default: authenticated user)')
|
||||||
|
.option('--type <type>', 'Type: all|owner|public|private|member', 'all')
|
||||||
|
.option('--page <n>', 'Page number', '1')
|
||||||
|
.option('--per-page <n>', 'Results per page (max 100)', '20')
|
||||||
|
.option('--json', 'Output raw JSON')
|
||||||
|
.action(async (opts: { owner?: string; type?: string; page?: string; perPage?: string; json?: boolean }) => {
|
||||||
|
const token = getToken();
|
||||||
|
try {
|
||||||
|
let repos: GiteeRepo[];
|
||||||
|
const params = {
|
||||||
|
type: opts.type || 'all',
|
||||||
|
page: opts.page || 1,
|
||||||
|
per_page: opts.perPage || 20,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (opts.owner) {
|
||||||
|
repos = await apiRequest<GiteeRepo[]>(`/users/${opts.owner}/repos`, { token, params });
|
||||||
|
} else {
|
||||||
|
if (!token) {
|
||||||
|
console.error('Error: Authentication required. Run `gitee auth login` or set GITEE_TOKEN.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
repos = await apiRequest<GiteeRepo[]>('/user/repos', { token, params });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.json) {
|
||||||
|
console.log(JSON.stringify(repos, null, 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!repos.length) {
|
||||||
|
console.log('No repositories found.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found ${repos.length} repositories:\n`);
|
||||||
|
for (const r of repos) {
|
||||||
|
const visibility = r.private ? '🔒 private' : '🌐 public';
|
||||||
|
const stars = r.stargazers_count !== undefined ? `⭐ ${r.stargazers_count}` : '';
|
||||||
|
const forks = r.forks_count !== undefined ? `🍴 ${r.forks_count}` : '';
|
||||||
|
console.log(` ${r.full_name} [${visibility}] ${stars} ${forks}`);
|
||||||
|
if (r.description) console.log(` ${r.description}`);
|
||||||
|
console.log(` ${r.html_url}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
repo
|
||||||
|
.command('create <name>')
|
||||||
|
.description('Create a new repository')
|
||||||
|
.option('--private', 'Make repository private')
|
||||||
|
.option('--description <desc>', 'Repository description')
|
||||||
|
.option('--org <org>', 'Create under an organization')
|
||||||
|
.option('--json', 'Output raw JSON')
|
||||||
|
.action(async (name: string, opts: { private?: boolean; description?: string; org?: string; json?: boolean }) => {
|
||||||
|
const token = getToken();
|
||||||
|
if (!token) {
|
||||||
|
console.error('Error: Authentication required. Run `gitee auth login` or set GITEE_TOKEN.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body: Record<string, unknown> = {
|
||||||
|
name,
|
||||||
|
private: opts.private || false,
|
||||||
|
description: opts.description || '',
|
||||||
|
auto_init: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let endpoint = '/user/repos';
|
||||||
|
if (opts.org) {
|
||||||
|
endpoint = `/orgs/${opts.org}/repos`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = await apiRequest<GiteeRepo>(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
token,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (opts.json) {
|
||||||
|
console.log(JSON.stringify(created, null, 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✓ Created repository: ${created.full_name}`);
|
||||||
|
console.log(` URL: ${created.html_url}`);
|
||||||
|
console.log(` Clone: ${created.clone_url || created.ssh_url}`);
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
repo
|
||||||
|
.command('view [repo]')
|
||||||
|
.description('View repository details (owner/repo or auto-detect)')
|
||||||
|
.option('--repo <owner/repo>', 'Repository (owner/repo)')
|
||||||
|
.option('--json', 'Output raw JSON')
|
||||||
|
.action(async (repoArg: string | undefined, opts: { repo?: string; json?: boolean }) => {
|
||||||
|
const token = getToken();
|
||||||
|
const repoName = resolveRepo(repoArg || opts.repo);
|
||||||
|
const [owner, name] = repoName.split('/');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = await apiRequest<GiteeRepo>(`/repos/${owner}/${name}`, { token });
|
||||||
|
|
||||||
|
if (opts.json) {
|
||||||
|
console.log(JSON.stringify(r, null, 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibility = r.private ? '🔒 private' : '🌐 public';
|
||||||
|
console.log(`${r.full_name} (${visibility})`);
|
||||||
|
console.log(`─────────────────────────────`);
|
||||||
|
if (r.description) console.log(`Description: ${r.description}`);
|
||||||
|
console.log(`URL: ${r.html_url}`);
|
||||||
|
console.log(`Clone: ${r.clone_url || r.ssh_url}`);
|
||||||
|
console.log(`Branch: ${r.default_branch || 'master'}`);
|
||||||
|
console.log(`Stars: ${r.stargazers_count ?? 0} Forks: ${r.forks_count ?? 0} Watchers: ${r.watchers_count ?? 0}`);
|
||||||
|
console.log(`Issues: ${r.open_issues_count ?? 0} open`);
|
||||||
|
if (r.language) console.log(`Language: ${r.language}`);
|
||||||
|
console.log(`Created: ${formatDate(r.created_at)}`);
|
||||||
|
console.log(`Updated: ${formatDate(r.updated_at)}`);
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
repo
|
||||||
|
.command('clone <repo>')
|
||||||
|
.description('Clone a Gitee repository (owner/repo)')
|
||||||
|
.action(async (repoArg: string) => {
|
||||||
|
const cloneUrl = `https://gitee.com/${repoArg}.git`;
|
||||||
|
console.log(`Cloning ${cloneUrl}...`);
|
||||||
|
try {
|
||||||
|
execSync(`git clone ${cloneUrl}`, { stdio: 'inherit' });
|
||||||
|
} catch {
|
||||||
|
console.error('Error: Clone failed.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
repo
|
||||||
|
.command('delete <repo>')
|
||||||
|
.description('Delete a repository (owner/repo) — requires confirmation')
|
||||||
|
.option('--yes', 'Skip confirmation prompt')
|
||||||
|
.action(async (repoArg: string, opts: { yes?: boolean }) => {
|
||||||
|
const token = getToken();
|
||||||
|
if (!token) {
|
||||||
|
console.error('Error: Authentication required. Run `gitee auth login` or set GITEE_TOKEN.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [owner, name] = repoArg.split('/');
|
||||||
|
if (!owner || !name) {
|
||||||
|
console.error('Error: Invalid repo format. Use owner/repo.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!opts.yes) {
|
||||||
|
const confirmed = await promptConfirm(`Are you sure you want to delete ${repoArg}? This is irreversible! (y/N): `);
|
||||||
|
if (!confirmed) {
|
||||||
|
console.log('Aborted.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await apiRequest(`/repos/${owner}/${name}`, { method: 'DELETE', token });
|
||||||
|
console.log(`✓ Deleted repository: ${repoArg}`);
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
40
src/config.ts
Normal file
40
src/config.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { homedir } from 'os';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from 'fs';
|
||||||
|
|
||||||
|
export interface Config {
|
||||||
|
token?: string;
|
||||||
|
username?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CONFIG_DIR = join(homedir(), '.config', 'gitee-cli');
|
||||||
|
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
||||||
|
|
||||||
|
export function getConfig(): Config {
|
||||||
|
try {
|
||||||
|
if (existsSync(CONFIG_FILE)) {
|
||||||
|
return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveConfig(config: Config): void {
|
||||||
|
if (!existsSync(CONFIG_DIR)) {
|
||||||
|
mkdirSync(CONFIG_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearConfig(): void {
|
||||||
|
if (existsSync(CONFIG_FILE)) {
|
||||||
|
unlinkSync(CONFIG_FILE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getToken(): string | undefined {
|
||||||
|
// env var has higher priority
|
||||||
|
return process.env.GITEE_TOKEN || getConfig().token;
|
||||||
|
}
|
||||||
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"declaration": true,
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user