openmate
Version:
OpenMate: A fast CLI tool to open local repos or entire collections in VS Code, Windsurf, or Cursor using simple shortcuts for an efficient developer workflow.
431 lines (382 loc) • 12.3 kB
JavaScript
const { app, BrowserWindow, ipcMain, dialog } = require("electron");
const path = require("node:path");
const os = require("os");
const fs = require("fs");
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (require("electron-squirrel-startup")) {
app.quit();
}
const readReposFile = () => {
const STORE_DIR = path.join(os.homedir(), ".openmate");
const STORE_FILE = path.join(STORE_DIR, "repos.json");
// Default structure for the repos file
const defaultReposData = {
version: 2,
repos: {},
collections: {},
};
try {
// Create .openmate directory if it doesn't exist
if (!fs.existsSync(STORE_DIR)) {
fs.mkdirSync(STORE_DIR, { recursive: true });
}
// Create default repos.json if it doesn't exist
if (!fs.existsSync(STORE_FILE)) {
fs.writeFileSync(
STORE_FILE,
JSON.stringify(defaultReposData, null, 2),
"utf8"
);
return defaultReposData;
}
// Read and return existing data
const data = fs.readFileSync(STORE_FILE, "utf8");
return JSON.parse(data);
} catch (error) {
console.error("Error reading repos.json:", error);
return null;
}
};
// Function to open a path using openmate cli
function openIDE(name, ide, ideName, path){
return new Promise((resolve, reject) => {
const { exec } = require("child_process");
exec(`om ${ide} "${name}"`, (error) => {
if (error) {
reject(new Error(`Failed to open ${name} in ${ideName}: ${error.message}`));
} else {
resolve();
}
});
});
}
// Function to open a path in the default application
function openInDefaultApplication(path) {
const { shell } = require("electron");
return shell.openPath(path);
}
// Function to open in VS Code
function openInVSCode(name, path) {
return new Promise((resolve, reject) => {
const { exec } = require("child_process");
exec(`code "${path}"`, (error) => {
if (error) {
reject(new Error(`Failed to open in VS Code: ${error.message}`));
} else {
resolve();
}
});
});
}
// Function to open in Windsurf
function openInWindsurf(path) {
return new Promise((resolve, reject) => {
const { exec } = require("child_process");
exec(`windsurf "${path}"`, (error) => {
if (error) {
reject(new Error(`Failed to open in Windsurf: ${error.message}`));
} else {
resolve();
}
});
});
}
// Function to open in Cursor
function openInCursor(path) {
return new Promise((resolve, reject) => {
const { exec } = require("child_process");
exec(`cursor "${path}"`, (error) => {
if (error) {
reject(new Error(`Failed to open in Cursor: ${error.message}`));
} else {
resolve();
}
});
});
}
// Function to open in IntelliJ
function openInIntelliJ(path) {
return new Promise((resolve, reject) => {
const { exec } = require("child_process");
exec(`idea "${path}"`, (error) => {
if (error) {
reject(new Error(`Failed to open in IntelliJ: ${error.message}`));
} else {
resolve();
}
});
});
}
// Function to open in PyCharm
function openInPyCharm(path) {
return new Promise((resolve, reject) => {
const { exec } = require("child_process");
exec(`pycharm "${path}"`, (error) => {
if (error) {
reject(new Error(`Failed to open in PyCharm: ${error.message}`));
} else {
resolve();
}
});
});
}
// Helper function to write repos data to file
const writeReposFile = (data) => {
const STORE_DIR = path.join(os.homedir(), ".openmate");
const STORE_FILE = path.join(STORE_DIR, "repos.json");
try {
fs.writeFileSync(STORE_FILE, JSON.stringify(data, null, 2), "utf8");
return true;
} catch (error) {
console.error("Error writing repos.json:", error);
return false;
}
};
// Expose writeReposFile to renderer
ipcMain.handle("writeReposFile", async (event, data) => {
return writeReposFile(data);
});
// Expose repos data to renderer
ipcMain.handle("getReposData", async () => {
return readReposFile();
});
const createWindow = () => {
// Set up IPC handler for opening in IDE
ipcMain.handle("open-in-ide", async (event, { name, path, ide }) => {
try {
switch (ide) {
case "vscode":
// await openInVSCode(name, path);
await openIDE(name, "vs", "VS Code", path);
break;
case "windsurf":
// await openInWindsurf(name, path);
await openIDE(name, "ws", "Windsurf", path);
break;
case "cursor":
// await openInCursor(name, path);
await openIDE(name, "cs", "Cursor", path);
break;
case "intellij":
// await openInIntelliJ(name, path);
await openIDE(name, "ij", "IntelliJ", path);
break;
case "pycharm":
// await openInPyCharm(name, path);
await openIDE(name, "pc", "PyCharm", path);
break;
default:
// await openInDefaultApplication(name, path);
await openIDE(name, "default", "Default", path);
}
return { success: true };
} catch (error) {
console.error(`Error opening ${path} in ${ide}:`, error);
return { success: false, error: error.message };
}
});
// Create the browser window.
// Get the absolute path to the icon
// const iconPath = path.join(app.getAppPath(), 'src/assets/logo.png');
let iconPath;
if (process.platform === "win32") {
iconPath = path.join(app.getAppPath(), "assets/logo.ico");
} else if (process.platform === "darwin") {
iconPath = path.join(app.getAppPath(), "assets/logo.icns");
} else {
iconPath = path.join(app.getAppPath(), "assets/logo.png");
}
const mainWindow = new BrowserWindow({
width: 1000,
height: 700,
icon: iconPath,
frame: true, // Remove the default menu bar and frame
autoHideMenuBar: true, // Hide the menu bar
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,
},
});
// and load the index.html of the app.
mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);
// Read and send repos data to renderer
const reposData = readReposFile();
if (reposData) {
mainWindow.webContents.on("did-finish-load", () => {
mainWindow.webContents.send("repos-data", {
repos: Object.entries(reposData.repos || {}).map(([name, data]) => ({
name,
path: data.path,
updatedAt: data.updatedAt,
})),
collections: Object.entries(reposData.collections || {}).map(
([name, data]) => ({
name,
repos: data.repos.join(", "),
updatedAt: data.updatedAt,
})
),
});
});
}
// Open DevTools in development
if (process.env.NODE_ENV === "development") {
mainWindow.webContents.openDevTools();
}
};
// Handle repository deletion
ipcMain.handle("delete-repo", async (event, repoName) => {
try {
const data = readReposFile();
if (data && data.repos && data.repos[repoName]) {
delete data.repos[repoName];
if (writeReposFile(data)) {
// Notify all windows about the update
BrowserWindow.getAllWindows().forEach((window) => {
window.webContents.send("repos-data", {
repos: Object.entries(data.repos || {}).map(([name, repo]) => ({
name,
path: repo.path,
updatedAt: repo.updatedAt,
})),
collections: Object.entries(data.collections || {}).map(
([name, collection]) => ({
name,
repos: collection.repos.join(", "),
updatedAt: collection.updatedAt,
})
),
});
});
return { success: true };
}
}
return {
success: false,
error: "Repository not found or could not be deleted",
};
} catch (error) {
console.error("Error deleting repository:", error);
return { success: false, error: error.message };
}
});
// Helper function to delete a collection
const deleteCollection = (collectionName) => {
try {
// Read current data
const data = readReposFile();
if (!data) {
console.error("❌ Failed to read repos file or file is empty");
return { success: false, error: "Failed to read repositories data" };
}
if (!data.collections) {
console.error("❌ No collections object found in data");
return { success: false, error: "No collections data found" };
}
// Check if collection exists
if (!data.collections[collectionName]) {
console.error(`❌ Collection not found: ${collectionName}`);
return { success: false, error: "Collection not found" };
}
// Delete the collection
delete data.collections[collectionName];
// Write changes to file
if (!writeReposFile(data)) {
console.error("❌ Failed to write to repos.json");
return { success: false, error: "Failed to save changes" };
}
// Get updated data
const updatedData = readReposFile();
if (!updatedData) {
console.error("❌ Failed to read updated data");
return { success: false, error: "Failed to verify deletion" };
}
// Prepare data for renderer
const responseData = {
repos: Object.entries(updatedData.repos || {}).map(([name, repo]) => ({
name,
path: repo.path,
updatedAt: repo.updatedAt,
})),
collections: Object.entries(updatedData.collections || {}).map(
([name, collection]) => ({
name,
repos: Array.isArray(collection.repos)
? collection.repos.join(", ")
: collection.repos,
updatedAt: collection.updatedAt,
})
),
};
// Notify all windows
const windows = BrowserWindow.getAllWindows();
windows.forEach((window, index) => {
window.webContents.send("repos-data", responseData);
});
return { success: true };
} catch (error) {
console.error("❌ ERROR in deleteCollection:", error);
console.error("Stack trace:", error.stack);
return { success: false, error: error.message };
}
};
// Handle collection deletion via IPC
ipcMain.handle("delete-collection", async (event, collectionName) => {
return deleteCollection(collectionName);
});
// Handle adding a new repository
ipcMain.handle("addRepository", async (event, { name, path }) => {
try {
const data = readReposFile();
// Check if repo with this name already exists
if (data.repos[name]) {
throw new Error("A repository with this name already exists");
}
// Add the new repository
data.repos[name] = {
path: path,
updatedAt: new Date().toISOString(),
};
// Save the updated data
await writeReposFile(data);
return { success: true };
} catch (error) {
console.error("Error adding repository:", error);
throw error;
}
});
// Handle opening directory dialog
ipcMain.handle("openDirectoryDialog", async () => {
const result = await dialog.showOpenDialog({
properties: ["openDirectory"],
title: "Select Repository Directory",
});
if (!result.canceled && result.filePaths.length > 0) {
return result.filePaths[0];
}
return null;
});
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
createWindow();
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and import them here.