gitea-cli-skill 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/SKILL.md +298 -0
- package/package.json +22 -0
- package/src/index.js +369 -0
package/assets/SKILL.md
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gitea-cli
|
|
3
|
+
description: Use when needing to interact with Gitea repositories, branches, issues, pull requests, releases, actions CI/CD, webhooks, labels, milestones, tags, or file content via the gitea-cli command-line tool. Triggers on Gitea API operations, repository management, CI/CD pipeline management, or any task requiring gitea-cli commands.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# gitea-cli
|
|
7
|
+
|
|
8
|
+
## Binary Location
|
|
9
|
+
|
|
10
|
+
Binaries for all platforms are bundled under the `scripts/` directory relative to this skill:
|
|
11
|
+
|
|
12
|
+
| Platform | Relative Path |
|
|
13
|
+
|----------|------|
|
|
14
|
+
| Linux amd64 | `scripts/linux-amd64/gitea-cli` |
|
|
15
|
+
| Linux arm64 | `scripts/linux-arm64/gitea-cli` |
|
|
16
|
+
| macOS amd64 | `scripts/darwin-amd64/gitea-cli` |
|
|
17
|
+
| macOS arm64 | `scripts/darwin-arm64/gitea-cli` |
|
|
18
|
+
| Windows amd64 | `scripts/windows-amd64/gitea-cli.exe` |
|
|
19
|
+
| Windows arm64 | `scripts/windows-arm64/gitea-cli.exe` |
|
|
20
|
+
|
|
21
|
+
**Skill directory is `~/.claude/skills/gitea-cli/`.** Before executing commands, resolve `gitea-cli` to the platform-appropriate binary. Auto-detect with:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
SKILL_DIR="$HOME/.claude/skills/gitea-cli"
|
|
25
|
+
|
|
26
|
+
# Linux / macOS
|
|
27
|
+
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
|
28
|
+
ARCH="$(uname -m)"
|
|
29
|
+
[[ "$ARCH" == "x86_64" ]] && ARCH="amd64"
|
|
30
|
+
[[ "$ARCH" == "aarch64" || "$ARCH" == "arm64" ]] && ARCH="arm64"
|
|
31
|
+
GITEA_CLI="$SKILL_DIR/scripts/${OS}-${ARCH}/gitea-cli"
|
|
32
|
+
|
|
33
|
+
# Windows (PowerShell)
|
|
34
|
+
# $GITEA_CLI = "$env:USERPROFILE\.claude\skills\gitea-cli\scripts\windows-amd64\gitea-cli.exe"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
If the tool is also available in PATH (e.g. installed globally), `gitea-cli` can be used directly.
|
|
38
|
+
|
|
39
|
+
## Overview
|
|
40
|
+
|
|
41
|
+
`gitea-cli` is a stateless CLI tool that manages Gitea repositories through the HTTP API. Each call is an independent API request, making it safe for concurrent use. It supports both human-friendly table output and machine-friendly JSON output (`--output json`).
|
|
42
|
+
|
|
43
|
+
## Agent Rules
|
|
44
|
+
|
|
45
|
+
### 1. Always use JSON output
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
gitea-cli <command> --output json --owner <owner> --repo <repo>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 2. Never switch context autonomously
|
|
52
|
+
|
|
53
|
+
Use `--owner`/`--repo` flags to target different repos. Do NOT call `config use` without user supervision.
|
|
54
|
+
|
|
55
|
+
### 3. Destructive operations require confirmation
|
|
56
|
+
|
|
57
|
+
Always run a list/get command first to verify the target exists before deleting.
|
|
58
|
+
|
|
59
|
+
### 4. Branch/ref auto-detection
|
|
60
|
+
|
|
61
|
+
When inside a git repo, `--branch` and `--ref` default to the current branch. `--owner`/`--repo` auto-detect from git remote.
|
|
62
|
+
|
|
63
|
+
## Global Flags
|
|
64
|
+
|
|
65
|
+
| Flag | Short | Description |
|
|
66
|
+
|------|-------|-------------|
|
|
67
|
+
| `--output` | `-o` | `json` or `table` (agents: always use `json`) |
|
|
68
|
+
| `--owner` | | Repository owner |
|
|
69
|
+
| `--repo` | `-r` | Repository name |
|
|
70
|
+
| `--context` | `-c` | Context name (override, no save) |
|
|
71
|
+
| `--config` | | Config file path |
|
|
72
|
+
| `--host` | | Gitea host URL |
|
|
73
|
+
| `--token` | | API token |
|
|
74
|
+
|
|
75
|
+
**Env vars:** `GITEA_HOST`, `GITEA_TOKEN`, `GITEA_OWNER`, `GITEA_REPO`, `GITEA_OUTPUT`
|
|
76
|
+
|
|
77
|
+
## Command Quick Reference
|
|
78
|
+
|
|
79
|
+
### Config
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
gitea-cli config list # List all contexts
|
|
83
|
+
gitea-cli config get <name> # Get context details
|
|
84
|
+
gitea-cli config set --name <ctx> --host <url> --token <tok> # Create/update context
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Repository
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
gitea-cli repo list --owner <org> --output json
|
|
91
|
+
gitea-cli repo get --owner <org> --repo <repo> --output json
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Branch
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
gitea-cli branch list --output json
|
|
98
|
+
gitea-cli branch get <name> --output json
|
|
99
|
+
gitea-cli branch create <name> --from <base>
|
|
100
|
+
gitea-cli branch delete <name> --force
|
|
101
|
+
gitea-cli branch protect list --output json
|
|
102
|
+
gitea-cli branch protect create <name> --enable-push --required-approvals 2
|
|
103
|
+
gitea-cli branch protect delete <name> --force
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### File Content
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
gitea-cli content list [--path] --ref <branch> --output json
|
|
110
|
+
gitea-cli content get <path> --ref <branch> --output json # --raw for decoded content in json
|
|
111
|
+
gitea-cli content create <path> --content "text" -m "msg" # or --file <local>
|
|
112
|
+
gitea-cli content update <path> --content "text" -m "msg" # --sha auto-fetched
|
|
113
|
+
gitea-cli content delete <path> -m "msg" --force
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Commit
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
gitea-cli commit list --branch <ref> --limit 10 --output json
|
|
120
|
+
gitea-cli commit get <sha> --output json
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Issue
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
gitea-cli issue list --state open --output json
|
|
127
|
+
gitea-cli issue get <number> --output json
|
|
128
|
+
gitea-cli issue create -t "Title" -b "Body" --label <id> --milestone <id>
|
|
129
|
+
gitea-cli issue update <number> -t "New Title" --state closed
|
|
130
|
+
gitea-cli issue close <number>
|
|
131
|
+
|
|
132
|
+
# Comments
|
|
133
|
+
gitea-cli issue comment list <number> --output json
|
|
134
|
+
gitea-cli issue comment create <number> -b "Comment text"
|
|
135
|
+
gitea-cli issue comment update <comment-id> -b "Updated text"
|
|
136
|
+
gitea-cli issue comment delete <comment-id> --force
|
|
137
|
+
|
|
138
|
+
# Attachments
|
|
139
|
+
gitea-cli issue attachment list <number> --output json
|
|
140
|
+
gitea-cli issue attachment upload <number> <file> --name <filename>
|
|
141
|
+
gitea-cli issue attachment download <number> <attach-id> -o ./save/
|
|
142
|
+
gitea-cli issue attachment delete <number> <attach-id> --force
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Pull Request
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
gitea-cli pr list --state open --output json
|
|
149
|
+
gitea-cli pr get <index> --output json
|
|
150
|
+
gitea-cli pr create --title "Title" --head <src> --base <target> --body "Desc"
|
|
151
|
+
gitea-cli pr close <index>
|
|
152
|
+
gitea-cli pr reopen <index>
|
|
153
|
+
gitea-cli pr merge <index> --strategy merge --delete-branch
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Label
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
gitea-cli label list --output json
|
|
160
|
+
gitea-cli label get <id> --output json
|
|
161
|
+
gitea-cli label create --name "bug" --color "#ff0000" --description "Bug report"
|
|
162
|
+
gitea-cli label delete <id> --force
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Milestone
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
gitea-cli milestone list --state open --output json
|
|
169
|
+
gitea-cli milestone get <id> --output json
|
|
170
|
+
gitea-cli milestone create --title "v1.0" --description "First release" --due-on 2025-12-31T00:00:00Z
|
|
171
|
+
gitea-cli milestone close <id>
|
|
172
|
+
gitea-cli milestone delete <id> --force
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Tag
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
gitea-cli tag list --output json
|
|
179
|
+
gitea-cli tag get <name> --output json
|
|
180
|
+
gitea-cli tag create <name> --target <ref> -m "Annotated tag message"
|
|
181
|
+
gitea-cli tag delete <name> --force
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Actions (CI/CD)
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
# Workflows
|
|
188
|
+
gitea-cli actions workflow list --output json
|
|
189
|
+
gitea-cli actions workflow get <id-or-filename> --output json
|
|
190
|
+
gitea-cli actions workflow enable <id-or-filename>
|
|
191
|
+
gitea-cli actions workflow disable <id-or-filename>
|
|
192
|
+
gitea-cli actions workflow dispatch <id-or-filename> --ref main --input key=value
|
|
193
|
+
|
|
194
|
+
# Runs
|
|
195
|
+
gitea-cli actions run list --status completed --output json
|
|
196
|
+
gitea-cli actions run get <run-id> --output json
|
|
197
|
+
gitea-cli actions run jobs <run-id> --output json
|
|
198
|
+
gitea-cli actions run artifacts <run-id> --output json
|
|
199
|
+
gitea-cli actions run delete <run-id> --force
|
|
200
|
+
|
|
201
|
+
# Jobs
|
|
202
|
+
gitea-cli actions job get <job-id> --output json
|
|
203
|
+
gitea-cli actions job logs <job-id> -o ./logs/
|
|
204
|
+
|
|
205
|
+
# Artifacts
|
|
206
|
+
gitea-cli actions artifact list --output json
|
|
207
|
+
gitea-cli actions artifact download <id> -o ./dist/
|
|
208
|
+
gitea-cli actions artifact delete <id> --force
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Webhook
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
gitea-cli webhook list --output json
|
|
215
|
+
gitea-cli webhook get <id> --output json
|
|
216
|
+
gitea-cli webhook create --url <endpoint> --secret <secret> --event push --event issues --active
|
|
217
|
+
gitea-cli webhook update <id> --url <new-url> --active false
|
|
218
|
+
gitea-cli webhook delete <id> --force
|
|
219
|
+
gitea-cli webhook test <id>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Release
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
gitea-cli release list --output json
|
|
226
|
+
gitea-cli release get <tag> --output json
|
|
227
|
+
gitea-cli release create --tag <tag> --name "Title" --body "Notes" --target <ref> --draft --prerelease
|
|
228
|
+
gitea-cli release delete <tag> --force
|
|
229
|
+
|
|
230
|
+
# Assets
|
|
231
|
+
gitea-cli release asset list <tag> --output json
|
|
232
|
+
gitea-cli release asset upload <tag> <file> --name <asset-name>
|
|
233
|
+
gitea-cli release asset download <tag> <asset-id> -o ./downloads/
|
|
234
|
+
gitea-cli release asset delete <tag> <asset-id> --force
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Common Patterns
|
|
238
|
+
|
|
239
|
+
### Read file from repo, modify, write back
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
# 1. Get current content (captures SHA for update)
|
|
243
|
+
gitea-cli content get src/config.yaml --output json --owner myorg --repo myrepo
|
|
244
|
+
|
|
245
|
+
# 2. Update with new content (SHA auto-fetched if omitted)
|
|
246
|
+
gitea-cli content update src/config.yaml --content "new: value" -m "update config" --owner myorg --repo myrepo
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Create issue with labels and milestone
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
# 1. Find label IDs
|
|
253
|
+
gitea-cli label list --output json --owner myorg --repo myrepo
|
|
254
|
+
|
|
255
|
+
# 2. Find milestone ID
|
|
256
|
+
gitea-cli milestone list --output json --owner myorg --repo myrepo
|
|
257
|
+
|
|
258
|
+
# 3. Create issue
|
|
259
|
+
gitea-cli issue create -t "Fix login bug" -b "Description" --label 3 --label 7 --milestone 2 --owner myorg --repo myrepo
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Monitor CI/CD pipeline
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
# 1. Find latest run
|
|
266
|
+
gitea-cli actions run list --limit 5 --output json
|
|
267
|
+
|
|
268
|
+
# 2. Check job status
|
|
269
|
+
gitea-cli actions run jobs <run-id> --output json
|
|
270
|
+
|
|
271
|
+
# 3. Get logs for failed job
|
|
272
|
+
gitea-cli actions job logs <job-id> -o ./logs/
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## Error Handling
|
|
276
|
+
|
|
277
|
+
| Exit Code | Meaning |
|
|
278
|
+
|-----------|---------|
|
|
279
|
+
| `0` | Success |
|
|
280
|
+
| `1` | Error (see stderr) |
|
|
281
|
+
|
|
282
|
+
JSON errors on stderr: `{"error": "API 404: repository not found"}`
|
|
283
|
+
|
|
284
|
+
| Error | Cause | Fix |
|
|
285
|
+
|-------|-------|-----|
|
|
286
|
+
| `host is required` | No context configured | Run `config set` |
|
|
287
|
+
| `API 404` | Resource not found | Verify owner/repo/index |
|
|
288
|
+
| `API 409` | Resource already exists | Skip duplicate creation |
|
|
289
|
+
| `API 401` | Invalid/expired token | Check context token |
|
|
290
|
+
|
|
291
|
+
## Destructive Operations Checklist
|
|
292
|
+
|
|
293
|
+
Before running any delete, always verify:
|
|
294
|
+
|
|
295
|
+
1. Run the corresponding `list` or `get` command first
|
|
296
|
+
2. Confirm the resource ID/name matches the intended target
|
|
297
|
+
3. Use `--force` flag to skip interactive prompts (required for agent use)
|
|
298
|
+
4. Consider irreversibility — deleted resources cannot be recovered
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gitea-cli-skill",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Install gitea-cli as a Claude Code skill",
|
|
5
|
+
"bin": {
|
|
6
|
+
"gitea-cli-skill": "src/index.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"src",
|
|
10
|
+
"assets"
|
|
11
|
+
],
|
|
12
|
+
"keywords": [
|
|
13
|
+
"claude-code",
|
|
14
|
+
"skill",
|
|
15
|
+
"gitea",
|
|
16
|
+
"cli"
|
|
17
|
+
],
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18"
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const { execSync } = require('child_process');
|
|
8
|
+
|
|
9
|
+
// ── Config ──────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
const GITEA_HOST = 'https://x.xgit.pro';
|
|
12
|
+
const GITEA_OWNER = 'chenqi';
|
|
13
|
+
const GITEA_REPO = 'git-cli';
|
|
14
|
+
const SKILL_NAME = 'gitea-cli';
|
|
15
|
+
|
|
16
|
+
// ── Platform definitions ────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
const PLATFORMS = {
|
|
19
|
+
claude: { dir: '.claude/skills', file: 'SKILL.md', desc: 'Claude Code' },
|
|
20
|
+
cursor: { dir: '.cursor/skills', file: 'SKILL.md', desc: 'Cursor' },
|
|
21
|
+
windsurf: { dir: '.windsurf/skills', file: 'SKILL.md', desc: 'Windsurf' },
|
|
22
|
+
copilot: { dir: '.copilot/skills', file: 'SKILL.md', desc: 'GitHub Copilot CLI' },
|
|
23
|
+
codex: { dir: '.codex/skills', file: 'SKILL.md', desc: 'OpenAI Codex CLI' },
|
|
24
|
+
gemini: { dir: '.gemini/skills', file: 'SKILL.md', desc: 'Gemini CLI' },
|
|
25
|
+
trae: { dir: '.trae/skills', file: 'SKILL.md', desc: 'Trae' },
|
|
26
|
+
roo: { dir: '.roo/skills', file: 'SKILL.md', desc: 'Roo Code' },
|
|
27
|
+
kiro: { dir: '.kiro/skills', file: 'SKILL.md', desc: 'Kiro' },
|
|
28
|
+
continue: { dir: '.continue/skills', file: 'SKILL.md', desc: 'Continue' },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// ── OS/Arch detection ───────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
function detectOSArch() {
|
|
34
|
+
const platform = os.platform();
|
|
35
|
+
const arch = os.arch();
|
|
36
|
+
|
|
37
|
+
let osName;
|
|
38
|
+
switch (platform) {
|
|
39
|
+
case 'darwin': osName = 'darwin'; break;
|
|
40
|
+
case 'linux': osName = 'linux'; break;
|
|
41
|
+
case 'win32': osName = 'windows'; break;
|
|
42
|
+
default: throw new Error(`Unsupported OS: ${platform}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let archName;
|
|
46
|
+
switch (arch) {
|
|
47
|
+
case 'x64': archName = 'amd64'; break;
|
|
48
|
+
case 'arm64': archName = 'arm64'; break;
|
|
49
|
+
default: throw new Error(`Unsupported arch: ${arch}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { os: osName, arch: archName };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Resolve skill install directory ─────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
function resolveSkillDir(platformName, targetPath) {
|
|
58
|
+
if (targetPath) {
|
|
59
|
+
return targetPath.replace(/^~/, os.homedir());
|
|
60
|
+
}
|
|
61
|
+
if (platformName && PLATFORMS[platformName]) {
|
|
62
|
+
return path.join(os.homedir(), PLATFORMS[platformName].dir, SKILL_NAME);
|
|
63
|
+
}
|
|
64
|
+
// Default: claude
|
|
65
|
+
return path.join(os.homedir(), PLATFORMS.claude.dir, SKILL_NAME);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── HTTP via curl (robust cross-platform) ───────────────────────────────────
|
|
69
|
+
|
|
70
|
+
function curlGet(url, token) {
|
|
71
|
+
const args = ['-sS', '-f', '-k', '-H', 'User-Agent: gitea-cli-skill-installer'];
|
|
72
|
+
if (token) {
|
|
73
|
+
args.push('-H', `Authorization: token ${token}`);
|
|
74
|
+
}
|
|
75
|
+
args.push(url);
|
|
76
|
+
return execSync(`curl ${args.map((a) => `"${a}"`).join(' ')}`, { encoding: 'utf-8', timeout: 30000 });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function curlDownload(url, destPath, token) {
|
|
80
|
+
const args = ['-sS', '-f', '-k', '-L', '-o', destPath, '-H', 'User-Agent: gitea-cli-skill-installer'];
|
|
81
|
+
if (token) {
|
|
82
|
+
args.push('-H', `Authorization: token ${token}`);
|
|
83
|
+
}
|
|
84
|
+
args.push(url);
|
|
85
|
+
execSync(`curl ${args.map((a) => `"${a}"`).join(' ')}`, { encoding: 'utf-8', timeout: 120000 });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── Gitea API ────────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
function getLatestRelease(host, owner, repo, token) {
|
|
91
|
+
// Try /latest first
|
|
92
|
+
try {
|
|
93
|
+
const data = curlGet(`${host}/api/v1/repos/${owner}/${repo}/releases/latest`, token);
|
|
94
|
+
const release = JSON.parse(data);
|
|
95
|
+
if (release.assets && release.assets.length > 0) return release;
|
|
96
|
+
} catch { /* fall through */ }
|
|
97
|
+
|
|
98
|
+
// List all releases, find latest with assets
|
|
99
|
+
const data = curlGet(`${host}/api/v1/repos/${owner}/${repo}/releases?limit=10`, token);
|
|
100
|
+
const releases = JSON.parse(data);
|
|
101
|
+
for (const r of releases) {
|
|
102
|
+
if (r.assets && r.assets.length > 0) return r;
|
|
103
|
+
}
|
|
104
|
+
throw new Error('No releases with assets found');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function findAsset(release, osArch) {
|
|
108
|
+
const ext = osArch.os === 'windows' ? 'zip' : 'tar.gz';
|
|
109
|
+
const pattern = `_${osArch.os}_${osArch.arch}.${ext}`;
|
|
110
|
+
return release.assets.find((a) => a.name.endsWith(pattern) && !a.name.includes('checksum'));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Archive extraction ──────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
function extractArchive(archivePath, destDir, osArch) {
|
|
116
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
117
|
+
if (osArch.os === 'windows') {
|
|
118
|
+
execSync(`powershell -Command "Expand-Archive -Path '${archivePath}' -DestinationPath '${destDir}' -Force"`, { stdio: 'pipe' });
|
|
119
|
+
} else {
|
|
120
|
+
execSync(`tar -xzf "${archivePath}" -C "${destDir}"`, { stdio: 'pipe' });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const binaryName = osArch.os === 'windows' ? 'gitea-cli.exe' : 'gitea-cli';
|
|
124
|
+
const entries = fs.readdirSync(destDir, { recursive: true });
|
|
125
|
+
for (const entry of entries) {
|
|
126
|
+
if (path.basename(entry) === binaryName) {
|
|
127
|
+
const fullPath = path.join(destDir, entry);
|
|
128
|
+
if (osArch.os !== 'windows') fs.chmodSync(fullPath, 0o755);
|
|
129
|
+
return fullPath;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
throw new Error(`Binary ${binaryName} not found in extracted archive`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── Config generation ───────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
function writeGiteaCliConfig(host, token) {
|
|
138
|
+
const configDir = path.join(os.homedir(), '.config', 'gitea-cli');
|
|
139
|
+
const configPath = path.join(configDir, 'config.yaml');
|
|
140
|
+
const contextName = 'default';
|
|
141
|
+
|
|
142
|
+
// Read existing config if present
|
|
143
|
+
let existing = '';
|
|
144
|
+
if (fs.existsSync(configPath)) {
|
|
145
|
+
existing = fs.readFileSync(configPath, 'utf-8');
|
|
146
|
+
if (existing.includes(contextName)) {
|
|
147
|
+
console.log(` Config already exists: ${configPath} (skipped)`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const newConfig = `${existing}contexts:
|
|
153
|
+
- name: ${contextName}
|
|
154
|
+
host: ${host.replace(/\/$/, '')}
|
|
155
|
+
token: ${token}
|
|
156
|
+
current-context: ${contextName}
|
|
157
|
+
`;
|
|
158
|
+
|
|
159
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
160
|
+
fs.writeFileSync(configPath, newConfig, 'utf-8');
|
|
161
|
+
console.log(` Config -> ${configPath}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── Install command ─────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
async function install(args) {
|
|
167
|
+
const { token: explicitToken, host: explicitHost, platform: platformName, target: targetPath, local: localDir, force } = args;
|
|
168
|
+
const giteaHost = explicitHost || GITEA_HOST;
|
|
169
|
+
const token = explicitToken || process.env.GITEA_TOKEN || null;
|
|
170
|
+
const osArch = detectOSArch();
|
|
171
|
+
const skillDir = resolveSkillDir(platformName, targetPath);
|
|
172
|
+
const scriptsDir = path.join(skillDir, 'scripts', `${osArch.os}-${osArch.arch}`);
|
|
173
|
+
|
|
174
|
+
console.log(`Installing gitea-cli skill (${osArch.os}-${osArch.arch}) to ${skillDir}...`);
|
|
175
|
+
|
|
176
|
+
// 1. Copy SKILL.md
|
|
177
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
178
|
+
const srcSkill = path.resolve(__dirname, '..', 'assets', 'SKILL.md');
|
|
179
|
+
const destSkill = path.join(skillDir, 'SKILL.md');
|
|
180
|
+
fs.copyFileSync(srcSkill, destSkill);
|
|
181
|
+
console.log(` SKILL.md -> ${destSkill}`);
|
|
182
|
+
|
|
183
|
+
// 2. Check existing binary
|
|
184
|
+
const binaryName = osArch.os === 'windows' ? 'gitea-cli.exe' : 'gitea-cli';
|
|
185
|
+
const destBinary = path.join(scriptsDir, binaryName);
|
|
186
|
+
|
|
187
|
+
if (fs.existsSync(destBinary) && !force) {
|
|
188
|
+
console.log(` Binary already exists: ${destBinary} (use --force to overwrite)`);
|
|
189
|
+
} else if (localDir) {
|
|
190
|
+
// --local: copy from local directory
|
|
191
|
+
await installFromLocal(localDir, scriptsDir, destBinary, osArch);
|
|
192
|
+
} else {
|
|
193
|
+
// Download from Gitea releases
|
|
194
|
+
await installFromRelease(giteaHost, token, scriptsDir, destBinary, osArch);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 4. Write gitea-cli config if token provided
|
|
198
|
+
if (token) {
|
|
199
|
+
writeGiteaCliConfig(giteaHost, token);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
console.log(`\nDone!`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── Install from local goreleaser dist ───────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
function installFromLocal(localDir, scriptsDir, destBinary, osArch) {
|
|
208
|
+
// Goreleaser dist structure: dist/gitea-cli_<version>_<os>_<arch>/gitea-cli[.exe]
|
|
209
|
+
const ext = osArch.os === 'windows' ? 'zip' : 'tar.gz';
|
|
210
|
+
const archiveName = `gitea-cli_*_${osArch.os}_${osArch.arch}.${ext}`;
|
|
211
|
+
|
|
212
|
+
const resolvedDir = localDir.replace(/^~/, os.homedir());
|
|
213
|
+
|
|
214
|
+
// Try to find archive in dist/
|
|
215
|
+
const files = fs.readdirSync(resolvedDir);
|
|
216
|
+
const extRe = ext === 'tar.gz' ? 'tar\\.gz' : 'zip';
|
|
217
|
+
const pattern = new RegExp('gitea-cli_\\d+\\.\\d+\\.\\d+(-\\w+)?_' + osArch.os + '_' + osArch.arch + '\\.' + extRe + '$');
|
|
218
|
+
const match = files.find((f) => pattern.test(f));
|
|
219
|
+
if (!match) {
|
|
220
|
+
// Try finding extracted binary directly
|
|
221
|
+
const binaryName = osArch.os === 'windows' ? 'gitea-cli.exe' : 'gitea-cli';
|
|
222
|
+
const subDirs = files.filter((f) => f.includes(`${osArch.os}_${osArch.arch}`) && fs.statSync(path.join(resolvedDir, f)).isDirectory());
|
|
223
|
+
for (const d of subDirs) {
|
|
224
|
+
const candidate = path.join(resolvedDir, d, binaryName);
|
|
225
|
+
if (fs.existsSync(candidate)) {
|
|
226
|
+
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
227
|
+
fs.copyFileSync(candidate, destBinary);
|
|
228
|
+
if (osArch.os !== 'windows') fs.chmodSync(destBinary, 0o755);
|
|
229
|
+
console.log(` Binary -> ${destBinary} (from ${candidate})`);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
throw new Error(`No binary found for ${osArch.os}-${osArch.arch} in ${resolvedDir}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const archivePath = path.join(resolvedDir, match);
|
|
237
|
+
console.log(` Extracting ${match}...`);
|
|
238
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitea-cli-'));
|
|
239
|
+
try {
|
|
240
|
+
const binaryPath = extractArchive(archivePath, path.join(tmpDir, 'extracted'), osArch);
|
|
241
|
+
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
242
|
+
fs.copyFileSync(binaryPath, destBinary);
|
|
243
|
+
if (osArch.os !== 'windows') fs.chmodSync(destBinary, 0o755);
|
|
244
|
+
console.log(` Binary -> ${destBinary}`);
|
|
245
|
+
} finally {
|
|
246
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ── Install from Gitea release ───────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
async function installFromRelease(giteaHost, token, scriptsDir, destBinary, osArch) {
|
|
253
|
+
console.log(` Fetching latest release from ${giteaHost}...`);
|
|
254
|
+
if (!token) {
|
|
255
|
+
console.error(' Error: Gitea API token is required.');
|
|
256
|
+
console.error(' Provide via --token <token> or GITEA_TOKEN env var.');
|
|
257
|
+
console.error(' Or use --local <dir> to install from a local build.');
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let release;
|
|
262
|
+
try {
|
|
263
|
+
release = getLatestRelease(giteaHost, GITEA_OWNER, GITEA_REPO, token);
|
|
264
|
+
} catch (err) {
|
|
265
|
+
console.error(` Failed to fetch release: ${err.message}`);
|
|
266
|
+
console.error(' Use --local <dir> to install from a local build instead.');
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const asset = findAsset(release, osArch);
|
|
271
|
+
if (!asset) {
|
|
272
|
+
console.error(` No binary for ${osArch.os}-${osArch.arch} in release ${release.tag_name}.`);
|
|
273
|
+
console.error(` Assets: ${release.assets.map((a) => a.name).join(', ')}`);
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
console.log(` Downloading ${asset.name} (${(asset.size / 1024 / 1024).toFixed(1)} MB)...`);
|
|
278
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitea-cli-'));
|
|
279
|
+
const tmpArchive = path.join(tmpDir, asset.name);
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
curlDownload(asset.browser_download_url, tmpArchive, token);
|
|
283
|
+
console.log(' Extracting...');
|
|
284
|
+
const binaryPath = extractArchive(tmpArchive, path.join(tmpDir, 'extracted'), osArch);
|
|
285
|
+
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
286
|
+
fs.copyFileSync(binaryPath, destBinary);
|
|
287
|
+
if (osArch.os !== 'windows') fs.chmodSync(destBinary, 0o755);
|
|
288
|
+
console.log(` Binary -> ${destBinary}`);
|
|
289
|
+
} finally {
|
|
290
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ── CLI ──────────────────────────────────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
function parseArgs(argv) {
|
|
297
|
+
const args = { token: null, host: null, platform: null, target: null, local: null, force: false, command: null };
|
|
298
|
+
|
|
299
|
+
for (let i = 2; i < argv.length; i++) {
|
|
300
|
+
const arg = argv[i];
|
|
301
|
+
if (arg === '--token' && i + 1 < argv.length) args.token = argv[++i];
|
|
302
|
+
else if (arg === '--host' && i + 1 < argv.length) args.host = argv[++i];
|
|
303
|
+
else if (arg === '--platform' && i + 1 < argv.length) args.platform = argv[++i];
|
|
304
|
+
else if (arg === '--target' && i + 1 < argv.length) args.target = argv[++i];
|
|
305
|
+
else if (arg === '--local' && i + 1 < argv.length) args.local = argv[++i];
|
|
306
|
+
else if (arg === '--force' || arg === '-f') args.force = true;
|
|
307
|
+
else if (arg === '--help' || arg === '-h') { printHelp(); process.exit(0); }
|
|
308
|
+
else if (!arg.startsWith('-')) args.command = arg;
|
|
309
|
+
}
|
|
310
|
+
return args;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function printHelp() {
|
|
314
|
+
const platformList = Object.entries(PLATFORMS)
|
|
315
|
+
.map(([k, v]) => ` ${k.padEnd(10)} ${v.desc}`)
|
|
316
|
+
.join('\n');
|
|
317
|
+
|
|
318
|
+
console.log(`gitea-cli-skill - Install gitea-cli as an AI agent skill
|
|
319
|
+
|
|
320
|
+
Usage:
|
|
321
|
+
npx gitea-cli-skill init [flags]
|
|
322
|
+
|
|
323
|
+
Commands:
|
|
324
|
+
init Install skill
|
|
325
|
+
|
|
326
|
+
Flags:
|
|
327
|
+
--token <t> Gitea API token (or set GITEA_TOKEN env var)
|
|
328
|
+
--host <url> Gitea host URL (default: ${GITEA_HOST})
|
|
329
|
+
--local <dir> Install from local goreleaser dist directory (skip download)
|
|
330
|
+
--platform <name> Target AI platform (default: claude)
|
|
331
|
+
--target <path> Custom install directory (overrides --platform)
|
|
332
|
+
--force Overwrite existing binary
|
|
333
|
+
-h, --help Show this help
|
|
334
|
+
|
|
335
|
+
Platforms:
|
|
336
|
+
${platformList}
|
|
337
|
+
|
|
338
|
+
Examples:
|
|
339
|
+
npx gitea-cli-skill init --local ../dist # from local build
|
|
340
|
+
npx gitea-cli-skill init --token abc123 # from Gitea release
|
|
341
|
+
npx gitea-cli-skill init --token abc123 --platform cursor # for Cursor
|
|
342
|
+
npx gitea-cli-skill init --local ../dist --target ~/custom # custom dir
|
|
343
|
+
`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function main() {
|
|
347
|
+
const args = parseArgs(process.argv);
|
|
348
|
+
|
|
349
|
+
if (!args.command) { printHelp(); process.exit(0); }
|
|
350
|
+
if (args.command !== 'init') {
|
|
351
|
+
console.error(`Unknown command: ${args.command}`);
|
|
352
|
+
console.error('Run with --help for usage.');
|
|
353
|
+
process.exit(1);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Validate platform name
|
|
357
|
+
if (args.platform && !PLATFORMS[args.platform] && !args.target) {
|
|
358
|
+
console.error(`Unknown platform: ${args.platform}`);
|
|
359
|
+
console.error(`Available: ${Object.keys(PLATFORMS).join(', ')}, or use --target for custom path.`);
|
|
360
|
+
process.exit(1);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
await install(args);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
main().catch((err) => {
|
|
367
|
+
console.error('Error:', err.message);
|
|
368
|
+
process.exit(1);
|
|
369
|
+
});
|