openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
106 lines (105 loc) • 3.24 kB
JavaScript
import { Rn as string, Tn as object } from "./schemas-zxit8y5H.js";
import { promisify } from "node:util";
import path from "node:path";
import fs from "node:fs/promises";
import { execFile } from "node:child_process";
import os from "node:os";
import pLimit from "p-limit";
//#region src/infra/installed-apps.ts
const execFileAsync = promisify(execFile);
const PLIST_READ_CONCURRENCY = 8;
const PLIST_READ_TIMEOUT_MS = 2e3;
const SYSTEM_APP_NAMES = /* @__PURE__ */ new Set([
"Calendar",
"Contacts",
"FaceTime",
"Home",
"Mail",
"Maps",
"Messages",
"Music",
"Notes",
"Photos",
"Podcasts",
"Reminders",
"Shortcuts"
]);
const InfoPlistSchema = object({ CFBundleIdentifier: string().trim().min(1).optional() }).passthrough();
function defaultRoots() {
return {
applications: "/Applications",
userApplications: path.join(os.homedir(), "Applications"),
systemApplications: "/System/Applications"
};
}
function isBackupishBundle(label) {
return /(?:^|[\s._-])(?:backup|previous|rollback)(?:[\s._-]|$)/i.test(label) || /(?:^|[\s._-])pre-[\p{L}\p{N}._-]+$/iu.test(label);
}
async function listAppPaths(root, system) {
let entries;
try {
entries = await fs.readdir(root, { withFileTypes: true });
} catch {
return [];
}
return (await Promise.all(entries.map(async (entry) => {
if (!entry.name.toLowerCase().endsWith(".app")) return [];
let isDirectory = entry.isDirectory();
if (!isDirectory && entry.isSymbolicLink()) isDirectory = await fs.stat(path.join(root, entry.name)).then((stats) => stats.isDirectory()).catch(() => false);
if (!isDirectory) return [];
const label = entry.name.slice(0, -4);
if (isBackupishBundle(label) || system && !SYSTEM_APP_NAMES.has(label)) return [];
return [{
path: path.join(root, entry.name),
system
}];
}))).flat();
}
async function readBundleIdWithPlutil(appPath) {
try {
const { stdout } = await execFileAsync("/usr/bin/plutil", [
"-convert",
"json",
"-o",
"-",
path.join(appPath, "Contents", "Info.plist")
], {
encoding: "utf8",
maxBuffer: 1048576,
timeout: PLIST_READ_TIMEOUT_MS
});
return InfoPlistSchema.parse(JSON.parse(stdout)).CFBundleIdentifier;
} catch {
return;
}
}
async function scanInstalledApps(options = {}) {
const platform = options.platform ?? process.platform;
if (platform !== "darwin") return {
status: "unsupported",
platform,
apps: []
};
const roots = options.roots ?? defaultRoots();
const appPaths = (await Promise.all([
listAppPaths(roots.applications, false),
listAppPaths(roots.userApplications, false),
listAppPaths(roots.systemApplications, true)
])).flat();
const readBundleId = options.readBundleId ?? readBundleIdWithPlutil;
const limit = pLimit(PLIST_READ_CONCURRENCY);
return {
status: "ok",
apps: (await Promise.all(appPaths.map((entry) => limit(async () => {
const bundleId = await readBundleId(entry.path);
return {
label: path.basename(entry.path, ".app"),
...bundleId ? { bundleId } : {},
path: entry.path,
system: entry.system
};
})))).toSorted((left, right) => left.label.localeCompare(right.label, "en", { sensitivity: "base" }) || left.path.localeCompare(right.path))
};
}
//#endregion
export { scanInstalledApps as t };