mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 00:21:21 +00:00
341 lines
9.9 KiB
TypeScript
341 lines
9.9 KiB
TypeScript
/**
|
|
* Windows Diagnostics Collector
|
|
*
|
|
* Collects Windows-specific diagnostic information for troubleshooting
|
|
* agent detection and process spawning issues on Windows platforms.
|
|
*
|
|
* This collector is only active on Windows (process.platform === 'win32').
|
|
*/
|
|
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import * as fs from 'fs';
|
|
import { execFileNoThrow } from '../../utils/execFile';
|
|
import { sanitizePath } from './settings';
|
|
|
|
export interface WindowsDiagnosticsInfo {
|
|
isWindows: boolean;
|
|
// Only populated on Windows
|
|
environment?: {
|
|
pathext: string[]; // PATHEXT extensions (what Windows considers executable)
|
|
pathDirs: string[]; // Sanitized PATH directories
|
|
pathDirsCount: number;
|
|
appData: string; // Sanitized APPDATA path
|
|
localAppData: string; // Sanitized LOCALAPPDATA path
|
|
programFiles: string; // Sanitized Program Files path
|
|
userProfile: string; // Sanitized user profile (home) path
|
|
};
|
|
agentProbing?: {
|
|
// For each agent, show what paths were probed and what was found
|
|
claude: AgentProbeResult;
|
|
codex: AgentProbeResult;
|
|
opencode: AgentProbeResult;
|
|
gemini: AgentProbeResult;
|
|
droid: AgentProbeResult;
|
|
};
|
|
whereResults?: {
|
|
// Results from 'where' command for each agent
|
|
claude: WhereResult;
|
|
codex: WhereResult;
|
|
opencode: WhereResult;
|
|
gemini: WhereResult;
|
|
droid: WhereResult;
|
|
};
|
|
npmInfo?: {
|
|
npmGlobalPrefix: string | null; // npm config get prefix (sanitized)
|
|
npmVersion: string | null;
|
|
nodeVersion: string | null;
|
|
};
|
|
fileSystemChecks?: {
|
|
// Check if common installation directories exist
|
|
npmGlobalDir: DirectoryCheck;
|
|
localBinDir: DirectoryCheck;
|
|
wingetLinksDir: DirectoryCheck;
|
|
scoopShimsDir: DirectoryCheck;
|
|
chocolateyBinDir: DirectoryCheck;
|
|
pythonScriptsDir: DirectoryCheck;
|
|
};
|
|
}
|
|
|
|
export interface AgentProbeResult {
|
|
probedPaths: Array<{
|
|
path: string; // Sanitized path
|
|
exists: boolean;
|
|
isFile: boolean;
|
|
extension: string;
|
|
}>;
|
|
foundPath: string | null; // First path that was found (sanitized)
|
|
}
|
|
|
|
export interface WhereResult {
|
|
success: boolean;
|
|
exitCode: number | string; // Number for exit codes, string for system errors (ENOENT, etc.)
|
|
paths: string[]; // Sanitized paths returned by where
|
|
error?: string;
|
|
}
|
|
|
|
export interface DirectoryCheck {
|
|
path: string; // Sanitized path
|
|
exists: boolean;
|
|
isDirectory: boolean;
|
|
files?: string[]; // List of executables found (if exists)
|
|
}
|
|
|
|
/**
|
|
* Collect Windows-specific diagnostics.
|
|
* Returns minimal info on non-Windows platforms.
|
|
*/
|
|
export async function collectWindowsDiagnostics(): Promise<WindowsDiagnosticsInfo> {
|
|
const isWindows = process.platform === 'win32';
|
|
|
|
if (!isWindows) {
|
|
return { isWindows: false };
|
|
}
|
|
|
|
const result: WindowsDiagnosticsInfo = {
|
|
isWindows: true,
|
|
};
|
|
|
|
// Collect environment info
|
|
result.environment = collectEnvironmentInfo();
|
|
|
|
// Probe for agent binaries
|
|
result.agentProbing = {
|
|
claude: await probeAgentPaths('claude'),
|
|
codex: await probeAgentPaths('codex'),
|
|
opencode: await probeAgentPaths('opencode'),
|
|
gemini: await probeAgentPaths('gemini'),
|
|
droid: await probeAgentPaths('droid'),
|
|
};
|
|
|
|
// Run 'where' command for each agent
|
|
result.whereResults = {
|
|
claude: await runWhereCommand('claude'),
|
|
codex: await runWhereCommand('codex'),
|
|
opencode: await runWhereCommand('opencode'),
|
|
gemini: await runWhereCommand('gemini'),
|
|
droid: await runWhereCommand('droid'),
|
|
};
|
|
|
|
// Collect npm info
|
|
result.npmInfo = await collectNpmInfo();
|
|
|
|
// Check common installation directories
|
|
result.fileSystemChecks = await checkInstallationDirectories();
|
|
|
|
return result;
|
|
}
|
|
|
|
function collectEnvironmentInfo(): WindowsDiagnosticsInfo['environment'] {
|
|
const home = os.homedir();
|
|
const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
|
const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
const programFiles = process.env.ProgramFiles || 'C:\\Program Files';
|
|
const pathext = (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean);
|
|
const pathDirs = (process.env.PATH || '').split(path.delimiter).filter(Boolean);
|
|
|
|
return {
|
|
pathext,
|
|
pathDirs: pathDirs.map((p) => sanitizePath(p)),
|
|
pathDirsCount: pathDirs.length,
|
|
appData: sanitizePath(appData),
|
|
localAppData: sanitizePath(localAppData),
|
|
programFiles: sanitizePath(programFiles),
|
|
userProfile: sanitizePath(home),
|
|
};
|
|
}
|
|
|
|
async function probeAgentPaths(binaryName: string): Promise<AgentProbeResult> {
|
|
const home = os.homedir();
|
|
const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
|
const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
const programFiles = process.env.ProgramFiles || 'C:\\Program Files';
|
|
const chocolateyInstall = process.env.ChocolateyInstall || 'C:\\ProgramData\\chocolatey';
|
|
|
|
// Known installation paths for each agent
|
|
const knownPaths: Record<string, string[]> = {
|
|
claude: [
|
|
// PowerShell installer location
|
|
path.join(home, '.local', 'bin', 'claude.exe'),
|
|
// winget locations
|
|
path.join(localAppData, 'Microsoft', 'WinGet', 'Links', 'claude.exe'),
|
|
path.join(programFiles, 'WinGet', 'Links', 'claude.exe'),
|
|
// npm global locations
|
|
path.join(appData, 'npm', 'claude.cmd'),
|
|
path.join(localAppData, 'npm', 'claude.cmd'),
|
|
// Windows Apps (Microsoft Store / App Installer)
|
|
path.join(localAppData, 'Microsoft', 'WindowsApps', 'claude.exe'),
|
|
],
|
|
codex: [
|
|
path.join(appData, 'npm', 'codex.cmd'),
|
|
path.join(localAppData, 'npm', 'codex.cmd'),
|
|
path.join(home, '.local', 'bin', 'codex.exe'),
|
|
],
|
|
opencode: [
|
|
path.join(home, 'scoop', 'shims', 'opencode.exe'),
|
|
path.join(home, 'scoop', 'apps', 'opencode', 'current', 'opencode.exe'),
|
|
path.join(chocolateyInstall, 'bin', 'opencode.exe'),
|
|
path.join(home, 'go', 'bin', 'opencode.exe'),
|
|
path.join(appData, 'npm', 'opencode.cmd'),
|
|
],
|
|
gemini: [path.join(appData, 'npm', 'gemini.cmd'), path.join(localAppData, 'npm', 'gemini.cmd')],
|
|
droid: [
|
|
path.join(home, '.factory', 'bin', 'droid.exe'),
|
|
path.join(localAppData, 'Factory', 'droid.exe'),
|
|
path.join(appData, 'Factory', 'droid.exe'),
|
|
path.join(home, '.local', 'bin', 'droid.exe'),
|
|
path.join(appData, 'npm', 'droid.cmd'),
|
|
path.join(localAppData, 'npm', 'droid.cmd'),
|
|
],
|
|
};
|
|
|
|
const pathsToProbe = knownPaths[binaryName] || [];
|
|
const probedPaths: AgentProbeResult['probedPaths'] = [];
|
|
let foundPath: string | null = null;
|
|
|
|
for (const probePath of pathsToProbe) {
|
|
let exists = false;
|
|
let isFile = false;
|
|
|
|
try {
|
|
const stats = fs.statSync(probePath);
|
|
exists = true;
|
|
isFile = stats.isFile();
|
|
if (isFile && !foundPath) {
|
|
foundPath = probePath;
|
|
}
|
|
} catch {
|
|
// Path doesn't exist
|
|
}
|
|
|
|
probedPaths.push({
|
|
path: sanitizePath(probePath),
|
|
exists,
|
|
isFile,
|
|
extension: path.extname(probePath).toLowerCase(),
|
|
});
|
|
}
|
|
|
|
return {
|
|
probedPaths,
|
|
foundPath: foundPath ? sanitizePath(foundPath) : null,
|
|
};
|
|
}
|
|
|
|
async function runWhereCommand(binaryName: string): Promise<WhereResult> {
|
|
try {
|
|
const result = await execFileNoThrow('where', [binaryName]);
|
|
const paths = result.stdout
|
|
.split(/\r?\n/)
|
|
.map((p) => p.trim())
|
|
.filter(Boolean)
|
|
.map((p) => sanitizePath(p));
|
|
|
|
return {
|
|
success: result.exitCode === 0,
|
|
exitCode: result.exitCode,
|
|
paths,
|
|
error: result.exitCode !== 0 ? result.stderr : undefined,
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
exitCode: -1,
|
|
paths: [],
|
|
error: error instanceof Error ? error.message : String(error),
|
|
};
|
|
}
|
|
}
|
|
|
|
async function collectNpmInfo(): Promise<WindowsDiagnosticsInfo['npmInfo']> {
|
|
let npmGlobalPrefix: string | null = null;
|
|
let npmVersion: string | null = null;
|
|
let nodeVersion: string | null = null;
|
|
|
|
try {
|
|
const prefixResult = await execFileNoThrow('npm', ['config', 'get', 'prefix']);
|
|
if (prefixResult.exitCode === 0) {
|
|
npmGlobalPrefix = sanitizePath(prefixResult.stdout.trim());
|
|
}
|
|
} catch {
|
|
// npm not available
|
|
}
|
|
|
|
try {
|
|
const versionResult = await execFileNoThrow('npm', ['--version']);
|
|
if (versionResult.exitCode === 0) {
|
|
npmVersion = versionResult.stdout.trim();
|
|
}
|
|
} catch {
|
|
// npm not available
|
|
}
|
|
|
|
try {
|
|
const nodeResult = await execFileNoThrow('node', ['--version']);
|
|
if (nodeResult.exitCode === 0) {
|
|
nodeVersion = nodeResult.stdout.trim();
|
|
}
|
|
} catch {
|
|
// node not available
|
|
}
|
|
|
|
return {
|
|
npmGlobalPrefix,
|
|
npmVersion,
|
|
nodeVersion,
|
|
};
|
|
}
|
|
|
|
async function checkInstallationDirectories(): Promise<WindowsDiagnosticsInfo['fileSystemChecks']> {
|
|
const home = os.homedir();
|
|
const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
|
const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
const chocolateyInstall = process.env.ChocolateyInstall || 'C:\\ProgramData\\chocolatey';
|
|
|
|
const dirsToCheck: Record<string, string> = {
|
|
npmGlobalDir: path.join(appData, 'npm'),
|
|
localBinDir: path.join(home, '.local', 'bin'),
|
|
wingetLinksDir: path.join(localAppData, 'Microsoft', 'WinGet', 'Links'),
|
|
scoopShimsDir: path.join(home, 'scoop', 'shims'),
|
|
chocolateyBinDir: path.join(chocolateyInstall, 'bin'),
|
|
pythonScriptsDir: path.join(appData, 'Python', 'Scripts'),
|
|
};
|
|
|
|
const result: Record<string, DirectoryCheck> = {};
|
|
|
|
for (const [key, dirPath] of Object.entries(dirsToCheck)) {
|
|
const check: DirectoryCheck = {
|
|
path: sanitizePath(dirPath),
|
|
exists: false,
|
|
isDirectory: false,
|
|
};
|
|
|
|
try {
|
|
const stats = fs.statSync(dirPath);
|
|
check.exists = true;
|
|
check.isDirectory = stats.isDirectory();
|
|
|
|
if (check.isDirectory) {
|
|
// List executable files in the directory
|
|
try {
|
|
const files = fs.readdirSync(dirPath);
|
|
const executables = files.filter((f) => {
|
|
const ext = path.extname(f).toLowerCase();
|
|
return ['.exe', '.cmd', '.bat', '.com'].includes(ext);
|
|
});
|
|
// Only include first 20 executables to avoid huge output
|
|
check.files = executables.slice(0, 20);
|
|
} catch {
|
|
// Can't read directory contents
|
|
}
|
|
}
|
|
} catch {
|
|
// Directory doesn't exist
|
|
}
|
|
|
|
result[key] = check;
|
|
}
|
|
|
|
return result as WindowsDiagnosticsInfo['fileSystemChecks'];
|
|
}
|