macDiagnostics.ts•5.97 kB
import { CommandRunner, type CommandResult } from "../utils/commandRunner.js";
import { SshService, type SshExecutionOptions } from "./ssh.js";
export type DiagnosticSuite =
| "hardware"
| "performance"
| "security"
| "network"
| "storage";
export type RepairAction =
| "disk-verify"
| "disk-repair"
| "reset-spotlight"
| "flush-cache"
| "software-update"
| "rebuild-permissions";
export interface DiagnosticsResult {
readonly label: string;
readonly command: string;
readonly stdout: string;
readonly stderr: string;
readonly exitCode: number | null;
}
export interface RemoteMacOptions extends SshExecutionOptions {
readonly host: string;
readonly username: string;
}
const SUITE_COMMANDS: Record<DiagnosticSuite, Array<{ label: string; command: string; requiresSudo?: boolean }>> = {
hardware: [
{ label: "Hardware profile", command: "system_profiler SPHardwareDataType" },
{ label: "Power profile", command: "system_profiler SPPowerDataType" },
{ label: "Thermal sensors", command: "pmset -g thermlog" },
],
performance: [
{ label: "CPU / Power metrics", command: "sudo powermetrics --show-process-energy -n 1", requiresSudo: true },
{ label: "Top processes", command: "top -l 1 -n 20" },
{ label: "Disk usage spikes", command: "sudo fs_usage -w -t 5", requiresSudo: true },
],
security: [
{ label: "FileVault", command: "fdesetup status" },
{ label: "Gatekeeper", command: "spctl --status" },
{
label: "Application firewall",
command: "defaults read /Library/Preferences/com.apple.alf globalstate",
requiresSudo: true,
},
{
label: "Recent security logs",
command: "log show --last 1h --predicate 'subsystem CONTAINS \"com.apple.security\"' --info",
requiresSudo: true,
},
],
network: [
{ label: "Interface status", command: "ifconfig" },
{ label: "Active sockets", command: "netstat -an" },
{ label: "Proxy settings", command: "scutil --proxy" },
{
label: "Network logs",
command: "log show --last 30m --predicate 'subsystem == \"com.apple.network\"'",
requiresSudo: true,
},
],
storage: [
{ label: "Disk usage", command: "df -h" },
{ label: "APFS list", command: "diskutil apfs list" },
{ label: "Volume verify", command: "sudo diskutil verifyVolume /", requiresSudo: true },
],
};
const REPAIR_COMMANDS: Record<RepairAction, { label: string; command: string; requiresSudo?: boolean }> = {
"disk-verify": { label: "Verify disk", command: "diskutil verifyVolume /" },
"disk-repair": { label: "Repair disk", command: "diskutil repairVolume /", requiresSudo: true },
"reset-spotlight": { label: "Reset Spotlight", command: "mdutil -E /", requiresSudo: true },
"flush-cache": {
label: "Flush DNS & directory cache",
command: "dscacheutil -flushcache && killall -HUP mDNSResponder",
requiresSudo: true,
},
"software-update": {
label: "Software Update",
command: "softwareupdate --install --all",
requiresSudo: true,
},
"rebuild-permissions": {
label: "Reset user permissions",
command: "diskutil resetUserPermissions / `id -u`",
requiresSudo: true,
},
};
export class MacDiagnosticsService {
public constructor(private readonly runner: CommandRunner, private readonly ssh: SshService) {}
public listSuites(): DiagnosticSuite[] {
return Object.keys(SUITE_COMMANDS) as DiagnosticSuite[];
}
public listRepairs(): RepairAction[] {
return Object.keys(REPAIR_COMMANDS) as RepairAction[];
}
public async runLocalDiagnostics(suite: DiagnosticSuite): Promise<DiagnosticsResult[]> {
const commands = SUITE_COMMANDS[suite];
return this.runLocalCommands(commands);
}
public async runRemoteDiagnostics(options: RemoteMacOptions & { suite: DiagnosticSuite }): Promise<DiagnosticsResult[]> {
const commands = SUITE_COMMANDS[options.suite];
return this.runRemoteCommands(options, commands);
}
public async runLocalRepair(action: RepairAction): Promise<DiagnosticsResult[]> {
const command = REPAIR_COMMANDS[action];
return this.runLocalCommands([command]);
}
public async runRemoteRepair(options: RemoteMacOptions & { action: RepairAction }): Promise<DiagnosticsResult[]> {
const command = REPAIR_COMMANDS[options.action];
return this.runRemoteCommands(options, [command]);
}
private async runLocalCommands(commands: Array<{ label: string; command: string; requiresSudo?: boolean }>): Promise<DiagnosticsResult[]> {
const results: DiagnosticsResult[] = [];
for (const item of commands) {
const result = await this.runner.run(item.command, { requiresSudo: item.requiresSudo ?? false });
results.push(this.formatResult(item.label, result));
}
return results;
}
private async runRemoteCommands(
options: RemoteMacOptions,
commands: Array<{ label: string; command: string; requiresSudo?: boolean }>,
): Promise<DiagnosticsResult[]> {
const results: DiagnosticsResult[] = [];
const sshOptions: SshExecutionOptions = {
port: options.port,
identityFile: options.identityFile,
knownHostsFile: options.knownHostsFile,
extraOptions: options.extraOptions,
allocateTty: options.allocateTty,
timeoutSeconds: options.timeoutSeconds,
};
for (const item of commands) {
const remoteCommand = item.requiresSudo ? `sudo ${item.command}` : item.command;
const result = await this.ssh.execute(
{
host: options.host,
username: options.username,
command: remoteCommand,
},
sshOptions,
);
results.push(this.formatResult(item.label, result));
}
return results;
}
private formatResult(label: string, result: CommandResult): DiagnosticsResult {
return {
label,
command: result.command,
stdout: result.stdout,
stderr: result.stderr,
exitCode: result.code,
};
}
}