/**
* mcp/skills/getXcodeDerivedDataInfo.ts — get_xcode_derived_data_info
*
* Reports Xcode DerivedData / Archives / DeviceSupport directory sizes
* without modifying anything. Read-only counterpart to
* `clear_xcode_derived_data`. macOS only.
*
* Smoke test
* npx tsx -r dotenv/config mcp/skills/getXcodeDerivedDataInfo.ts
*
* NOTE: target-path resolution is duplicated from `clearXcodeDerivedData.ts`
* — keep in sync if the Xcode directory layout changes.
*/
import * as fs from "fs/promises";
import * as os from "os";
import * as nodePath from "path";
import { execAsync, isDarwin } from "./_shared/platform";
import { formatBytes } from "./_shared/formatBytes";
// -- Meta ---------------------------------------------------------------------
export const meta = {
name: "get_xcode_derived_data_info",
description:
"Reports Xcode DerivedData / Archives / DeviceSupport directory sizes " +
"(macOS) without modifying anything. Read-only counterpart to " +
"clear_xcode_derived_data. Returns supported:false on non-darwin platforms.",
riskLevel: "low",
destructive: false,
requiresConsent: false,
supportsDryRun: false,
affectedScope: ["user"],
auditRequired: false,
tccCategories: ["FullDiskAccess"],
schema: {},
} as const;
// -- Types --------------------------------------------------------------------
export interface XcodeTargetInfo {
name: string;
path: string;
exists: boolean;
sizeBytes: number;
}
export interface GetXcodeDerivedDataInfoResult {
platform: NodeJS.Platform;
supported: boolean;
targets: XcodeTargetInfo[];
totalBytes: number;
/** Pre-formatted size of `totalBytes` (decimal/SI). The disk-cleanup card
* substitutes this verbatim into the Xcode category's {size} placeholder. */
totalHuman: string;
errors?: Array<{ scope: string; message: string }>;
}
// -- Helpers ------------------------------------------------------------------
async function getDirSizeBytes(dirPath: string): Promise<number> {
try {
const safePath = dirPath.replace(/'/g, `'\\''`);
const { stdout } = await execAsync(
`du -sk '${safePath}' 2>/dev/null`,
{ maxBuffer: 2 * 1024 * 1024, shell: "/bin/bash" },
);
const kb = parseInt(stdout.trim().split("\t")[0], 10);
return isNaN(kb) ? 0 : kb * 1024;
} catch {
return 0;
}
}
function resolveDarwinTargets(): Array<{ name: string; path: string }> {
const home = os.homedir();
const xcodeDir = nodePath.join(home, "Library", "Developer", "Xcode");
return [
{ name: "derivedData", path: nodePath.join(xcodeDir, "DerivedData") },
{ name: "archives", path: nodePath.join(xcodeDir, "Archives") },
{ name: "iOS DeviceSupport", path: nodePath.join(xcodeDir, "iOS DeviceSupport") },
{ name: "watchOS DeviceSupport", path: nodePath.join(xcodeDir, "watchOS DeviceSupport") },
{ name: "tvOS DeviceSupport", path: nodePath.join(xcodeDir, "tvOS DeviceSupport") },
{ name: "visionOS DeviceSupport", path: nodePath.join(xcodeDir, "visionOS DeviceSupport") },
];
}
// -- darwin -------------------------------------------------------------------
async function getXcodeDerivedDataInfoDarwin(): Promise<GetXcodeDerivedDataInfoResult> {
const defs = resolveDarwinTargets();
const targets: XcodeTargetInfo[] = [];
for (const def of defs) {
let exists = false;
let sizeBytes = 0;
try {
await fs.access(def.path);
exists = true;
sizeBytes = await getDirSizeBytes(def.path);
} catch { /* not present */ }
targets.push({ name: def.name, path: def.path, exists, sizeBytes });
}
targets.sort((a, b) => b.sizeBytes - a.sizeBytes);
const totalBytes = targets.reduce((s, t) => s + t.sizeBytes, 0);
return { platform: "darwin", supported: true, targets, totalBytes, totalHuman: formatBytes(totalBytes) };
}
// -- Exported run -------------------------------------------------------------
export async function run(_args: Record<string, never> = {}): Promise<GetXcodeDerivedDataInfoResult> {
if (!isDarwin()) {
return {
platform: os.platform(),
supported: false,
targets: [],
totalBytes: 0,
totalHuman: formatBytes(0),
};
}
return getXcodeDerivedDataInfoDarwin();
}
// -- CLI smoke test -----------------------------------------------------------
if (require.main === module) {
run()
.then((r) => console.log(JSON.stringify(r, null, 2)))
.catch((err: Error) => { console.error(err.message); process.exit(1); });
}