* 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
150 lines
4.4 KiB
JavaScript
150 lines
4.4 KiB
JavaScript
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;
|