feat(mcp): add gitea_* tools so the AI can write code, not just deploy it
Closes the AI's self-reported gap: "I cannot directly commit or push code". New MCP capabilities (8) — all scoped to the workspace's Gitea org via requireGiteaOrg + ensureRepoOwnerInOrg: - gitea.repos.list — discover existing repos - gitea.repo.get — metadata (default branch, clone URL) - gitea.repo.create — mint a new private repo with auto-init - gitea.file.read — read a file (or list a directory) - gitea.file.write — create/update one file in one commit - gitea.file.delete — delete a file (auto-resolves sha) - gitea.branches.list — list branches with head sha - gitea.branch.create — branch off an existing branch Wired through: - lib/gitea.ts: giteaReadFile, giteaListContents, giteaListBranches, giteaCreateBranch, giteaListOrgRepos, giteaDeleteFile. - lib/ai/vibn-tools.ts: 8 new Gemini tool declarations (53 total). - app/api/chat/route.ts: system prompt now teaches the end-to-end scaffold-then-deploy recipe so the AI stops deferring to the user. MCP capability descriptor bumped to version 2.5.0. Made-with: Cursor
This commit is contained in:
@@ -95,6 +95,19 @@ You are talking to the owner of the "${workspace}" workspace.
|
||||
2. \`domains_register { domain }\` to buy it (uses workspace billing).
|
||||
3. \`apps_domains_set { uuid, domains }\` to attach. DNS + Traefik are wired automatically.
|
||||
|
||||
## Writing & shipping code (Gitea)
|
||||
You CAN write code directly — don't tell the user "I can only generate code, you push it." Use these tools to scaffold and edit a project's repo end-to-end:
|
||||
- \`gitea_repo_create { name }\` — mint a new private repo in the workspace org.
|
||||
- \`gitea_file_write { repo, path, content, message }\` — commit one file at a time. Call repeatedly to scaffold a project.
|
||||
- \`gitea_file_read { repo, path }\` — inspect existing code (returns directory listings if path is a folder).
|
||||
- \`gitea_branches_list\` / \`gitea_branch_create\` — branch for risky edits.
|
||||
- \`gitea_repos_list\` — discover what already exists before creating anything new.
|
||||
|
||||
End-to-end recipe for "build me X":
|
||||
1. \`gitea_repo_create { name: 'x' }\`.
|
||||
2. \`gitea_file_write\` × N — package.json, Dockerfile, src/index.ts, etc.
|
||||
3. \`apps_create { projectId, repo: 'x', ports, domain }\` — Pathway 1 deploys from the Gitea repo. Coolify auto-redeploys on subsequent file writes.
|
||||
|
||||
## Troubleshooting
|
||||
- Deploy stuck or "exited (1)" → \`apps_logs { uuid }\` and \`apps_containers_list { uuid }\`. Common causes: missing env var, wrong port, image pull failure.
|
||||
- 502 / "no available server" → app probably has no public domain yet. Check \`apps_get\`; if \`fqdn\` is empty, attach a domain.
|
||||
|
||||
@@ -83,7 +83,17 @@ import {
|
||||
type CoolifyDatabaseType,
|
||||
} from '@/lib/coolify';
|
||||
import { query, queryOne } from '@/lib/db-postgres';
|
||||
import { getRepo } from '@/lib/gitea';
|
||||
import {
|
||||
getRepo,
|
||||
createRepo,
|
||||
giteaPushFile,
|
||||
giteaReadFile,
|
||||
giteaListContents,
|
||||
giteaListBranches,
|
||||
giteaCreateBranch,
|
||||
giteaListOrgRepos,
|
||||
giteaDeleteFile,
|
||||
} from '@/lib/gitea';
|
||||
import {
|
||||
giteaHttpsUrl,
|
||||
isDomainUnderWorkspace,
|
||||
@@ -101,7 +111,7 @@ const GITEA_API_URL = process.env.GITEA_API_URL ?? 'https://git.vibnai.com';
|
||||
export async function GET() {
|
||||
return NextResponse.json({
|
||||
name: 'vibn-mcp',
|
||||
version: '2.4.8',
|
||||
version: '2.5.0',
|
||||
authentication: {
|
||||
scheme: 'Bearer',
|
||||
tokenPrefix: 'vibn_sk_',
|
||||
@@ -155,6 +165,14 @@ export async function GET() {
|
||||
'storage.describe',
|
||||
'storage.provision',
|
||||
'storage.inject_env',
|
||||
'gitea.repos.list',
|
||||
'gitea.repo.get',
|
||||
'gitea.repo.create',
|
||||
'gitea.file.read',
|
||||
'gitea.file.write',
|
||||
'gitea.file.delete',
|
||||
'gitea.branches.list',
|
||||
'gitea.branch.create',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -283,6 +301,23 @@ export async function POST(request: Request) {
|
||||
case 'storage.inject_env':
|
||||
return await toolStorageInjectEnv(principal, params);
|
||||
|
||||
case 'gitea.repos.list':
|
||||
return await toolGiteaReposList(principal);
|
||||
case 'gitea.repo.get':
|
||||
return await toolGiteaRepoGet(principal, params);
|
||||
case 'gitea.repo.create':
|
||||
return await toolGiteaRepoCreate(principal, params);
|
||||
case 'gitea.file.read':
|
||||
return await toolGiteaFileRead(principal, params);
|
||||
case 'gitea.file.write':
|
||||
return await toolGiteaFileWrite(principal, params);
|
||||
case 'gitea.file.delete':
|
||||
return await toolGiteaFileDelete(principal, params);
|
||||
case 'gitea.branches.list':
|
||||
return await toolGiteaBranchesList(principal, params);
|
||||
case 'gitea.branch.create':
|
||||
return await toolGiteaBranchCreate(principal, params);
|
||||
|
||||
default:
|
||||
return NextResponse.json(
|
||||
{ error: `Unknown tool "${action}"` },
|
||||
@@ -2544,3 +2579,191 @@ async function toolStorageInjectEnv(principal: Principal, params: Record<string,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// Gitea — repos & file CRUD (lets the AI write code, not just deploy it)
|
||||
// ──────────────────────────────────────────────────
|
||||
//
|
||||
// Tenant safety: every operation is scoped to `principal.workspace.gitea_org`.
|
||||
// A workspace can never read or write into another workspace's repos because
|
||||
// requireGiteaOrg() rejects any path whose owner isn't the caller's org.
|
||||
|
||||
function requireGiteaOrg(principal: Principal): string | NextResponse {
|
||||
const org = principal.workspace.gitea_org;
|
||||
if (!org) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Workspace has no Gitea org yet (provisioning incomplete)' },
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
return org;
|
||||
}
|
||||
|
||||
function ensureRepoOwnerInOrg(
|
||||
ownerParam: unknown,
|
||||
org: string,
|
||||
): string | NextResponse {
|
||||
const owner = typeof ownerParam === 'string' && ownerParam.length > 0 ? ownerParam : org;
|
||||
if (owner !== org) {
|
||||
return NextResponse.json(
|
||||
{ error: `owner "${owner}" is outside this workspace's org "${org}"` },
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
return owner;
|
||||
}
|
||||
|
||||
async function toolGiteaReposList(principal: Principal) {
|
||||
const org = requireGiteaOrg(principal);
|
||||
if (org instanceof NextResponse) return org;
|
||||
const repos = await giteaListOrgRepos(org);
|
||||
return NextResponse.json({
|
||||
result: repos.map(r => ({
|
||||
name: r.name,
|
||||
fullName: r.full_name,
|
||||
defaultBranch: r.default_branch,
|
||||
cloneUrl: r.clone_url,
|
||||
htmlUrl: r.html_url,
|
||||
private: r.private,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
async function toolGiteaRepoGet(principal: Principal, params: Record<string, any>) {
|
||||
const org = requireGiteaOrg(principal);
|
||||
if (org instanceof NextResponse) return org;
|
||||
const owner = ensureRepoOwnerInOrg(params.owner, org);
|
||||
if (owner instanceof NextResponse) return owner;
|
||||
const name = String(params.repo ?? params.name ?? '').trim();
|
||||
if (!name) return NextResponse.json({ error: 'Param "repo" is required' }, { status: 400 });
|
||||
const r = await getRepo(owner, name);
|
||||
if (!r) return NextResponse.json({ error: `Repo ${owner}/${name} not found` }, { status: 404 });
|
||||
return NextResponse.json({
|
||||
result: {
|
||||
name: r.name,
|
||||
fullName: r.full_name,
|
||||
defaultBranch: r.default_branch,
|
||||
cloneUrl: r.clone_url,
|
||||
htmlUrl: r.html_url,
|
||||
private: r.private,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function toolGiteaRepoCreate(principal: Principal, params: Record<string, any>) {
|
||||
const org = requireGiteaOrg(principal);
|
||||
if (org instanceof NextResponse) return org;
|
||||
const name = slugify(String(params.name ?? '').trim());
|
||||
if (!name) return NextResponse.json({ error: 'Param "name" is required' }, { status: 400 });
|
||||
const repo = await createRepo(name, {
|
||||
owner: org,
|
||||
description: params.description ? String(params.description) : undefined,
|
||||
private: params.private !== false,
|
||||
auto_init: params.autoInit !== false,
|
||||
});
|
||||
return NextResponse.json({
|
||||
result: {
|
||||
name: repo.name,
|
||||
fullName: repo.full_name,
|
||||
defaultBranch: repo.default_branch,
|
||||
cloneUrl: repo.clone_url,
|
||||
htmlUrl: repo.html_url,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function toolGiteaFileRead(principal: Principal, params: Record<string, any>) {
|
||||
const org = requireGiteaOrg(principal);
|
||||
if (org instanceof NextResponse) return org;
|
||||
const owner = ensureRepoOwnerInOrg(params.owner, org);
|
||||
if (owner instanceof NextResponse) return owner;
|
||||
const repo = String(params.repo ?? '').trim();
|
||||
const path = String(params.path ?? '').trim();
|
||||
if (!repo || !path) {
|
||||
return NextResponse.json({ error: 'Params "repo" and "path" are required' }, { status: 400 });
|
||||
}
|
||||
const ref = params.ref ? String(params.ref) : undefined;
|
||||
// If path is a directory, list contents instead. The contents API returns
|
||||
// an array for dirs and an object for files.
|
||||
try {
|
||||
const file = await giteaReadFile(owner, repo, path, ref);
|
||||
return NextResponse.json({ result: { type: 'file', ...file } });
|
||||
} catch (err: any) {
|
||||
// Probably a directory — fall back to listing.
|
||||
try {
|
||||
const items = await giteaListContents(owner, repo, path, ref);
|
||||
return NextResponse.json({ result: { type: 'directory', items } });
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || `Path ${path} not found in ${owner}/${repo}` },
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function toolGiteaFileWrite(principal: Principal, params: Record<string, any>) {
|
||||
const org = requireGiteaOrg(principal);
|
||||
if (org instanceof NextResponse) return org;
|
||||
const owner = ensureRepoOwnerInOrg(params.owner, org);
|
||||
if (owner instanceof NextResponse) return owner;
|
||||
const repo = String(params.repo ?? '').trim();
|
||||
const path = String(params.path ?? '').trim();
|
||||
const content = typeof params.content === 'string' ? params.content : '';
|
||||
const message = String(params.message ?? `Update ${path}`).trim();
|
||||
const branch = String(params.branch ?? 'main').trim();
|
||||
if (!repo || !path) {
|
||||
return NextResponse.json({ error: 'Params "repo" and "path" are required' }, { status: 400 });
|
||||
}
|
||||
await giteaPushFile(owner, repo, path, content, message, branch);
|
||||
return NextResponse.json({
|
||||
result: { ok: true, owner, repo, path, branch, bytes: Buffer.byteLength(content, 'utf-8') },
|
||||
});
|
||||
}
|
||||
|
||||
async function toolGiteaFileDelete(principal: Principal, params: Record<string, any>) {
|
||||
const org = requireGiteaOrg(principal);
|
||||
if (org instanceof NextResponse) return org;
|
||||
const owner = ensureRepoOwnerInOrg(params.owner, org);
|
||||
if (owner instanceof NextResponse) return owner;
|
||||
const repo = String(params.repo ?? '').trim();
|
||||
const path = String(params.path ?? '').trim();
|
||||
const branch = String(params.branch ?? 'main').trim();
|
||||
const message = String(params.message ?? `Delete ${path}`).trim();
|
||||
if (!repo || !path) {
|
||||
return NextResponse.json({ error: 'Params "repo" and "path" are required' }, { status: 400 });
|
||||
}
|
||||
// Need the file's current sha to delete; fetch it first.
|
||||
const file = await giteaReadFile(owner, repo, path, branch).catch(() => null);
|
||||
if (!file) return NextResponse.json({ error: `${path} not found` }, { status: 404 });
|
||||
await giteaDeleteFile(owner, repo, path, file.sha, message, branch);
|
||||
return NextResponse.json({ result: { ok: true, owner, repo, path, branch } });
|
||||
}
|
||||
|
||||
async function toolGiteaBranchesList(principal: Principal, params: Record<string, any>) {
|
||||
const org = requireGiteaOrg(principal);
|
||||
if (org instanceof NextResponse) return org;
|
||||
const owner = ensureRepoOwnerInOrg(params.owner, org);
|
||||
if (owner instanceof NextResponse) return owner;
|
||||
const repo = String(params.repo ?? '').trim();
|
||||
if (!repo) return NextResponse.json({ error: 'Param "repo" is required' }, { status: 400 });
|
||||
const branches = await giteaListBranches(owner, repo);
|
||||
return NextResponse.json({
|
||||
result: branches.map(b => ({ name: b.name, sha: b.commit?.id, protected: b.protected })),
|
||||
});
|
||||
}
|
||||
|
||||
async function toolGiteaBranchCreate(principal: Principal, params: Record<string, any>) {
|
||||
const org = requireGiteaOrg(principal);
|
||||
if (org instanceof NextResponse) return org;
|
||||
const owner = ensureRepoOwnerInOrg(params.owner, org);
|
||||
if (owner instanceof NextResponse) return owner;
|
||||
const repo = String(params.repo ?? '').trim();
|
||||
const name = String(params.name ?? params.branch ?? '').trim();
|
||||
const fromBranch = params.from ? String(params.from) : undefined;
|
||||
if (!repo || !name) {
|
||||
return NextResponse.json({ error: 'Params "repo" and "name" are required' }, { status: 400 });
|
||||
}
|
||||
const b = await giteaCreateBranch(owner, repo, name, fromBranch);
|
||||
return NextResponse.json({ result: { name: b.name, sha: b.commit?.id } });
|
||||
}
|
||||
|
||||
@@ -518,6 +518,125 @@ Auto-domain {name}.{workspace}.vibnai.com is assigned automatically.`,
|
||||
},
|
||||
},
|
||||
|
||||
// ── Gitea — repos & file CRUD (write code, not just deploy it) ────────────
|
||||
//
|
||||
// All gitea_* tools are scoped to the workspace's Gitea org. The AI can
|
||||
// create repos, read & write files, and manage branches inside its own
|
||||
// org but never outside it (enforced by requireGiteaOrg + ensureRepoOwnerInOrg
|
||||
// in the MCP route).
|
||||
|
||||
{
|
||||
name: 'gitea_repos_list',
|
||||
description:
|
||||
'List every Gitea repo in the workspace org. Use to discover repos already provisioned for projects.',
|
||||
parameters: { type: 'OBJECT', properties: {}, required: [] },
|
||||
},
|
||||
{
|
||||
name: 'gitea_repo_get',
|
||||
description:
|
||||
'Get metadata for a single Gitea repo (default branch, clone URL, html URL, private flag).',
|
||||
parameters: {
|
||||
type: 'OBJECT',
|
||||
properties: {
|
||||
repo: { type: 'STRING', description: 'Repo name (without org prefix).' },
|
||||
owner: { type: 'STRING', description: 'Optional org/user. Defaults to the workspace Gitea org.' },
|
||||
},
|
||||
required: ['repo'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'gitea_repo_create',
|
||||
description:
|
||||
'Create a new Gitea repo inside the workspace org. By default initializes with a README and is private. ' +
|
||||
'Use this when scaffolding a new app the AI is going to write code for and then deploy via apps_create({ repo }).',
|
||||
parameters: {
|
||||
type: 'OBJECT',
|
||||
properties: {
|
||||
name: { type: 'STRING', description: 'Repo name. Will be slugified (lowercase, hyphens).' },
|
||||
description: { type: 'STRING', description: 'Optional repo description.' },
|
||||
private: { type: 'BOOLEAN', description: 'Whether the repo is private (default true).' },
|
||||
autoInit: { type: 'BOOLEAN', description: 'Initialize with README (default true). Set false if writing files immediately yourself.' },
|
||||
},
|
||||
required: ['name'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'gitea_file_read',
|
||||
description:
|
||||
'Read a single file from a workspace Gitea repo, OR list directory contents if path is a directory. ' +
|
||||
'Returns { type: "file", content, sha, size, encoding } or { type: "directory", items: [...] }.',
|
||||
parameters: {
|
||||
type: 'OBJECT',
|
||||
properties: {
|
||||
repo: { type: 'STRING', description: 'Repo name.' },
|
||||
path: { type: 'STRING', description: 'File or directory path within the repo.' },
|
||||
ref: { type: 'STRING', description: 'Branch, tag, or commit SHA (default: repo default branch).' },
|
||||
owner: { type: 'STRING', description: 'Optional org. Defaults to the workspace Gitea org.' },
|
||||
},
|
||||
required: ['repo', 'path'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'gitea_file_write',
|
||||
description:
|
||||
'Create or update a single file in a workspace Gitea repo (one commit per call). Use to scaffold or patch code: ' +
|
||||
'package.json, Dockerfile, src/server.ts, README.md, etc. Always include a meaningful commit message.',
|
||||
parameters: {
|
||||
type: 'OBJECT',
|
||||
properties: {
|
||||
repo: { type: 'STRING', description: 'Repo name.' },
|
||||
path: { type: 'STRING', description: 'File path within the repo (e.g. "src/index.ts" or "Dockerfile").' },
|
||||
content: { type: 'STRING', description: 'Full new file content as a UTF-8 string.' },
|
||||
message: { type: 'STRING', description: 'Commit message. Default: "Update {path}".' },
|
||||
branch: { type: 'STRING', description: 'Target branch (default "main"). Branch must already exist.' },
|
||||
owner: { type: 'STRING', description: 'Optional org. Defaults to the workspace Gitea org.' },
|
||||
},
|
||||
required: ['repo', 'path', 'content'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'gitea_file_delete',
|
||||
description: 'Delete a single file from a workspace Gitea repo (one commit). Resolves the file sha automatically.',
|
||||
parameters: {
|
||||
type: 'OBJECT',
|
||||
properties: {
|
||||
repo: { type: 'STRING', description: 'Repo name.' },
|
||||
path: { type: 'STRING', description: 'File path within the repo.' },
|
||||
message: { type: 'STRING', description: 'Commit message. Default: "Delete {path}".' },
|
||||
branch: { type: 'STRING', description: 'Target branch (default "main").' },
|
||||
owner: { type: 'STRING', description: 'Optional org. Defaults to the workspace Gitea org.' },
|
||||
},
|
||||
required: ['repo', 'path'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'gitea_branches_list',
|
||||
description: 'List all branches of a workspace Gitea repo with their head SHA.',
|
||||
parameters: {
|
||||
type: 'OBJECT',
|
||||
properties: {
|
||||
repo: { type: 'STRING', description: 'Repo name.' },
|
||||
owner: { type: 'STRING', description: 'Optional org. Defaults to the workspace Gitea org.' },
|
||||
},
|
||||
required: ['repo'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'gitea_branch_create',
|
||||
description:
|
||||
'Create a new branch in a workspace Gitea repo, branched off an existing one (default = repo default branch).',
|
||||
parameters: {
|
||||
type: 'OBJECT',
|
||||
properties: {
|
||||
repo: { type: 'STRING', description: 'Repo name.' },
|
||||
name: { type: 'STRING', description: 'New branch name.' },
|
||||
from: { type: 'STRING', description: 'Existing branch to branch from (default: repo default branch).' },
|
||||
owner: { type: 'STRING', description: 'Optional org. Defaults to the workspace Gitea org.' },
|
||||
},
|
||||
required: ['repo', 'name'],
|
||||
},
|
||||
},
|
||||
|
||||
// ── Non-MCP: GitHub & web ─────────────────────────────────────────────────
|
||||
|
||||
{
|
||||
|
||||
91
lib/gitea.ts
91
lib/gitea.ts
@@ -521,4 +521,95 @@ export async function giteaPushFile(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a file from a Gitea repo. Returns decoded content + sha.
|
||||
* Throws if path is missing.
|
||||
*/
|
||||
export async function giteaReadFile(
|
||||
owner: string,
|
||||
repo: string,
|
||||
path: string,
|
||||
ref?: string,
|
||||
): Promise<{ content: string; sha: string; size: number; encoding: string }> {
|
||||
const qs = ref ? `?ref=${encodeURIComponent(ref)}` : '';
|
||||
const data = await giteaFetch(`/repos/${owner}/${repo}/contents/${encodeURIComponent(path)}${qs}`);
|
||||
const d = data as any;
|
||||
const raw = d.content && d.encoding === 'base64'
|
||||
? Buffer.from(d.content, 'base64').toString('utf-8')
|
||||
: (d.content ?? '');
|
||||
return { content: raw, sha: d.sha, size: d.size ?? 0, encoding: d.encoding ?? 'utf-8' };
|
||||
}
|
||||
|
||||
/** List files/folders at a path inside a Gitea repo. */
|
||||
export async function giteaListContents(
|
||||
owner: string,
|
||||
repo: string,
|
||||
path = '',
|
||||
ref?: string,
|
||||
): Promise<Array<{ name: string; path: string; type: string; size: number; sha: string }>> {
|
||||
const qs = ref ? `?ref=${encodeURIComponent(ref)}` : '';
|
||||
const data = await giteaFetch(`/repos/${owner}/${repo}/contents/${encodeURIComponent(path)}${qs}`);
|
||||
const arr = Array.isArray(data) ? data : [data];
|
||||
return arr.map((d: any) => ({
|
||||
name: d.name,
|
||||
path: d.path,
|
||||
type: d.type,
|
||||
size: d.size ?? 0,
|
||||
sha: d.sha,
|
||||
}));
|
||||
}
|
||||
|
||||
export interface GiteaBranch {
|
||||
name: string;
|
||||
commit: { id: string; message?: string };
|
||||
protected: boolean;
|
||||
}
|
||||
|
||||
export async function giteaListBranches(owner: string, repo: string): Promise<GiteaBranch[]> {
|
||||
const data = await giteaFetch(`/repos/${owner}/${repo}/branches`);
|
||||
return Array.isArray(data) ? (data as GiteaBranch[]) : [];
|
||||
}
|
||||
|
||||
/** Create a new branch from an existing one (defaults to repo's default branch). */
|
||||
export async function giteaCreateBranch(
|
||||
owner: string,
|
||||
repo: string,
|
||||
newBranch: string,
|
||||
fromBranch?: string,
|
||||
): Promise<GiteaBranch> {
|
||||
return giteaFetch(`/repos/${owner}/${repo}/branches`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
new_branch_name: newBranch,
|
||||
...(fromBranch ? { old_branch_name: fromBranch } : {}),
|
||||
}),
|
||||
}) as Promise<GiteaBranch>;
|
||||
}
|
||||
|
||||
/** List repos owned by an org (or user). */
|
||||
export async function giteaListOrgRepos(orgOrUser: string): Promise<GiteaRepo[]> {
|
||||
const data = await giteaFetch(`/orgs/${orgOrUser}/repos`).catch(async () => {
|
||||
// If it's not an org, try the user endpoint.
|
||||
return giteaFetch(`/users/${orgOrUser}/repos`);
|
||||
});
|
||||
return Array.isArray(data) ? (data as GiteaRepo[]) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a single file at path. Requires the file's current sha.
|
||||
*/
|
||||
export async function giteaDeleteFile(
|
||||
owner: string,
|
||||
repo: string,
|
||||
path: string,
|
||||
sha: string,
|
||||
message: string,
|
||||
branch = 'main',
|
||||
): Promise<void> {
|
||||
await giteaFetch(`/repos/${owner}/${repo}/contents/${encodeURIComponent(path)}`, {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify({ message, sha, branch }),
|
||||
});
|
||||
}
|
||||
|
||||
export const GITEA_ADMIN_USER_EXPORT = GITEA_ADMIN_USER;
|
||||
|
||||
Reference in New Issue
Block a user