Skip to main content
The GitHubService handles GitHub authentication, repository management, and pull request operations in Emdash. It uses OAuth Device Flow for authentication and the gh CLI for Git/GitHub operations.

Overview

Key features:
  • OAuth Device Flow: Browser-based authentication with automatic background polling
  • gh CLI integration: Leverages GitHub CLI for repository and PR operations
  • Secure token storage: Stores OAuth tokens in OS keychain (via keytar)
  • Automatic re-auth: Detects auth failures and re-authenticates gh CLI
  • Issue search: Search and view GitHub issues
  • PR management: List, checkout, and create pull requests
Authentication flow:
  1. Request device code from GitHub
  2. Display code to user (browser opens to GitHub)
  3. Poll for token in background
  4. Store token in keychain
  5. Authenticate gh CLI with token

Authentication

startDeviceFlowAuth

Start OAuth Device Flow authentication with automatic background polling.
async startDeviceFlowAuth(): Promise<DeviceCodeResult>
DeviceCodeResult
object
success
boolean
Whether device code request succeeded
device_code
string
Device code for polling (internal use)
user_code
string
Code for user to enter on GitHub (e.g., A1B2-C3D4)
verification_uri
string
GitHub verification URL (e.g., https://github.com/login/device)
expires_in
number
Seconds until code expires (typically 900)
interval
number
Seconds between polling attempts (typically 5)
error
string
Error message (if request failed)
Process:
  1. Stops any existing polling loop
  2. Requests device code from GitHub
  3. Emits github:auth:device-code event to renderer (with 100ms delay)
  4. Starts background polling loop
  5. Returns device code result immediately
Events emitted to renderer:
  • github:auth:device-code: Device code ready for display
  • github:auth:polling: Still waiting for user authorization
  • github:auth:slow-down: GitHub requested slower polling
  • github:auth:success: Authentication succeeded (token acquired)
  • github:auth:error: Authentication failed or expired
  • github:auth:cancelled: User cancelled authentication
Example:
const result = await githubService.startDeviceFlowAuth();

if (result.success) {
  console.log('Visit:', result.verification_uri);
  console.log('Code:', result.user_code);
  console.log('Polling in background...');
  // Renderer will receive events as polling progresses
} else {
  console.error('Failed to start auth:', result.error);
}

pollDeviceToken

Poll for access token using device code (called automatically by background loop).
async pollDeviceToken(deviceCode: string, interval: number = 5): Promise<AuthResult>
deviceCode
string
required
Device code from requestDeviceCode
interval
number
Polling interval in seconds
AuthResult
object
success
boolean
Whether token was acquired
token
string
OAuth access token
user
GitHubUser
User information
error
string
Error code or message
Error codes:
  • authorization_pending: User hasn’t authorized yet (continue polling)
  • slow_down: GitHub wants slower polling (increase interval by 5s)
  • expired_token: Device code expired (stop polling)
  • access_denied: User denied authorization (stop polling)
Automatic actions on success:
  1. Stores token in keychain
  2. Authenticates gh CLI with token
  3. Fetches user info from GitHub API
  4. Emits github:auth:success event to renderer

cancelAuth

Cancel the authentication flow.
cancelAuth(): void
Example:
githubService.cancelAuth();
// Stops polling and emits github:auth:cancelled

isAuthenticated

Check if user is authenticated (checks gh CLI and keychain).
async isAuthenticated(): Promise<boolean>
Example:
const authenticated = await githubService.isAuthenticated();
if (authenticated) {
  console.log('User is authenticated');
} else {
  console.log('User needs to authenticate');
}

logout

Logout and clear stored token.
async logout(): Promise<void>
Process:
  1. Logs out from gh CLI: gh auth logout --hostname github.com
  2. Removes token from keychain
Example:
await githubService.logout();
console.log('Logged out');

User Information

getCurrentUser

Get the authenticated user’s information.
async getCurrentUser(): Promise<GitHubUser | null>
GitHubUser
object
id
number
GitHub user ID
login
string
Username
name
string
Full name
email
string
Email address
avatar_url
string
Avatar image URL
Example:
const user = await githubService.getCurrentUser();
if (user) {
  console.log(`Logged in as ${user.login} (${user.name})`);
}

getOwners

Get available owners (user + organizations).
async getOwners(): Promise<Array<{ login: string; type: 'User' | 'Organization' }>>
Example:
const owners = await githubService.getOwners();
for (const owner of owners) {
  console.log(`${owner.login} (${owner.type})`);
}
// Output:
// octocat (User)
// github (Organization)

Repository Operations

getRepositories

Get the user’s repositories (up to 100).
async getRepositories(token: string): Promise<GitHubRepo[]>
token
string
required
OAuth token (unused, kept for API compatibility)
GitHubRepo
object
id
number
Repository ID
name
string
Repository name
full_name
string
Owner/name (e.g., user/repo)
description
string
Repository description
html_url
string
GitHub repository URL
clone_url
string
HTTPS clone URL
ssh_url
string
SSH clone URL
default_branch
string
Default branch name (e.g., main)
private
boolean
Whether repository is private
updated_at
string
Last update timestamp
language
string
Primary language
stargazers_count
number
Star count
forks_count
number
Fork count
Example:
const repos = await githubService.getRepositories('');
for (const repo of repos) {
  console.log(`${repo.full_name}: ${repo.description}`);
}

createRepository

Create a new GitHub repository.
async createRepository(params: {
  name: string;
  description?: string;
  owner: string;
  isPrivate: boolean;
}): Promise<{ url: string; defaultBranch: string; fullName: string }>
name
string
required
Repository name (validated via validateRepositoryName)
description
string
Repository description
owner
string
required
Owner login (user or organization)
isPrivate
boolean
required
Whether repository should be private
Example:
const repo = await githubService.createRepository({
  name: 'my-new-repo',
  description: 'A cool project',
  owner: 'octocat',
  isPrivate: false,
});

console.log(`Created: ${repo.url}`);
// https://github.com/octocat/my-new-repo

validateRepositoryName

Validate a repository name against GitHub rules.
validateRepositoryName(name: string): { valid: boolean; error?: string }
Rules:
  • Required (non-empty)
  • Max 100 characters
  • Alphanumeric, hyphens, underscores, dots only
  • Cannot start/end with hyphen, dot, or underscore
  • Cannot be all dots
  • Cannot be a reserved name (e.g., con, prn, aux)
Example:
const result = githubService.validateRepositoryName('my-repo');
if (result.valid) {
  console.log('Valid name');
} else {
  console.error('Invalid:', result.error);
}

checkRepositoryExists

Check if a repository exists.
async checkRepositoryExists(owner: string, name: string): Promise<boolean>
Example:
const exists = await githubService.checkRepositoryExists('octocat', 'Hello-World');
if (exists) {
  console.log('Repository exists');
}

cloneRepository

Clone a repository to a local path.
async cloneRepository(
  repoUrl: string,
  localPath: string
): Promise<{ success: boolean; error?: string }>
repoUrl
string
required
HTTPS or SSH clone URL
localPath
string
required
Absolute path to clone destination
Example:
const result = await githubService.cloneRepository(
  'https://github.com/octocat/Hello-World.git',
  '/Users/dev/Hello-World'
);

if (result.success) {
  console.log('Cloned successfully');
} else {
  console.error('Clone failed:', result.error);
}

initializeNewProject

Initialize a new project with README and initial commit.
async initializeNewProject(params: {
  repoUrl: string;
  localPath: string;
  name: string;
  description?: string;
}): Promise<void>
repoUrl
string
required
Repository URL
localPath
string
required
Local project path
name
string
required
Project name
description
string
Project description
Process:
  1. Creates README.md with name and description
  2. Runs git add README.md
  3. Commits with message “Initial commit”
  4. Pushes to origin/main (or origin/master fallback)
Example:
await githubService.initializeNewProject({
  repoUrl: 'https://github.com/octocat/my-project.git',
  localPath: '/Users/dev/my-project',
  name: 'My Project',
  description: 'A cool project',
});

Pull Request Operations

getPullRequests

List open pull requests for a repository.
async getPullRequests(projectPath: string): Promise<GitHubPullRequest[]>
projectPath
string
required
Absolute path to Git repository
GitHubPullRequest
object
number
number
PR number
title
string
PR title
headRefName
string
Head branch name
baseRefName
string
Base branch name
url
string
GitHub PR URL
isDraft
boolean
Whether PR is a draft
updatedAt
string
Last update timestamp
headRefOid
string
Head commit SHA
author
object
PR author metadata
headRepositoryOwner
object
Head repository owner
headRepository
object
Head repository metadata
Example:
const prs = await githubService.getPullRequests('/Users/dev/my-project');
for (const pr of prs) {
  console.log(`#${pr.number}: ${pr.title} (${pr.headRefName}${pr.baseRefName})`);
}

ensurePullRequestBranch

Checkout a pull request branch locally (creates or updates).
async ensurePullRequestBranch(
  projectPath: string,
  prNumber: number,
  branchName: string
): Promise<string>
projectPath
string
required
Repository path
prNumber
number
required
PR number
branchName
string
required
Local branch name (defaults to pr/{number} if empty)
Process:
  1. Saves current branch
  2. Runs gh pr checkout {prNumber} --branch {branchName} --force
  3. Restores previous branch
Example:
const branch = await githubService.ensurePullRequestBranch(
  '/Users/dev/my-project',
  42,
  'pr/fix-login'
);

console.log(`PR #42 checked out to: ${branch}`);
// pr/fix-login

Issue Operations

listIssues

List open issues for a repository.
async listIssues(
  projectPath: string,
  limit: number = 50
): Promise<Array<{
  number: number;
  title: string;
  url?: string;
  state?: string;
  updatedAt?: string | null;
  assignees?: Array<{ login?: string; name?: string }>;
  labels?: Array<{ name?: string }>;
}>>
projectPath
string
required
Repository path
limit
number
Max issues to return (1-200, default: 50)
Example:
const issues = await githubService.listIssues('/Users/dev/my-project', 20);
for (const issue of issues) {
  console.log(`#${issue.number}: ${issue.title}`);
}

searchIssues

Search open issues in a repository.
async searchIssues(
  projectPath: string,
  searchTerm: string,
  limit: number = 20
): Promise<Array<{
  number: number;
  title: string;
  url?: string;
  state?: string;
  updatedAt?: string | null;
  assignees?: Array<{ login?: string; name?: string }>;
  labels?: Array<{ name?: string }>;
}>>
searchTerm
string
required
Search query
Example:
const results = await githubService.searchIssues(
  '/Users/dev/my-project',
  'login bug',
  10
);

for (const issue of results) {
  console.log(`#${issue.number}: ${issue.title}`);
}

getIssue

Get a single issue with body (for enrichment).
async getIssue(
  projectPath: string,
  number: number
): Promise<{
  number: number;
  title?: string;
  body?: string;
  url?: string;
  state?: string;
  updatedAt?: string | null;
  assignees?: Array<{ login?: string; name?: string }>;
  labels?: Array<{ name?: string }>;
} | null>
Example:
const issue = await githubService.getIssue('/Users/dev/my-project', 42);
if (issue) {
  console.log(`#${issue.number}: ${issue.title}`);
  console.log(issue.body);
}

Security

Token Storage

OAuth tokens are stored in the OS keychain via keytar:
  • macOS: Keychain Access
  • Linux: libsecret (GNOME Keyring, KWallet)
  • Windows: Credential Vault
Service name: emdash-github Account name: github-token

gh CLI Authentication

Tokens are passed to gh CLI via stdin (not shell interpolation) to prevent command injection:
const child = spawn('gh', ['auth', 'login', '--with-token'], {
  stdio: ['pipe', 'pipe', 'pipe'],
});

child.stdin.write(token);
child.stdin.end();

Automatic Re-authentication

The execGH method detects auth failures and re-authenticates:
try {
  return await execAsync(command);
} catch (error) {
  if (error.message.includes('not authenticated')) {
    const token = await this.getStoredToken();
    if (token) {
      await this.authenticateGHCLI(token);
      return await execAsync(command); // Retry
    }
  }
  throw error;
}

PrGenerationService

The PrGenerationService is a companion service that generates PR titles and descriptions using CLI agents or heuristics. Source: src/main/services/PrGenerationService.ts

generatePrContent

Generate PR title and description based on git changes.
async generatePrContent(
  taskPath: string,
  baseBranch: string = 'main',
  preferredProviderId?: string | null
): Promise<GeneratedPrContent>
taskPath
string
required
Worktree or repository path
baseBranch
string
Base branch to compare against (default: main)
preferredProviderId
string
Optional provider ID to try first (e.g., from task.agentId)
GeneratedPrContent
object
title
string
PR title (max 72 chars, conventional commit format)
description
string
Markdown PR description
Provider order:
  1. Preferred provider (if specified and available)
  2. Claude (most reliable for structured output)
  3. Remaining providers (codex, qwen, etc.)
  4. Fallback to heuristics (commit messages + file analysis)
Example:
import { prGenerationService } from './services/PrGenerationService';

const content = await prGenerationService.generatePrContent(
  '/Users/dev/worktrees/fix-login-bug-a7f',
  'origin/main',
  'claude'
);

console.log('Title:', content.title);
console.log('Description:');
console.log(content.description);

Location

Source: src/main/services/GitHubService.ts PR Generation: src/main/services/PrGenerationService.ts IPC Handlers: src/main/ipc/githubIpc.ts Singleton export:
export const githubService = new GitHubService();
export const prGenerationService = new PrGenerationService();
  • GitService (src/main/services/GitService.ts): Low-level Git operations
  • WorktreeService (src/main/services/WorktreeService.ts): Worktree management
  • ErrorTracking (src/main/errorTracking.ts): Telemetry and error capture

See Also

Build docs developers (and LLMs) love