diff --git a/package.json b/package.json index 6b3da69..8e1e803 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@guidepup/setup", - "version": "0.26.0", + "version": "0.27.0", "description": "CLI for configuring environments and install screen reader assets for Guidepup.", "main": "lib/index.js", "typings": "lib/index.d.ts", diff --git a/src/commands/setup/macOS/setup.ts b/src/commands/setup/macOS/setup.ts index dbd2c05..1fea7cc 100644 --- a/src/commands/setup/macOS/setup.ts +++ b/src/commands/setup/macOS/setup.ts @@ -7,15 +7,12 @@ import { isSipEnabled } from "./isSipEnabled"; import { writeDatabaseFile } from "./writeDatabaseFile"; import { SYSTEM_PATH, USER_PATH, updateTccDb } from "./updateTccDb"; import { isAppleScriptControlEnabled } from "./isAppleScriptControlEnabled"; -import { - handleNote, - handleSetupManualRequired, - handleWarning, -} from "../../../logging"; +import { handleNote, handleWarning } from "../../../logging"; import { ERR_SETUP_MACOS_REQUIRES_MANUAL_USER_INTERACTION } from "../../../errors"; import { enableDoNotDisturb } from "./enableDoNotDisturb"; import { enabledDbFile } from "./isAppleScriptControlEnabled/enabledDbFile"; import { ensureLocalPreferencesExist } from "./ensureLocalPreferencesExist"; +import { waitForAppleScriptControl } from "./waitForAppleScriptControl"; interface MacOSSetupOptions { ci?: boolean; @@ -37,7 +34,7 @@ export async function setup({ } else { handleNote( "Unable to configure automation permissions", - "This can be expected when running the Guidepup setup locally if macOS does not grant the required permissions automatically.\nPlease accept any system dialogs requesting automation permissions while using Guidepup.\nIf you are running Guidepup in CI, use the `--ci` option to skip interactive permission setup.\nAlternatively, please refer to https://www.guidepup.dev/docs/guides/manual-voiceover-setup for instructions on manually configuring VoiceOver permissions.", + "This can be expected when running the Guidepup setup locally if macOS does not grant the required permissions automatically.\n\nPlease accept any system dialogs requesting automation permissions while using Guidepup.\n\nIf you are running Guidepup in CI, use the `--ci` option to skip interactive permission setup.\n\nAlternatively, please refer to https://www.guidepup.dev/docs/guides/manual-voiceover-setup for instructions on manually configuring VoiceOver permissions.", ); } } @@ -87,7 +84,11 @@ export async function setup({ await enableDoNotDisturb(); } - if (!isSipEnabled() && !(await enabledDbFile())) { + if (await enabledDbFile()) { + return; + } + + if (!isSipEnabled()) { writeDatabaseFile(); return; @@ -101,7 +102,7 @@ export async function setup({ throw new Error(ERR_SETUP_MACOS_REQUIRES_MANUAL_USER_INTERACTION); } - handleSetupManualRequired(); + await waitForAppleScriptControl(); } finally { stopRecording(); } diff --git a/src/commands/setup/macOS/waitForAppleScriptControl.ts b/src/commands/setup/macOS/waitForAppleScriptControl.ts new file mode 100644 index 0000000..c101633 --- /dev/null +++ b/src/commands/setup/macOS/waitForAppleScriptControl.ts @@ -0,0 +1,44 @@ +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import chalk from "chalk"; +import { + ERR_SETUP_MACOS_APPLESCRIPT_CONTROL_ENABLE_TIMEOUT, + ERR_SETUP_MACOS_UNABLE_TO_OPEN_VOICEOVER_UTILITY, +} from "../../../errors"; +import { handleNote, handleSetupManualRequired } from "../../../logging"; +import { enabledDbFile } from "./isAppleScriptControlEnabled/enabledDbFile"; + +const open = promisify(execFile); +const ENABLE_TIMEOUT_MS = 5 * 60 * 1000; +const POLL_INTERVAL_MS = 1000; + +export async function waitForAppleScriptControl(): Promise { + try { + await open("/usr/bin/open", ["-a", "VoiceOver Utility"]); + } catch (cause) { + handleSetupManualRequired(); + throw new Error(ERR_SETUP_MACOS_UNABLE_TO_OPEN_VOICEOVER_UTILITY, { + cause, + }); + } + + handleNote( + "Interaction required", + `Opening ${chalk.bold("VoiceOver Utility")} for a manual user step.\n\nIn the ${chalk.bold("General")} tab, please tick the checkbox for ${chalk.bold("Allow VoiceOver to be controlled with AppleScript")}.\n\nWaiting for user to apply the change...`, + ); + + const deadline = Date.now() + ENABLE_TIMEOUT_MS; + + while (Date.now() < deadline) { + if (await enabledDbFile()) { + return; + } + + await new Promise((resolve) => { + setTimeout(resolve, POLL_INTERVAL_MS); + }); + } + + handleSetupManualRequired(); + throw new Error(ERR_SETUP_MACOS_APPLESCRIPT_CONTROL_ENABLE_TIMEOUT); +} diff --git a/src/errors.ts b/src/errors.ts index ddce01c..3c08bf6 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -15,10 +15,14 @@ export const ERR_SETUP_MACOS_UNABLE_TO_WRITE_USER_TCC_DB = "Unable to write to the user TCC database\n\nEnsure that SIP is disabled for the database, or pass '--macos-ignore-tcc-db' to ignore TCC database updates"; export const ERR_SETUP_MACOS_REQUIRES_MANUAL_USER_INTERACTION = "Unable to setup environment without manual user interaction\n\nEnsure that SIP is disabled or preconfigure your environment with 'Allow VoiceOver to be controlled with AppleScript' enabled"; +export const ERR_SETUP_MACOS_APPLESCRIPT_CONTROL_ENABLE_TIMEOUT = + "Timed out waiting for 'Allow VoiceOver to be controlled with AppleScript' to be enabled"; export const ERR_SETUP_MACOS_FAILED_TO_ENABLE_DO_NOT_DISTURB = 'Failed to enable "Do not disturb" mode'; export const ERR_SETUP_MACOS_UNABLE_TO_START_VOICEOVER = "Unable to start VoiceOver"; +export const ERR_SETUP_MACOS_UNABLE_TO_OPEN_VOICEOVER_UTILITY = + "Unable to open VoiceOver Utility"; export const ERR_SETUP_MACOS_UNABLE_TO_STOP_VOICEOVER = "Unable to stop VoiceOver"; export const ERR_SETUP_MACOS_UNABLE_TO_FIND_VOICEOVER_PREFERENCES = diff --git a/src/logging.ts b/src/logging.ts index 0389d68..0d7ca36 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -37,7 +37,8 @@ export function handleSetupComplete(): never { } export function handleSetupManualRequired(): void { - logInfo( + handleNote( + "Manual setup required", "Please complete remaining setup by following this guide:\n\n--> " + chalk.underline( chalk.bold(