Skip to content
4 changes: 2 additions & 2 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@
"varsIgnorePattern": "^_"
}
],
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-empty-function": "off",
"prefer-const": "warn",
"react-hooks/set-state-in-effect": "warn"
"react-hooks/set-state-in-effect": "off"
},
"overrides": [
{
Expand Down
76 changes: 49 additions & 27 deletions src/main/ipc/appIpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ export function registerAppIpc() {
}
try {
const platform = process.platform;
const quoted = (p: string) => `'${p.replace(/'/g, "'\\''")}'`;
const quotedPosix = (p: string) => `'${p.replace(/'/g, "'\\''")}'`;
const quotedWin = (p: string) => `"${p.replace(/"/g, '""')}"`;

if (which === 'warp') {
const urls = [
Expand All @@ -58,11 +59,11 @@ export function registerAppIpc() {
switch (which) {
case 'finder':
// Open directory in Finder
command = `open ${quoted(target)}`;
command = `open ${quotedPosix(target)}`;
break;
case 'cursor':
// Prefer CLI when available to ensure the folder opens in-app
command = `command -v cursor >/dev/null 2>&1 && cursor ${quoted(target)} || open -a "Cursor" ${quoted(target)}`;
command = `command -v cursor >/dev/null 2>&1 && cursor ${quotedPosix(target)} || open -a "Cursor" ${quotedPosix(target)}`;
break;
case 'vscode':
command = [
Expand All @@ -74,14 +75,14 @@ export function registerAppIpc() {
case 'terminal':
// Open Terminal app at the target directory
// This should open a new tab/window with CWD set to target
command = `open -a Terminal ${quoted(target)}`;
command = `open -a Terminal ${quotedPosix(target)}`;
break;
case 'iterm2':
// iTerm2 by bundle id, then by app name
command = [
`open -b com.googlecode.iterm2 ${quoted(target)}`,
`open -a "iTerm" ${quoted(target)}`,
`open -a "iTerm2" ${quoted(target)}`,
`open -b com.googlecode.iterm2 ${quotedPosix(target)}`,
`open -a "iTerm" ${quotedPosix(target)}`,
`open -a "iTerm2" ${quotedPosix(target)}`,
].join(' || ');
break;
case 'ghostty':
Expand All @@ -93,46 +94,47 @@ export function registerAppIpc() {
].join(' || ');
break;
case 'zed':
command = `command -v zed >/dev/null 2>&1 && zed ${quoted(target)} || open -a "Zed" ${quoted(target)}`;
command = `command -v zed >/dev/null 2>&1 && zed ${quotedPosix(target)} || open -a "Zed" ${quotedPosix(target)}`;
break;
}
} else if (platform === 'win32') {
switch (which) {
case 'finder':
command = `explorer ${quoted(target)}`;
command = `explorer ${quotedWin(target)}`;
break;
case 'cursor':
command = `start "" cursor ${quoted(target)}`;
command = `start "" cursor ${quotedWin(target)}`;
break;
case 'vscode':
command = `start "" code ${quoted(target)} || start "" code-insiders ${quoted(target)}`;
command = `start "" code ${quotedWin(target)} || start "" code-insiders ${quotedWin(target)}`;
break;
case 'terminal':
command = `wt -d ${quoted(target)} || start cmd /K "cd /d ${target}"`;
command = `wt -d ${quotedWin(target)} || start cmd /K "cd /d ${quotedWin(target)}"`;
break;
case 'ghostty':
case 'zed':
return { success: false, error: `${which} is not supported on Windows` } as any;
}
} else {
// Linux: use proper quoting for shell commands
switch (which) {
case 'finder':
command = `xdg-open ${quoted(target)}`;
command = `xdg-open ${quotedPosix(target)}`;
break;
case 'cursor':
command = `cursor ${quoted(target)}`;
command = `cursor ${quotedPosix(target)}`;
break;
case 'vscode':
command = `code ${quoted(target)} || code-insiders ${quoted(target)}`;
command = `code ${quotedPosix(target)} || code-insiders ${quotedPosix(target)}`;
break;
case 'terminal':
command = `x-terminal-emulator --working-directory=${quoted(target)} || gnome-terminal --working-directory=${quoted(target)} || konsole --workdir ${quoted(target)}`;
command = `x-terminal-emulator --working-directory=${quotedPosix(target)} || gnome-terminal --working-directory=${quotedPosix(target)} || konsole --workdir ${quotedPosix(target)}`;
break;
case 'ghostty':
command = `ghostty --working-directory=${quoted(target)} || x-terminal-emulator --working-directory=${quoted(target)}`;
break;
case 'zed':
command = `zed ${quoted(target)} || xdg-open ${quoted(target)}`;
command = `zed ${quotedPosix(target)} || xdg-open ${quotedPosix(target)}`;
break;
case 'iterm2':
return { success: false, error: 'iTerm2 is only available on macOS' } as any;
Expand Down Expand Up @@ -186,22 +188,42 @@ export function registerAppIpc() {
// App metadata
ipcMain.handle('app:getAppVersion', () => {
try {
// Try multiple possible paths for package.json
const possiblePaths = [
join(__dirname, '../../package.json'), // from dist/main/ipc
join(__dirname, '../../../package.json'), // alternative path
join(app.getAppPath(), 'package.json'), // production build
];

for (const packageJsonPath of possiblePaths) {
const readVersion = (packageJsonPath: string) => {
try {
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
if (packageJson.name === 'emdash' && packageJson.version) {
return packageJson.version;
return packageJson.version as string;
}
} catch {
continue;
// ignore parse/fs errors and continue
}
return null;
};

// Walk upward from a base directory to find package.json (helps in dev where the
// compiled code lives in dist/main/main/**).
const collectCandidates = (base: string | undefined, maxDepth = 6) => {
if (!base) return [];
const paths: string[] = [];
let current = base;
for (let i = 0; i < maxDepth; i++) {
paths.push(join(current, 'package.json'));
const parent = join(current, '..');
if (parent === current) break;
current = parent;
}
return paths;
};

const possiblePaths = [
...collectCandidates(__dirname),
...collectCandidates(app.getAppPath()),
join(process.cwd(), 'package.json'),
];

for (const packageJsonPath of possiblePaths) {
const version = readVersion(packageJsonPath);
if (version) return version;
}
return app.getVersion();
} catch {
Expand Down
15 changes: 13 additions & 2 deletions src/main/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
worktreeId: string;
worktreePath?: string;
branch?: string;
deleteRemoteBranch?: boolean;
}) => ipcRenderer.invoke('worktree:remove', args),
worktreeStatus: (args: { worktreePath: string }) => ipcRenderer.invoke('worktree:status', args),
worktreeMerge: (args: { projectPath: string; worktreeId: string }) =>
Expand Down Expand Up @@ -374,7 +375,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
// Type definitions for the exposed API
export interface ElectronAPI {
// App info
getVersion: () => Promise<string>;
getAppVersion: () => Promise<string>;
getElectronVersion: () => Promise<string>;
getPlatform: () => Promise<string>;
// Updater
checkForUpdates: () => Promise<{ success: boolean; result?: any; error?: string }>;
Expand Down Expand Up @@ -444,7 +446,16 @@ export interface ElectronAPI {
worktreeRemove: (args: {
projectPath: string;
worktreeId: string;
}) => Promise<{ success: boolean; error?: string }>;
worktreePath?: string;
branch?: string;
deleteRemoteBranch?: boolean;
}) => Promise<{
success: boolean;
error?: string;
localBranchDeleted?: boolean | null;
remoteBranchDeleted?: boolean | null;
remoteBranchDeleteError?: string;
}>;
worktreeStatus: (args: {
worktreePath: string;
}) => Promise<{ success: boolean; status?: any; error?: string }>;
Expand Down
36 changes: 31 additions & 5 deletions src/main/services/GitHubService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -592,14 +592,40 @@ export class GitHubService {
try {
const token = await this.getStoredToken();

if (!token) {
// No stored token, user needs to authenticate
if (token) {
// Test the token by making a simple API call
const user = await this.getUserInfo(token);
if (user) return true;

// Stored token is invalid; clear it and fall through to CLI auth check.
try {
await this.logout();
} catch {}
}

// No stored token (or token invalid). If the user is already logged into the GitHub CLI,
// treat it as authenticated and (best-effort) persist the token for future sessions.
try {
await execAsync('gh auth status', { encoding: 'utf8' });
} catch {
return false;
}

// Test the token by making a simple API call
const user = await this.getUserInfo(token);
return !!user;
try {
const { stdout } = await execAsync('gh auth token', { encoding: 'utf8' });
const cliToken = String(stdout || '').trim();
if (cliToken) {
try {
await this.storeToken(cliToken);
} catch (storeErr) {
console.warn('Failed to store GitHub token from gh CLI:', storeErr);
}
}
} catch {
// Token retrieval is optional; auth status already succeeded.
}

return true;
} catch (error) {
console.error('Authentication check failed:', error);
return false;
Expand Down
17 changes: 2 additions & 15 deletions src/main/services/TerminalConfigParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,10 +282,10 @@ function loadiTerm2Config(): TerminalConfig | null {
*/
function loadiTerm2ConfigXML(plistPath: string): TerminalConfig | null {
try {
const xmlContent = readFileSync(plistPath, 'utf8');
const _xmlContent = readFileSync(plistPath, 'utf8');
// Simple XML parsing for color values
// This is a basic implementation - could be improved
const colorRegex =
const _colorRegex =
/<key>([^<]+)<\/key>\s*<dict>[\s\S]*?<key>Red Component<\/key>\s*<real>([\d.]+)<\/real>[\s\S]*?<key>Green Component<\/key>\s*<real>([\d.]+)<\/real>[\s\S]*?<key>Blue Component<\/key>\s*<real>([\d.]+)<\/real>/g;
// This is complex - for now, return null and rely on JSON conversion
return null;
Expand Down Expand Up @@ -456,19 +456,6 @@ function parseAlacrittyTOML(content: string): TerminalConfig | null {
theme.cursor = cursorMatch[1];
}

// Parse ANSI colors (simplified - Alacritty uses nested structure)
const ansiColors = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'];
const brightColors = [
'bright_black',
'bright_red',
'bright_green',
'bright_yellow',
'bright_blue',
'bright_magenta',
'bright_cyan',
'bright_white',
];

const colorMap: Record<string, keyof TerminalTheme> = {
black: 'black',
red: 'red',
Expand Down
Loading