Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions drizzle/0002_add_run_config_status_to_projects.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
ALTER TABLE `projects` ADD COLUMN `run_config_status` text;
--> statement-breakpoint
ALTER TABLE `projects` ADD COLUMN `run_config_error` text;
--> statement-breakpoint
ALTER TABLE `projects` ADD COLUMN `run_config_provider` text;
--> statement-breakpoint
ALTER TABLE `projects` ADD COLUMN `run_config_updated_at` text;

180 changes: 180 additions & 0 deletions src/main/ipc/githubIpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { exec } from 'child_process';
import { promisify } from 'util';
import * as path from 'path';
import * as fs from 'fs';
import { homedir } from 'os';

const execAsync = promisify(exec);
const githubService = new GitHubService();
Expand Down Expand Up @@ -333,4 +334,183 @@ export function registerGithubIpc() {
};
}
});

ipcMain.handle('github:getOwners', async () => {
try {
const owners = await githubService.getOwners();
return { success: true, owners };
} catch (error) {
log.error('Failed to get owners:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to get owners',
};
}
});

ipcMain.handle('github:validateRepoName', async (_, name: string, owner: string) => {
try {
// First validate format
const formatValidation = githubService.validateRepositoryName(name);
if (!formatValidation.valid) {
return {
success: true,
valid: false,
exists: false,
error: formatValidation.error,
};
}

// Then check if it exists
const exists = await githubService.checkRepositoryExists(owner, name);
if (exists) {
return {
success: true,
valid: true,
exists: true,
error: `Repository ${owner}/${name} already exists`,
};
}

return {
success: true,
valid: true,
exists: false,
};
} catch (error) {
log.error('Failed to validate repo name:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Validation failed',
};
}
});

ipcMain.handle(
'github:createNewProject',
async (
_,
params: {
name: string;
description?: string;
owner: string;
isPrivate: boolean;
gitignoreTemplate?: string;
}
) => {
let githubRepoCreated = false;
let localDirCreated = false;
let repoUrl: string | undefined;
let localPath: string | undefined;

try {
const { name, description, owner, isPrivate, gitignoreTemplate } = params;

// Validate inputs
const formatValidation = githubService.validateRepositoryName(name);
if (!formatValidation.valid) {
return {
success: false,
error: formatValidation.error || 'Invalid repository name',
};
}

// Check if repo already exists
const exists = await githubService.checkRepositoryExists(owner, name);
if (exists) {
return {
success: false,
error: `Repository ${owner}/${name} already exists`,
};
}

// Get project directory from settings
const { getAppSettings } = await import('../settings');
const settings = getAppSettings();
const projectDir =
settings.projects?.defaultDirectory || path.join(homedir(), 'emdash-projects');

// Ensure project directory exists
if (!fs.existsSync(projectDir)) {
fs.mkdirSync(projectDir, { recursive: true });
}

localPath = path.join(projectDir, name);
if (fs.existsSync(localPath)) {
return {
success: false,
error: `Directory ${localPath} already exists`,
};
}

// Create GitHub repository
const repoInfo = await githubService.createRepository({
name,
description,
owner,
isPrivate,
});
githubRepoCreated = true;
repoUrl = repoInfo.url;

// Clone repository
const cloneResult = await githubService.cloneRepository(repoUrl, localPath);
if (!cloneResult.success) {
// Cleanup: delete GitHub repo on clone failure
try {
await execAsync(`gh repo delete ${owner}/${name} --yes`, {
timeout: 10000,
});
} catch (cleanupError) {
log.warn('Failed to cleanup GitHub repo after clone failure:', cleanupError);
}
return {
success: false,
error: cloneResult.error || 'Failed to clone repository',
};
}
localDirCreated = true;

// Initialize project (create README, commit, push)
await githubService.initializeNewProject({
repoUrl,
localPath,
name,
description,
});

// TODO: Add .gitignore if template specified (for future enhancement)

return {
success: true,
projectPath: localPath,
repoUrl,
fullName: repoInfo.fullName,
defaultBranch: repoInfo.defaultBranch,
};
} catch (error) {
log.error('Failed to create new project:', error);

// Cleanup on failure
if (localDirCreated && localPath && fs.existsSync(localPath)) {
try {
fs.rmSync(localPath, { recursive: true, force: true });
} catch (cleanupError) {
log.warn('Failed to cleanup local directory:', cleanupError);
}
}

// Note: We don't delete the GitHub repo automatically here
// as the user might want to keep it for manual setup
// The error message will inform them

return {
success: false,
error: error instanceof Error ? error.message : 'Failed to create project',
githubRepoCreated, // Inform frontend about orphaned repo
repoUrl,
};
}
}
);
}
10 changes: 10 additions & 0 deletions src/main/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,16 @@ contextBridge.exposeInMainWorld('electronAPI', {
githubGetRepositories: () => ipcRenderer.invoke('github:getRepositories'),
githubCloneRepository: (repoUrl: string, localPath: string) =>
ipcRenderer.invoke('github:cloneRepository', repoUrl, localPath),
githubGetOwners: () => ipcRenderer.invoke('github:getOwners'),
githubValidateRepoName: (name: string, owner: string) =>
ipcRenderer.invoke('github:validateRepoName', name, owner),
githubCreateNewProject: (params: {
name: string;
description?: string;
owner: string;
isPrivate: boolean;
gitignoreTemplate?: string;
}) => ipcRenderer.invoke('github:createNewProject', params),
githubListPullRequests: (projectPath: string) =>
ipcRenderer.invoke('github:listPullRequests', { projectPath }),
githubCreatePullRequestWorktree: (args: {
Expand Down
Loading