Add GitHub integration for PRs, issues and AI PR description (#205)
* feat: integrate GitHub OAuth device flow across runtimes Add GitHub OAuth device flow endpoints across runtimes Introduce GitHubSettings UI panel and sidebar entry Persist GitHub auth state in per-runtime storage * feat: add GitHub PR status and PR description generation Show PR status for the current branch in the Git view Generate a pull request description from the diff between base and head Expose prStatus, prCreate, and prMerge APIs in web and desktop clients * feat: add GitHub PR ready for review Add API to mark pull requests as ready for review Show a Ready button for draft PRs and reflect status in UI Handle token expiration and GraphQL errors when marking ready
This commit is contained in:
committed by
GitHub
parent
0e715be7d6
commit
463e9ec4e3
@@ -0,0 +1,149 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
|
||||
const OPENCHAMBER_DATA_DIR = process.env.OPENCHAMBER_DATA_DIR
|
||||
? path.resolve(process.env.OPENCHAMBER_DATA_DIR)
|
||||
: path.join(os.homedir(), '.config', 'openchamber');
|
||||
|
||||
const STORAGE_DIR = OPENCHAMBER_DATA_DIR;
|
||||
const STORAGE_FILE = path.join(STORAGE_DIR, 'github-auth.json');
|
||||
const SETTINGS_FILE = path.join(OPENCHAMBER_DATA_DIR, 'settings.json');
|
||||
|
||||
const DEFAULT_GITHUB_CLIENT_ID = 'Ov23liNd8TxDcMXtAHHM';
|
||||
const DEFAULT_GITHUB_SCOPES = 'repo read:org workflow read:user user:email';
|
||||
|
||||
function ensureStorageDir() {
|
||||
if (!fs.existsSync(STORAGE_DIR)) {
|
||||
fs.mkdirSync(STORAGE_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function readJsonFile() {
|
||||
ensureStorageDir();
|
||||
if (!fs.existsSync(STORAGE_FILE)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const raw = fs.readFileSync(STORAGE_FILE, 'utf8');
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
const parsed = JSON.parse(trimmed);
|
||||
if (!parsed || typeof parsed !== 'object') {
|
||||
return null;
|
||||
}
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
console.error('Failed to read GitHub auth file:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeJsonFile(payload) {
|
||||
ensureStorageDir();
|
||||
fs.writeFileSync(STORAGE_FILE, JSON.stringify(payload, null, 2), 'utf8');
|
||||
try {
|
||||
fs.chmodSync(STORAGE_FILE, 0o600);
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
}
|
||||
|
||||
export function getGitHubAuth() {
|
||||
const data = readJsonFile();
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
const accessToken = typeof data.accessToken === 'string' ? data.accessToken : '';
|
||||
if (!accessToken) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
accessToken,
|
||||
scope: typeof data.scope === 'string' ? data.scope : '',
|
||||
tokenType: typeof data.tokenType === 'string' ? data.tokenType : 'bearer',
|
||||
createdAt: typeof data.createdAt === 'number' ? data.createdAt : null,
|
||||
user: data.user && typeof data.user === 'object'
|
||||
? {
|
||||
login: typeof data.user.login === 'string' ? data.user.login : null,
|
||||
avatarUrl: typeof data.user.avatarUrl === 'string' ? data.user.avatarUrl : null,
|
||||
id: typeof data.user.id === 'number' ? data.user.id : null,
|
||||
name: typeof data.user.name === 'string' ? data.user.name : null,
|
||||
email: typeof data.user.email === 'string' ? data.user.email : null,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function setGitHubAuth({ accessToken, scope, tokenType, user }) {
|
||||
if (!accessToken || typeof accessToken !== 'string') {
|
||||
throw new Error('accessToken is required');
|
||||
}
|
||||
writeJsonFile({
|
||||
accessToken,
|
||||
scope: typeof scope === 'string' ? scope : '',
|
||||
tokenType: typeof tokenType === 'string' ? tokenType : 'bearer',
|
||||
createdAt: Date.now(),
|
||||
user: user && typeof user === 'object'
|
||||
? {
|
||||
login: typeof user.login === 'string' ? user.login : undefined,
|
||||
avatarUrl: typeof user.avatarUrl === 'string' ? user.avatarUrl : undefined,
|
||||
id: typeof user.id === 'number' ? user.id : undefined,
|
||||
name: typeof user.name === 'string' ? user.name : undefined,
|
||||
email: typeof user.email === 'string' ? user.email : undefined,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
export function clearGitHubAuth() {
|
||||
try {
|
||||
if (fs.existsSync(STORAGE_FILE)) {
|
||||
fs.unlinkSync(STORAGE_FILE);
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to clear GitHub auth file:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function getGitHubClientId() {
|
||||
const raw = process.env.OPENCHAMBER_GITHUB_CLIENT_ID;
|
||||
const clientId = typeof raw === 'string' ? raw.trim() : '';
|
||||
if (clientId) return clientId;
|
||||
|
||||
try {
|
||||
if (fs.existsSync(SETTINGS_FILE)) {
|
||||
const parsed = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8'));
|
||||
const stored = typeof parsed?.githubClientId === 'string' ? parsed.githubClientId.trim() : '';
|
||||
if (stored) return stored;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return DEFAULT_GITHUB_CLIENT_ID;
|
||||
}
|
||||
|
||||
export function getGitHubScopes() {
|
||||
const raw = process.env.OPENCHAMBER_GITHUB_SCOPES;
|
||||
const fromEnv = typeof raw === 'string' ? raw.trim() : '';
|
||||
if (fromEnv) return fromEnv;
|
||||
|
||||
try {
|
||||
if (fs.existsSync(SETTINGS_FILE)) {
|
||||
const parsed = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8'));
|
||||
const stored = typeof parsed?.githubScopes === 'string' ? parsed.githubScopes.trim() : '';
|
||||
if (stored) return stored;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return DEFAULT_GITHUB_SCOPES;
|
||||
}
|
||||
|
||||
export const GITHUB_AUTH_FILE = STORAGE_FILE;
|
||||
Reference in New Issue
Block a user