import { execFile } from "node:child_process"; import { stat } from "node:fs/promises"; import path from "node:path"; import { promisify } from "node:util"; const execFileAsync = promisify(execFile); const PROCESS_SCRIPT = ` $ErrorActionPreference = 'Stop' $rows = @(Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -like '*\\Microsoft Scout\\*' -or $_.CommandLine -like '*\\Microsoft Scout\\*' } | Select-Object ProcessId, Name, ExecutablePath) ConvertTo-Json -Compress -Depth 3 -InputObject $rows `; function normalize(filePath) { return path.resolve(filePath).replaceAll("/", "\\").toLowerCase(); } function isWithin(candidatePath, parentPath) { const relative = path.relative(path.resolve(parentPath), path.resolve(candidatePath)); return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)); } function getDirectoryAggregate(result, directoryPath) { const normalizedPath = normalize(directoryPath); return result.directories.find((directory) => normalize(directory.path) === normalizedPath); } async function describeDirectory(result, id, name, directoryPath, kind) { let stats; try { stats = await stat(directoryPath); } catch (error) { if (error?.code === "ENOENT") { return undefined; } throw error; } if (!stats.isDirectory()) { return undefined; } const aggregate = getDirectoryAggregate(result, directoryPath); return { id, name, path: directoryPath, kind, bytes: aggregate?.bytes ?? 0, files: aggregate?.files ?? 0, modifiedAt: stats.mtime.toISOString(), }; } async function inspectProcesses() { if (process.platform !== "win32") { return { status: "unsupported", processCount: 0, processes: [] }; } try { const { stdout } = await execFileAsync("powershell.exe", [ "-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(PROCESS_SCRIPT, "utf16le").toString("base64"), ], { windowsHide: true, maxBuffer: 4 * 1024 * 1024 }); const parsed = JSON.parse(stdout.trim() || "[]"); const processes = Array.isArray(parsed) ? parsed : [parsed]; return { status: processes.length ? "running" : "not-running", processCount: processes.length, processes, }; } catch (error) { return { status: "unknown", processCount: 0, processes: [], error: error.message, }; } } export async function analyzeMicrosoftScout(result) { const userProfile = process.env.USERPROFILE ?? ""; const localAppData = process.env.LOCALAPPDATA ?? path.join(userProfile, "AppData", "Local"); const installRoot = path.resolve(localAppData, "Programs", "Clawpilot", "Microsoft Scout"); const userDataRoot = path.resolve(userProfile, ".scout"); const processInspection = await inspectProcesses(); const locations = (await Promise.all([ describeDirectory(result, "scout-install", "Installed application", installRoot, "application"), describeDirectory(result, "scout-user-data", "User data", userDataRoot, "user-data"), ])).filter(Boolean); const cleanupLocations = (await Promise.all([ describeDirectory(result, "scout-copilot-cache", "Copilot package cache", path.join(userDataRoot, "copilot", "cache"), "cache"), describeDirectory(result, "scout-logs", "Logs", path.join(userDataRoot, "logs"), "logs"), describeDirectory(result, "scout-crashpad", "Crash reports", path.join(userDataRoot, "Crashpad"), "crash-reports"), describeDirectory(result, "scout-temp", "Temporary data", path.join(userDataRoot, "temp"), "temporary"), ])).filter(Boolean); const canClean = processInspection.status === "not-running"; const cleanupItems = cleanupLocations.map((location) => ({ ...location, entryType: "directory", cleanupEligible: canClean, risk: "low", reason: `${location.name} can be regenerated by Microsoft Scout`, })); const roots = [installRoot, userDataRoot]; const topFiles = result.largestFiles .filter((file) => roots.some((root) => isWithin(file.path, root))) .sort((left, right) => right.bytes - left.bytes) .slice(0, 20); return { id: "microsoft-scout", status: locations.length ? processInspection.status : "not-found", processCount: processInspection.processCount, processInspectionError: processInspection.error, locations, cleanupItems, topFiles, totalBytes: locations.reduce((total, location) => total + location.bytes, 0), cleanupBytes: cleanupItems.reduce((total, item) => total + item.bytes, 0), message: !locations.length ? "Microsoft Scout storage was not found in its standard Windows locations." : processInspection.status === "running" ? "Microsoft Scout is running. Close it before cleaning its cache, logs, or temporary data." : processInspection.status === "not-running" ? "Microsoft Scout is not running. Regenerable storage locations can be selected for cleanup." : "Microsoft Scout process state could not be verified, so cleanup is disabled.", }; }