feat: consolidated Gitea API client and pi extension

- Pure fetch-based API client (src/) with zero external dependencies
- Pi extension adapter (pi-extension/) registering 17 tools
- Standalone CLI (cli.ts) replacing gitea-scripts/gitea.js
- Token auth everywhere (no HMAC secrets)
- SKILL.md for agent auto-discovery
- TOOL.md with full parameter reference

Consolidates pi-bot/extensions/pi-gitea and clawbot/gitea-scripts
into a single shared package.
This commit is contained in:
2026-03-13 14:49:55 -07:00
commit 25e49db155
19 changed files with 1988 additions and 0 deletions

187
cli.ts Normal file
View File

@@ -0,0 +1,187 @@
#!/usr/bin/env node
/**
* Gitea CLI — standalone command-line interface
*
* Usage: gitea <command> [options]
* Replaces gitea-scripts/gitea.js with the consolidated core.
*/
import { GiteaClient } from "./src/client.js";
import * as repos from "./src/repos.js";
import * as issues from "./src/issues.js";
import * as pulls from "./src/pulls.js";
import * as actions from "./src/actions.js";
import * as webhooks from "./src/webhooks.js";
import * as files from "./src/files.js";
const client = new GiteaClient();
function parseArgs(argv: string[]): { positional: string[]; flags: Record<string, string | boolean> } {
const positional: string[] = [];
const flags: Record<string, string | boolean> = {};
for (let i = 0; i < argv.length; i++) {
if (argv[i].startsWith("--")) {
const key = argv[i].slice(2);
const next = argv[i + 1];
if (next && !next.startsWith("--")) {
flags[key] = next;
i++;
} else {
flags[key] = true;
}
} else {
positional.push(argv[i]);
}
}
return { positional, flags };
}
function splitOwnerRepo(input: string): { owner: string; repo: string } {
const parts = input.split("/");
if (parts.length === 2) return { owner: parts[0], repo: parts[1] };
return { owner: client.defaultOwner, repo: input };
}
const commands: Record<string, (args: string[]) => Promise<void>> = {
async whoami() {
const user = await client.get<{ login: string; email: string }>("/user");
console.log(JSON.stringify(user, null, 2));
},
async repos(args) {
const { flags } = parseArgs(args);
const result = await repos.listRepos(client, {
owner: flags.owner as string,
limit: flags.limit ? parseInt(flags.limit as string) : undefined,
});
for (const r of result) {
console.log(`${r.full_name} [${r.private ? "private" : "public"}]`);
}
},
async "create-repo"(args) {
const { positional, flags } = parseArgs(args);
const name = positional[0];
if (!name) { console.error("Usage: create-repo <name> [--private] [--description ...]"); process.exit(1); }
const repo = await repos.createRepo(client, {
name,
private: !!flags.private,
description: flags.description as string,
});
console.log(`✅ Created: ${repo.html_url}`);
},
async "ensure-repo"(args) {
const { positional, flags } = parseArgs(args);
const name = positional[0];
if (!name) { console.error("Usage: ensure-repo <name> [--private]"); process.exit(1); }
const { repo, created } = await repos.ensureRepo(client, client.defaultOwner, name, {
private: !!flags.private,
});
console.log(`${created ? "created" : "exists"}: ${repo.clone_url}`);
},
async issues(args) {
const { positional } = parseArgs(args);
const target = positional[0];
if (!target) { console.error("Usage: issues <owner/repo>"); process.exit(1); }
const { owner, repo } = splitOwnerRepo(target);
const list = await issues.listIssues(client, owner, repo);
for (const i of list) {
console.log(`#${i.number} [${i.state}] ${i.title}`);
}
},
async "create-issue"(args) {
const { positional, flags } = parseArgs(args);
const target = positional[0];
const title = positional[1];
if (!target || !title) { console.error("Usage: create-issue <owner/repo> <title> [--body ...]"); process.exit(1); }
const { owner, repo } = splitOwnerRepo(target);
const issue = await issues.createIssue(client, owner, repo, {
title,
body: flags.body as string,
});
console.log(`✅ Issue #${issue.number}: ${issue.html_url}`);
},
async prs(args) {
const { positional } = parseArgs(args);
const target = positional[0];
if (!target) { console.error("Usage: prs <owner/repo>"); process.exit(1); }
const { owner, repo } = splitOwnerRepo(target);
const list = await pulls.listPullRequests(client, owner, repo);
for (const p of list) {
console.log(`#${p.number} [${p.state}] ${p.title} (${p.head?.label}${p.base?.label})`);
}
},
async runs(args) {
const { positional } = parseArgs(args);
const target = positional[0];
if (!target) { console.error("Usage: runs <owner/repo>"); process.exit(1); }
const { owner, repo } = splitOwnerRepo(target);
const { runs } = await actions.listRuns(client, owner, repo);
for (const r of runs) {
console.log(`#${r.run_number} [${r.id}] ${r.conclusion ?? r.status}${r.display_title} (${r.head_branch})`);
}
},
async "add-webhook"(args) {
const { positional, flags } = parseArgs(args);
const target = positional[0];
const webhookUrl = positional[1];
if (!target || !webhookUrl) { console.error("Usage: add-webhook <owner/repo> <url> [--token ...]"); process.exit(1); }
const { owner, repo } = splitOwnerRepo(target);
const hook = await webhooks.createWebhook(client, owner, repo, {
url: webhookUrl,
token: flags.token as string,
});
console.log(`✅ Webhook created: ${hook.id}`);
},
async help() {
console.log(`
Gitea CLI — ${client.url}
Commands:
whoami Show current user
repos [--owner ...] [--limit N] List repos
create-repo <name> [--private] [--description "..."]
ensure-repo <name> [--private] Create if not exists
issues <owner/repo> List issues
create-issue <owner/repo> <title> [--body "..."]
prs <owner/repo> List pull requests
runs <owner/repo> List workflow runs
add-webhook <owner/repo> <url> [--token "..."]
Env: GITEA_URL, GITEA_TOKEN, GITEA_OWNER, GITEA_REPO
`);
},
};
async function main() {
if (!client.token) {
console.error("❌ No GITEA_TOKEN found. Set GITEA_TOKEN in environment.");
process.exit(1);
}
const cmd = process.argv[2];
if (!cmd || cmd === "--help" || cmd === "help") {
await commands.help([]);
return;
}
const fn = commands[cmd];
if (!fn) {
console.error(`Unknown command: ${cmd}`);
process.exit(1);
}
await fn(process.argv.slice(3));
}
main().catch((err) => {
console.error("Error:", err.message);
process.exit(1);
});