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.
@@ -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
+ });