Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 0 additions & 12 deletions .github/workflows/resolutionFix.ts

This file was deleted.

42 changes: 28 additions & 14 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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"
}
]
}
Expand Down Expand Up @@ -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"
}
]
}
Expand Down
7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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",
Expand Down
21 changes: 17 additions & 4 deletions src/commands/setup/macOS/ensureLocalPreferencesExist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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));
}
Expand Down
4 changes: 2 additions & 2 deletions src/commands/setup/macOS/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -27,7 +27,7 @@ export async function setup({
}: MacOSSetupOptions = {}): Promise<void> {
if (!macosIgnoreTccDb) {
try {
await updateTccDb(USER_PATH);
await updateTccDb(getUserTccDbPath());
} catch (e) {
if (ci) {
throw e;
Expand Down
112 changes: 106 additions & 6 deletions src/commands/setup/macOS/updateTccDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof execFileSync>[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<void> {
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, {
Expand All @@ -221,6 +321,6 @@ export async function updateTccDb(path: string): Promise<void> {
}
}

// 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));
}
Loading