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
28 changes: 18 additions & 10 deletions packages/daemon-core/src/cli-adapters/provider-cli-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -484,17 +484,25 @@ export function findBinary(name: string): string {
return path.isAbsolute(expanded) ? expanded : path.resolve(expanded);
}
const isWin = os.platform() === 'win32';
try {
const cmd = isWin ? `where ${trimmed}` : `which ${trimmed}`;
return execSync(cmd, {
encoding: 'utf-8',
timeout: 5000,
stdio: ['pipe', 'pipe', 'pipe'],
...(isWin ? { windowsHide: true } : {}),
}).trim().split('\n')[0].trim();
} catch {
return isWin ? `${trimmed}.cmd` : trimmed;
const paths = (process.env.PATH || '').split(path.delimiter);
const exes = isWin ? ['.exe', '.cmd', '.bat', ''] : [''];

for (const p of paths) {
if (!p) continue;
for (const ext of exes) {
const fullPath = path.join(p, trimmed + ext);
try {
const fs = require('fs');
if (fs.existsSync(fullPath)) {
const stat = fs.statSync(fullPath);
if (stat.isFile() && (isWin || (stat.mode & 0o111))) {
return fullPath;
}
}
} catch { }
}
}
return isWin ? `${trimmed}.cmd` : trimmed;
}

export function isScriptBinary(binaryPath: string): boolean {
Expand Down
2 changes: 1 addition & 1 deletion packages/daemon-core/src/commands/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5101,7 +5101,7 @@ export class DaemonCommandRouter {

// 3. Kill OS process if requested
if (killProcess) {
const running = isIdeRunning(ideType);
const running = await isIdeRunning(ideType);
if (running) {
LOG.info('StopIDE', `Killing IDE process: ${ideType}`);
const killed = await killIdeProcess(ideType);
Expand Down
42 changes: 26 additions & 16 deletions packages/daemon-core/src/detection/ide-detector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
* Migrated from @adhdev/core — this is now the single source of truth.
*/

import { execSync } from 'child_process';
import { existsSync } from 'fs';
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
import { existsSync, statSync } from 'fs';
import { platform, homedir } from 'os';
import * as path from 'path';
import type { ProviderLoader } from '../providers/provider-loader.js';
Expand Down Expand Up @@ -73,25 +75,33 @@ function findCliCommand(command: string): string | null {
const resolved = path.isAbsolute(candidate) ? candidate : path.resolve(candidate);
return existsSync(resolved) ? resolved : null;
}
try {
const result = execSync(
platform() === 'win32' ? `where ${trimmed}` : `which ${trimmed}`,
{ encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }
).trim();
return result.split('\n')[0] || null;
} catch {
return null;
const isWin = platform() === 'win32';
const paths = (process.env.PATH || '').split(isWin ? ';' : ':');
const exes = isWin ? ['.exe', '.cmd', '.bat', ''] : [''];
for (const p of paths) {
if (!p) continue;
for (const ext of exes) {
const fullPath = path.join(p, trimmed + ext);
try {
if (existsSync(fullPath)) {
const stat = statSync(fullPath);
if (stat.isFile() && (isWin || (stat.mode & 0o111))) {
return fullPath;
}
}
} catch { }
}
}
return null;
}

function getIdeVersion(cliCommand: string): string | null {
async function getIdeVersion(cliCommand: string): Promise<string | null> {
try {
const result = execSync(`"${cliCommand}" --version`, {
const { stdout } = await execAsync(`"${cliCommand}" --version`, {
encoding: 'utf-8',
timeout: 10000,
stdio: ['pipe', 'pipe', 'pipe'],
}).trim();
return result.split('\n')[0] || null;
});
return stdout.trim().split('\n')[0] || null;
} catch {
return null;
}
Expand Down Expand Up @@ -152,7 +162,7 @@ export async function detectIDEs(providerLoader?: ProviderLoader): Promise<IDEIn
const installed = os === 'darwin'
? !!(resolvedCli || appPath)
: !!resolvedCli;
const version = resolvedCli ? getIdeVersion(resolvedCli) : null;
const version = resolvedCli ? await getIdeVersion(resolvedCli) : null;

results.push({
id: def.id,
Expand Down
2 changes: 1 addition & 1 deletion packages/daemon-core/src/installer.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export interface InstallResult {
/**
* Check if an extension is already installed
*/
export declare function isExtensionInstalled(ide: IDEInfo, marketplaceId: string): boolean;
export declare function isExtensionInstalled(ide: IDEInfo, marketplaceId: string): Promise<boolean>;
/**
* Install a single extension
*/
Expand Down
14 changes: 8 additions & 6 deletions packages/daemon-core/src/installer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,20 +122,22 @@ export interface InstallResult {
/**
* Check if an extension is already installed
*/
export function isExtensionInstalled(
import { promisify } from 'util';
const execAsync = promisify(exec);

export async function isExtensionInstalled(
ide: IDEInfo,
marketplaceId: string
): boolean {
): Promise<boolean> {
if (!ide.cliCommand) return false;

try {
const result = execSync(`"${ide.cliCommand}" --list-extensions`, {
const { stdout } = await execAsync(`"${ide.cliCommand}" --list-extensions`, {
encoding: 'utf-8',
timeout: 15000,
stdio: ['pipe', 'pipe', 'pipe'],
});

const installed = result
const installed = stdout
.trim()
.split('\n')
.map((e) => e.trim().toLowerCase());
Expand Down Expand Up @@ -163,7 +165,7 @@ export async function installExtension(
}

// Check if already installed
const alreadyInstalled = isExtensionInstalled(ide, extension.marketplaceId);
const alreadyInstalled = await isExtensionInstalled(ide, extension.marketplaceId);
if (alreadyInstalled) {
return {
extensionId: extension.id,
Expand Down
2 changes: 1 addition & 1 deletion packages/daemon-core/src/launch.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
/** Kill IDE process (graceful → force) */
export declare function killIdeProcess(ideId: string): Promise<boolean>;
/** Check if IDE process is running */
export declare function isIdeRunning(ideId: string): boolean;
export declare function isIdeRunning(ideId: string): Promise<boolean>;
export interface LaunchOptions {
ideId?: string;
workspace?: string;
Expand Down
65 changes: 37 additions & 28 deletions packages/daemon-core/src/launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,16 @@
* adhdev launch --workspace /path — Open specific workspace
*/

import { execSync, spawn, spawnSync } from 'child_process';
import { exec, spawn, spawnSync } from 'child_process';

async function execQuiet(command: string, options: any = {}): Promise<string> {
return new Promise((resolve) => {
exec(command, options, (error, stdout) => {
if (error) return resolve('');
resolve(stdout.toString());
});
});
}
import * as net from 'net';
import * as os from 'os';
import * as path from 'path';
Expand Down Expand Up @@ -76,11 +85,11 @@ function getIdePathCandidates(ideId: string): string[] {
return getProviderLoader().getIdePathCandidates(ideId);
}

function getMacAppProcessPids(ideId: string): number[] {
async function getMacAppProcessPids(ideId: string): Promise<number[]> {
const appPaths = getIdePathCandidates(ideId);
if (appPaths.length === 0) return [];
try {
const output = execSync('ps axww -o pid=,args=', {
const output = await execQuiet('ps axww -o pid=,args=', {
encoding: 'utf-8',
timeout: 3000,
stdio: ['pipe', 'pipe', 'pipe'],
Expand All @@ -91,8 +100,8 @@ function getMacAppProcessPids(ideId: string): number[] {
}
}

function killMacAppPathProcesses(ideId: string, signal: NodeJS.Signals): boolean {
const pids = getMacAppProcessPids(ideId);
async function killMacAppPathProcesses(ideId: string, signal: NodeJS.Signals): Promise<boolean> {
const pids = (await getMacAppProcessPids(ideId));
let signalled = false;
for (const pid of pids) {
try {
Expand Down Expand Up @@ -163,73 +172,73 @@ export async function killIdeProcess(ideId: string): Promise<boolean> {
if (plat === 'darwin' && appName) {
// macOS: graceful quit via osascript
try {
execSync(`osascript -e 'tell application "${escapeForAppleScript(appName)}" to quit' 2>/dev/null`, {
await execQuiet(`osascript -e 'tell application "${escapeForAppleScript(appName)}" to quit' 2>/dev/null`, {
timeout: 5000,
});
} catch {
try { execSync(`pkill -x "${appName}" 2>/dev/null`, { timeout: 5000 }); } catch { }
try { await execQuiet(`pkill -x "${appName}" 2>/dev/null`, { timeout: 5000 }); } catch { }
}
killMacAppPathProcesses(ideId, 'SIGTERM');
await killMacAppPathProcesses(ideId, 'SIGTERM');
} else if (plat === 'win32' && winProcesses) {
// Windows: taskkill for each process name
for (const proc of winProcesses) {
try {
execSync(`taskkill /IM "${proc}" /F 2>nul`, { timeout: 5000 });
await execQuiet(`taskkill /IM "${proc}" /F 2>nul`, { timeout: 5000 });
} catch { }
}
// Process name may differ, so also try via WMIC
try {
const exeName = winProcesses[0].replace('.exe', '');
execSync(`powershell -Command "Get-Process -Name '${exeName}' -ErrorAction SilentlyContinue | Stop-Process -Force"`, {
await execQuiet(`powershell -Command "Get-Process -Name '${exeName}' -ErrorAction SilentlyContinue | Stop-Process -Force"`, {
timeout: 10000,
});
} catch { }
} else {
try { execSync(`pkill -f "${ideId}" 2>/dev/null`); } catch { }
try { await execQuiet(`pkill -f "${ideId}" 2>/dev/null`); } catch { }
}

// Wait for process kill (max 15 seconds)
for (let i = 0; i < 30; i++) {
await new Promise(r => setTimeout(r, 500));
if (!isIdeRunning(ideId)) return true;
if (!(await isIdeRunning(ideId))) return true;
}

// Force terminate retry
if (plat === 'darwin' && appName) {
try { execSync(`pkill -9 -x "${appName}" 2>/dev/null`, { timeout: 5000 }); } catch { }
killMacAppPathProcesses(ideId, 'SIGKILL');
try { await execQuiet(`pkill -9 -x "${appName}" 2>/dev/null`, { timeout: 5000 }); } catch { }
await killMacAppPathProcesses(ideId, 'SIGKILL');
} else if (plat === 'win32' && winProcesses) {
for (const proc of winProcesses) {
try { execSync(`taskkill /IM "${proc}" /F 2>nul`); } catch { }
try { await execQuiet(`taskkill /IM "${proc}" /F 2>nul`); } catch { }
}
}

await new Promise(r => setTimeout(r, 2000));
return !isIdeRunning(ideId);
return !(await isIdeRunning(ideId));

} catch {
return false;
}
}

/** Check if IDE process is running */
export function isIdeRunning(ideId: string): boolean {
export async function isIdeRunning(ideId: string): Promise<boolean> {
const plat = os.platform();

try {
if (plat === 'darwin') {
const appName = getMacAppIdentifiers()[ideId];
if (!appName) return getMacAppProcessPids(ideId).length > 0;
if (!appName) return (await getMacAppProcessPids(ideId)).length > 0;
try {
const result = execSync(`pgrep -x "${appName}" 2>/dev/null`, {
const result = await execQuiet(`pgrep -x "${appName}" 2>/dev/null`, {
encoding: 'utf-8',
timeout: 3000,
});
if (result.trim().length > 0) return true;
} catch { }

try {
const result = execSync(
const result = await execQuiet(
`osascript -e 'tell application "System Events" to count (every process whose name is "${escapeForAppleScript(appName)}")'`,
{
encoding: 'utf-8',
Expand All @@ -240,29 +249,29 @@ export function isIdeRunning(ideId: string): boolean {
if (Number.parseInt(result.trim() || '0', 10) > 0) return true;
} catch { }

return getMacAppProcessPids(ideId).length > 0;
return (await getMacAppProcessPids(ideId)).length > 0;
} else if (plat === 'win32') {
const winProcesses = getWinProcessNames()[ideId];
if (!winProcesses) return false;
// Check each process name
for (const proc of winProcesses) {
try {
const result = execSync(`tasklist /FI "IMAGENAME eq ${proc}" /NH 2>nul`, { encoding: 'utf-8' });
const result = await execQuiet(`tasklist /FI "IMAGENAME eq ${proc}" /NH 2>nul`, { encoding: 'utf-8' });
if (result.includes(proc)) return true;
} catch { }
}
// Also check via PowerShell (when tasklist cannot find)
try {
const exeName = winProcesses[0].replace('.exe', '');
const result = execSync(
const result = await execQuiet(
`powershell -Command "(Get-Process -Name '${exeName}' -ErrorAction SilentlyContinue).Count"`,
{ encoding: 'utf-8', timeout: 5000 }
);
return parseInt(result.trim()) > 0;
} catch { }
return false;
} else {
const result = execSync(`pgrep -f "${ideId}" 2>/dev/null`, { encoding: 'utf-8' });
const result = await execQuiet(`pgrep -f "${ideId}" 2>/dev/null`, { encoding: 'utf-8' });
return result.trim().length > 0;
}
} catch {
Expand All @@ -271,14 +280,14 @@ export function isIdeRunning(ideId: string): boolean {
}

/** Detect currently open workspace path */
function detectCurrentWorkspace(ideId: string): string | undefined {
async function detectCurrentWorkspace(ideId: string): Promise<string | undefined> {
const plat = os.platform();

if (plat === 'darwin') {
try {
const appName = getMacAppIdentifiers()[ideId];
if (!appName) return undefined;
const result = execSync(
const result = await execQuiet(
`lsof -c "${appName}" 2>/dev/null | grep cwd | head -1 | awk '{print $NF}'`,
{ encoding: 'utf-8', timeout: 3000 }
);
Expand Down Expand Up @@ -392,8 +401,8 @@ export async function launchWithCdp(options: LaunchOptions = {}): Promise<Launch
}

// 4. Check if IDE is currently running
const alreadyRunning = isIdeRunning(targetIde.id);
const workspace = options.workspace || (alreadyRunning ? detectCurrentWorkspace(targetIde.id) : undefined);
const alreadyRunning = await isIdeRunning(targetIde.id);
const workspace = options.workspace || (alreadyRunning ? await detectCurrentWorkspace(targetIde.id) : undefined);

// 5. If IDE is running, terminate it
if (alreadyRunning) {
Expand Down
Loading