feat: add GitHub service for PAT validation, forking, and PR management
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
export const REPOS = [
|
||||
{ name: "nwn-module", upstream: "layonara/nwn-module", branch: "ee" },
|
||||
{ name: "nwn-haks", upstream: "layonara/nwn-haks", branch: "64bit" },
|
||||
{ name: "unified", upstream: "plenarius/unified", branch: "master" },
|
||||
] as const;
|
||||
|
||||
export type RepoName = (typeof REPOS)[number]["name"];
|
||||
@@ -12,6 +12,7 @@ import terminalRouter from "./routes/terminal.js";
|
||||
import buildRouter from "./routes/build.js";
|
||||
import serverRouter from "./routes/server.js";
|
||||
import toolsetRouter from "./routes/toolset.js";
|
||||
import githubRouter from "./routes/github.js";
|
||||
import { attachWebSocket, createTerminalSession } from "./services/terminal.service.js";
|
||||
import { attachLspWebSocket } from "./services/lsp.service.js";
|
||||
|
||||
@@ -39,6 +40,7 @@ app.use("/api/terminal", terminalRouter);
|
||||
app.use("/api/build", buildRouter);
|
||||
app.use("/api/server", serverRouter);
|
||||
app.use("/api/toolset", toolsetRouter);
|
||||
app.use("/api/github", githubRouter);
|
||||
|
||||
const frontendDist = path.resolve(__dirname, "../../frontend/dist");
|
||||
app.use(express.static(frontendDist));
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import { Router } from "express";
|
||||
import { readConfig, writeConfig } from "../services/workspace.service.js";
|
||||
import { REPOS } from "../config/repos.js";
|
||||
import {
|
||||
validatePat,
|
||||
forkRepo,
|
||||
listUserForks,
|
||||
createPullRequest,
|
||||
listPullRequests,
|
||||
} from "../services/github.service.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
async function getPat(): Promise<string> {
|
||||
const config = await readConfig();
|
||||
const pat = config.githubPat;
|
||||
if (!pat) throw new Error("GitHub PAT not configured");
|
||||
return pat;
|
||||
}
|
||||
|
||||
router.post("/validate-pat", async (req, res) => {
|
||||
try {
|
||||
const { pat } = req.body;
|
||||
if (!pat) {
|
||||
res.status(400).json({ error: "PAT is required" });
|
||||
return;
|
||||
}
|
||||
const result = await validatePat(pat);
|
||||
|
||||
const config = await readConfig();
|
||||
await writeConfig({ ...config, githubPat: pat });
|
||||
|
||||
res.json(result);
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : "Invalid PAT";
|
||||
res.status(401).json({ error: message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/fork", async (req, res) => {
|
||||
try {
|
||||
const pat = await getPat();
|
||||
const { repo: repoName } = req.body;
|
||||
const repoDef = REPOS.find((r) => r.name === repoName);
|
||||
if (!repoDef) {
|
||||
res.status(400).json({ error: `Unknown repo: ${repoName}` });
|
||||
return;
|
||||
}
|
||||
const [owner, repo] = repoDef.upstream.split("/");
|
||||
const result = await forkRepo(pat, owner, repo);
|
||||
res.json(result);
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : "Fork failed";
|
||||
res.status(500).json({ error: message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/forks", async (_req, res) => {
|
||||
try {
|
||||
const pat = await getPat();
|
||||
const forks = await listUserForks(pat);
|
||||
res.json(forks);
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : "Failed to list forks";
|
||||
res.status(500).json({ error: message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/pr", async (req, res) => {
|
||||
try {
|
||||
const pat = await getPat();
|
||||
const { repo: repoName, title, body, headBranch } = req.body;
|
||||
const repoDef = REPOS.find((r) => r.name === repoName);
|
||||
if (!repoDef) {
|
||||
res.status(400).json({ error: `Unknown repo: ${repoName}` });
|
||||
return;
|
||||
}
|
||||
|
||||
const { login } = await import("../services/github.service.js").then((m) =>
|
||||
m.validatePat(pat),
|
||||
);
|
||||
|
||||
const result = await createPullRequest(pat, {
|
||||
upstream: repoDef.upstream,
|
||||
repo: repoName,
|
||||
title,
|
||||
body,
|
||||
head: `${login}:${headBranch}`,
|
||||
base: repoDef.branch,
|
||||
});
|
||||
res.json(result);
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : "PR creation failed";
|
||||
res.status(500).json({ error: message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/prs/:repo", async (req, res) => {
|
||||
try {
|
||||
const pat = await getPat();
|
||||
const repoName = req.params.repo;
|
||||
const repoDef = REPOS.find((r) => r.name === repoName);
|
||||
if (!repoDef) {
|
||||
res.status(400).json({ error: `Unknown repo: ${repoName}` });
|
||||
return;
|
||||
}
|
||||
const [owner, repo] = repoDef.upstream.split("/");
|
||||
const prs = await listPullRequests(pat, owner, repo);
|
||||
res.json(prs);
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : "Failed to list PRs";
|
||||
res.status(500).json({ error: message });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Octokit } from "@octokit/rest";
|
||||
import { REPOS } from "../config/repos.js";
|
||||
|
||||
export async function validatePat(pat: string) {
|
||||
const octokit = new Octokit({ auth: pat });
|
||||
const { data, headers } = await octokit.rest.users.getAuthenticated();
|
||||
const scopes = (headers["x-oauth-scopes"] || "")
|
||||
.split(",")
|
||||
.map((s: string) => s.trim())
|
||||
.filter(Boolean);
|
||||
return { login: data.login, scopes };
|
||||
}
|
||||
|
||||
export async function forkRepo(pat: string, owner: string, repo: string) {
|
||||
const octokit = new Octokit({ auth: pat });
|
||||
const { data } = await octokit.rest.repos.createFork({ owner, repo });
|
||||
return { fullName: data.full_name, cloneUrl: data.clone_url };
|
||||
}
|
||||
|
||||
export async function listUserForks(pat: string) {
|
||||
const octokit = new Octokit({ auth: pat });
|
||||
const { data: user } = await octokit.rest.users.getAuthenticated();
|
||||
const login = user.login;
|
||||
|
||||
const results: Array<{ repo: string; forked: boolean; fullName?: string }> = [];
|
||||
|
||||
for (const repo of REPOS) {
|
||||
try {
|
||||
const { data } = await octokit.rest.repos.get({
|
||||
owner: login,
|
||||
repo: repo.name,
|
||||
});
|
||||
results.push({ repo: repo.name, forked: true, fullName: data.full_name });
|
||||
} catch {
|
||||
results.push({ repo: repo.name, forked: false });
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
export async function createPullRequest(
|
||||
pat: string,
|
||||
opts: { upstream: string; repo: string; title: string; body: string; head: string; base: string },
|
||||
) {
|
||||
const octokit = new Octokit({ auth: pat });
|
||||
const [owner, repo] = opts.upstream.split("/");
|
||||
const { data } = await octokit.rest.pulls.create({
|
||||
owner,
|
||||
repo,
|
||||
title: opts.title,
|
||||
body: opts.body,
|
||||
head: opts.head,
|
||||
base: opts.base,
|
||||
});
|
||||
return { number: data.number, url: data.html_url };
|
||||
}
|
||||
|
||||
export async function listPullRequests(pat: string, owner: string, repo: string) {
|
||||
const octokit = new Octokit({ auth: pat });
|
||||
const { data } = await octokit.rest.pulls.list({ owner, repo, state: "open" });
|
||||
return data.map((pr) => ({
|
||||
number: pr.number,
|
||||
title: pr.title,
|
||||
state: pr.state,
|
||||
url: pr.html_url,
|
||||
user: pr.user?.login || "",
|
||||
createdAt: pr.created_at,
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user