diff --git a/.github/workflows/resolutionFix.ts b/.github/workflows/resolutionFix.ts deleted file mode 100644 index 0cbbbaa..0000000 --- a/.github/workflows/resolutionFix.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { execSync } from "child_process"; - -if (process.platform === "darwin") { - try { - execSync( - `"/Library/Application Support/VMware Tools/vmware-resolutionSet" 1920 1080`, - { encoding: "utf8" } - ); - } catch (_) { - // swallow - } -} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7d8a932..2cbc926 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,11 +16,11 @@ jobs: matrix: os: [ - macos-14, macos-15, macos-15-intel, macos-26, macos-26-intel, + xcode-27, windows-2022, windows-2025, windows-11-arm, @@ -92,11 +92,11 @@ jobs: matrix: os: [ - macos-14, macos-15, macos-15-intel, macos-26, macos-26-intel, + xcode-27, windows-2022, windows-2025, windows-11-arm, @@ -159,25 +159,32 @@ jobs: "defaultDownload": true, "assets": [ { - "version": "0.0.1", + "version": "0.0.1-VoiceOver4", "platformVersion": "23", "repository": "guidepup/voiceover", "asset": "guidepup-voiceover-preferences-macos-14.dmg", - "sha256": "c6595f5ca50440e8553cde278936a46eff58d4c904e19c87e7d0e25950617ee1" + "sha256": "fd88500b740bb3389ec295465d35a5351d49a57fe120983aed3c761cfb349c6a" }, { - "version": "0.0.1", + "version": "0.0.1-VoiceOver4", "platformVersion": "24", "repository": "guidepup/voiceover", "asset": "guidepup-voiceover-preferences-macos-15.dmg", - "sha256": "8bdc3a11c45a19cc876a859bfbd64529469a8b7d4ad41fd7e00a0729ebdbcb25" + "sha256": "ef5a533e38f37aff44682b125b07855b4cd0eb24d04416d48df5097386580799" }, { - "version": "0.0.1", + "version": "0.0.1-VoiceOver4", "platformVersion": "25", "repository": "guidepup/voiceover", "asset": "guidepup-voiceover-preferences-macos-26.dmg", - "sha256": "9af2a3af7c9bffae1b2af26f1970548ecd62f33965fd30f4e43f21b9f57ce5ca" + "sha256": "c46e353d4f2d4a717d3362211de638ee55fcb58dbec9cf9abd0170c35d41b225" + }, + { + "version": "0.1.0-VoiceOver4", + "platformVersion": "27", + "repository": "guidepup/voiceover", + "asset": "guidepup-voiceover-preferences-xcode-27.dmg", + "sha256": "3ea13925dc75bf42df0ffe981814efc2a6d822b13321e2d5b0cb77593d15cee2" } ] } @@ -248,25 +255,32 @@ jobs: "defaultDownload": true, "assets": [ { - "version": "0.0.1", + "version": "0.0.1-VoiceOver4", "platformVersion": "23", "repository": "guidepup/voiceover", "asset": "guidepup-voiceover-preferences-macos-14.dmg", - "sha256": "c6595f5ca50440e8553cde278936a46eff58d4c904e19c87e7d0e25950617ee1" + "sha256": "fd88500b740bb3389ec295465d35a5351d49a57fe120983aed3c761cfb349c6a" }, { - "version": "0.0.1", + "version": "0.0.1-VoiceOver4", "platformVersion": "24", "repository": "guidepup/voiceover", "asset": "guidepup-voiceover-preferences-macos-15.dmg", - "sha256": "8bdc3a11c45a19cc876a859bfbd64529469a8b7d4ad41fd7e00a0729ebdbcb25" + "sha256": "ef5a533e38f37aff44682b125b07855b4cd0eb24d04416d48df5097386580799" }, { - "version": "0.0.1", + "version": "0.0.1-VoiceOver4", "platformVersion": "25", "repository": "guidepup/voiceover", "asset": "guidepup-voiceover-preferences-macos-26.dmg", - "sha256": "9af2a3af7c9bffae1b2af26f1970548ecd62f33965fd30f4e43f21b9f57ce5ca" + "sha256": "c46e353d4f2d4a717d3362211de638ee55fcb58dbec9cf9abd0170c35d41b225" + }, + { + "version": "0.1.0-VoiceOver4", + "platformVersion": "27", + "repository": "guidepup/voiceover", + "asset": "guidepup-voiceover-preferences-xcode-27.dmg", + "sha256": "3ea13925dc75bf42df0ffe981814efc2a6d822b13321e2d5b0cb77593d15cee2" } ] } diff --git a/package.json b/package.json index 8e1e803..9adf164 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@guidepup/setup", - "version": "0.27.0", + "version": "0.28.0", "description": "CLI for configuring environments and install screen reader assets for Guidepup.", "main": "lib/index.js", "typings": "lib/index.d.ts", @@ -27,12 +27,11 @@ ], "scripts": { "build": "yarn clean && yarn compile", - "ci": "yarn clean && yarn lint && yarn build && yarn resolutionFix && yarn start:setup --ci --macos-record", - "ci:ignore-tcc-db": "yarn clean && yarn lint && yarn build && yarn resolutionFix && yarn start:setup --ci --macos-record --macos-ignore-tcc-db", + "ci": "yarn clean && yarn lint && yarn build && yarn start:setup --ci --macos-record", + "ci:ignore-tcc-db": "yarn clean && yarn lint && yarn build && yarn start:setup --ci --macos-record --macos-ignore-tcc-db", "clean": "rimraf lib", "compile": "tsc", "dev": "ts-node ./src/index.ts", - "resolutionFix": "ts-node ./.github/workflows/resolutionFix.ts", "lint": "eslint . --ext .ts", "lint:fix": "yarn lint --fix", "start:setup": "node ./bin/guidepup setup", diff --git a/src/commands/setup/macOS/ensureLocalPreferencesExist.ts b/src/commands/setup/macOS/ensureLocalPreferencesExist.ts index c6b1622..a69817d 100644 --- a/src/commands/setup/macOS/ensureLocalPreferencesExist.ts +++ b/src/commands/setup/macOS/ensureLocalPreferencesExist.ts @@ -28,11 +28,24 @@ function getPreferencesDirectory(): string { return join(homedir(), "Library", "Preferences"); } +const voiceOverAppPath = "/System/Library/CoreServices/VoiceOver.app"; + +const voiceOverStarterPath = `${voiceOverAppPath}/Contents/MacOS/VoiceOverStarter`; + async function startVoiceOver(): Promise { - execSync( - "/System/Library/CoreServices/VoiceOver.app/Contents/MacOS/VoiceOverStarter &", - { stdio: "ignore", timeout: 2000 }, - ); + const darwinMajorVersion = platformMajorVersion(); + + if (darwinMajorVersion >= 27) { + execFileSync("/usr/bin/open", ["-a", voiceOverAppPath], { + stdio: "ignore", + timeout: 2000, + }); + } else { + execSync(`${voiceOverStarterPath} &`, { + stdio: "ignore", + timeout: 2000, + }); + } await new Promise((resolve) => setTimeout(resolve, 1000)); } diff --git a/src/commands/setup/macOS/setup.ts b/src/commands/setup/macOS/setup.ts index 1fea7cc..07570a3 100644 --- a/src/commands/setup/macOS/setup.ts +++ b/src/commands/setup/macOS/setup.ts @@ -5,7 +5,7 @@ import { disableSplashScreenSystemDefaults } from "./disableSplashScreenSystemDe import { disableDictationInputAutoEnable } from "./disableDictationInputAutoEnable"; import { isSipEnabled } from "./isSipEnabled"; import { writeDatabaseFile } from "./writeDatabaseFile"; -import { SYSTEM_PATH, USER_PATH, updateTccDb } from "./updateTccDb"; +import { getUserTccDbPath, SYSTEM_PATH, updateTccDb } from "./updateTccDb"; import { isAppleScriptControlEnabled } from "./isAppleScriptControlEnabled"; import { handleNote, handleWarning } from "../../../logging"; import { ERR_SETUP_MACOS_REQUIRES_MANUAL_USER_INTERACTION } from "../../../errors"; @@ -27,7 +27,7 @@ export async function setup({ }: MacOSSetupOptions = {}): Promise { if (!macosIgnoreTccDb) { try { - await updateTccDb(USER_PATH); + await updateTccDb(getUserTccDbPath()); } catch (e) { if (ci) { throw e; diff --git a/src/commands/setup/macOS/updateTccDb.ts b/src/commands/setup/macOS/updateTccDb.ts index 4c29ab8..9f3b555 100644 --- a/src/commands/setup/macOS/updateTccDb.ts +++ b/src/commands/setup/macOS/updateTccDb.ts @@ -189,24 +189,124 @@ const getEntries = (): string[] => { const TIMEOUT_BACKOFFS = [1000, 1000, 3000, 5000, 8000]; -export const USER_PATH = `${homedir()}/Library/Application Support/com.apple.TCC/TCC.db`; export const SYSTEM_PATH = "/Library/Application Support/com.apple.TCC/TCC.db"; +const LEGACY_USER_PATH = `${homedir()}/Library/Application Support/com.apple.TCC/TCC.db`; +const PROTECTED_SYSTEM_PATH = "/private/var/containers/Data/ProtectedSystem"; + +function getMacOsMajorVersion(): number { + const major = parseInt(release().split(".")[0], 10); + + if (Number.isNaN(major)) { + throw new Error(`Unexpected macOS version: ${release()}`); + } + + return major; +} + +function execFileSyncAsRoot( + file: string, + args: string[], + options?: Parameters[2], +): string { + return execFileSync("sudo", [file, ...args], { + ...options, + encoding: "utf8", + }) as string; +} + +export function getUserTccDbPath(): string { + const macOsMajor = getMacOsMajorVersion(); + + if (macOsMajor < 27) { + return LEGACY_USER_PATH; + } + + const databases = execFileSyncAsRoot("find", [ + PROTECTED_SYSTEM_PATH, + "-mindepth", + "6", + "-maxdepth", + "6", + "-type", + "f", + "-path", + "*/Data/Library/Application Support/com.apple.TCC/TCC.db", + "-print", + ]) + .trim() + .split("\n") + .filter(Boolean); + + if (databases.length === 0) { + throw new Error("Unable to find a ProtectedSystem TCC database"); + } + + if (databases.length === 1) { + return databases[0]; + } + + const openFiles = execFileSyncAsRoot("lsof", ["-c", "tccd", "-Fn"]) + .split("\n") + .filter((line) => line.startsWith("n")) + .map((line) => line.slice(1)) + .filter( + (path) => + path.startsWith(`${PROTECTED_SYSTEM_PATH}/`) && + /\/com\.apple\.TCC\/TCC\.db$/.test(path), + ); + + const activeDatabases = databases.filter((database) => + openFiles.includes(database), + ); + + if (activeDatabases.length !== 1) { + throw new Error( + [ + "Unable to identify one active ProtectedSystem TCC database:", + ...databases.map((database) => ` ${database}`), + ].join("\n"), + ); + } + + return activeDatabases[0]; +} + export async function updateTccDb(path: string): Promise { - const osRelease = release(); - const isSonomaOrNewer = parseInt(osRelease.split(".")[0], 10) >= 23; + const macOsMajor = getMacOsMajorVersion(); + const isSonomaOrNewer = macOsMajor >= 23; + const columns = [ + "service", + "client", + "client_type", + "auth_value", + "auth_reason", + "auth_version", + "csreq", + "policy_id", + "indirect_object_identifier_type", + "indirect_object_identifier", + "indirect_object_code_identity", + "flags", + "last_modified", + ...(isSonomaOrNewer + ? ["pid", "pid_version", "boot_uuid", "last_reminded"] + : []), + ]; for (const values of getEntries()) { - const query = `INSERT OR IGNORE INTO access VALUES(${values}${ + const query = `INSERT OR IGNORE INTO access (${columns.join(",")}) VALUES(${values}${ isSonomaOrNewer ? `,NULL,NULL,'UNUSED',${epoch}` : "" });`; for (let i = 0; i < TIMEOUT_BACKOFFS.length + 1; i++) { try { - execFileSync("sqlite3", [path, query], { + execFileSyncAsRoot("sqlite3", [path, query], { encoding: "utf8", stdio: "ignore", }); + + break; } catch (cause) { if (i === TIMEOUT_BACKOFFS.length) { throw new Error(ERR_SETUP_MACOS_UNABLE_TO_WRITE_USER_TCC_DB, { @@ -221,6 +321,6 @@ export async function updateTccDb(path: string): Promise { } } - // 1s sleep to give cache for updates to propagate + // Give the TCC cache time to observe the database updates. await new Promise((resolve) => setTimeout(resolve, 1000)); }