diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 02d5e05..5e9d572 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,7 +23,7 @@ jobs: windows-11-arm, ] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version-file: .nvmrc @@ -41,7 +41,7 @@ jobs: matrix: os: [macos-latest] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version-file: .nvmrc @@ -53,22 +53,49 @@ jobs: name: artifacts-test-ignore-tcc-db-${{ matrix.os }} path: | **/recordings/**/* - test-nvda-install-dir: + + install: runs-on: ${{ matrix.os }} strategy: matrix: - os: [windows-2022, windows-2025, windows-11-arm] + os: [macos-14, macos-15, macos-15-intel, macos-26, macos-26-intel] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version-file: .nvmrc - run: yarn install --frozen-lockfile - - run: yarn ci:nvda-install-dir - - run: Get-ChildItem $env:USERPROFILE\nvda - - uses: actions/upload-artifact@v4 - if: always() - with: - name: artifacts-test-nvda-install-dir-${{ matrix.os }} - path: | - **/recordings/**/* + - run: yarn build + - run: | + tmpdir=$(mktemp -d) + cd "$tmpdir" + npm init -y + npm install @guidepup/guidepup + npm install -g "${GITHUB_WORKSPACE}" + export PATH="$(npm bin -g):$PATH" + + cat > node_modules/@guidepup/guidepup/manifest.json <<'EOF' + { + "version": 1, + "screenReaders": [ + { + "id": "voiceover", + "name": "VoiceOver", + "platforms": ["darwin"], + "defaultDownload": true, + "assets": [ + { + "version": "1.0.0", + "platformVersion": "26", + "repository": "guidepup/voiceover", + "release": "v1.0.0", + "asset": "voiceover-macos-26.dmg", + "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + } + ] + } + ] + } + EOF + + guidepup install diff --git a/README.md b/README.md index b804805..100b0b5 100644 --- a/README.md +++ b/README.md @@ -6,15 +6,15 @@ ## [Documentation](https://www.guidepup.dev/docs/guides/automated-environment-setup) -[![MacOS Sonoma Support](https://img.shields.io/badge/macos-Somona-blue.svg?logo=apple)](https://apps.apple.com/us/app/macos-sonoma/id6450717509) -[![MacOS Sequoia Support](https://img.shields.io/badge/macos-Sequoia-blue.svg?logo=apple)](https://apps.apple.com/us/app/macos-sequoia/id6596773750) -[![MacOS Tahoe Support](https://img.shields.io/badge/macos-Tahoe-blue.svg?logo=apple)](https://www.apple.com/uk/os/macos/) +[![macOS Sonoma Support](https://img.shields.io/badge/macos-Somona-blue.svg?logo=apple)](https://apps.apple.com/us/app/macos-sonoma/id6450717509) +[![macOS Sequoia Support](https://img.shields.io/badge/macos-Sequoia-blue.svg?logo=apple)](https://apps.apple.com/us/app/macos-sequoia/id6596773750) +[![macOS Tahoe Support](https://img.shields.io/badge/macos-Tahoe-blue.svg?logo=apple)](https://www.apple.com/uk/os/macos/) [![Windows Server 2022 Support](https://img.shields.io/badge/windows_server-2022-blue.svg?logo=windows)](https://www.microsoft.com/en-us/evalcenter/evaluate-windows-server-2022) [![Windows Server 2025 Support](https://img.shields.io/badge/windows_server-2025-blue.svg?logo=windows)](https://www.microsoft.com/en-us/evalcenter/evaluate-windows-server-2025) This package sets up your environment for screen reader automation. -It enables automation for VoiceOver on MacOS and NVDA on Windows. +It enables automation for VoiceOver on macOS and NVDA on Windows. ## Getting Started @@ -37,7 +37,7 @@ If you are using GitHub Actions, check out the dedicated [`guidepup/setup-action uses: guidepup/setup-action ``` -### MacOS +### macOS If you are running this command locally you may need to take some manual steps to complete setup by following the [manual VoiceOver setup documentation](https://www.guidepup.dev/docs/guides/manual-voiceover-setup). @@ -49,12 +49,12 @@ If you are running this command in CI/CD, it is recommended to add the `--ci` fl npx @guidepup/setup --ci ``` -#### Ignore TCC.db Updates +#### Ignore TCC Database Updates -If updating the TCC.db is not possible (due to SIP) or required you can skip the database update step by using the `--ignore-tcc-db` flag: +If updating the `TCC.db` is not possible (due to enabled SIP) or not required, you can skip the database update step by using the `--macos-ignore-tcc-db` flag: ```console -npx @guidepup/setup --ignore-tcc-db +npx @guidepup/setup --macos-ignore-tcc-db ``` > [!NOTE] @@ -62,22 +62,10 @@ npx @guidepup/setup --ignore-tcc-db #### Recording -If you are encountering errors in CI for MacOS you can pass a `--record` flag to the command which will output a screen-recording of the setup to a `./recordings/` directory: +If you are encountering errors in CI for macOS you can pass a `--macos-record` flag to the command which will output a screen-recording of the setup to a `./recordings/` directory: ```console -npx @guidepup/setup --ci --record -``` - -### Windows - -#### NVDA Installation - -When running on windows a portable NVDA instance compatible with Guidepup will be installed to a temporary directory by default. The location of this installation directory is stored in the Windows registry under the key `HKCU\Software\Guidepup\Nvda`. - -If you want to specify the directory that NVDA is installed to you can pass a `--nvda-install-dir` flag to the command: - -```console -npx @guidepup/setup --nvda-install-dir +npx @guidepup/setup --ci --macos-record ``` ##### Using HTTP / HTTPS Proxy for Installation @@ -89,19 +77,6 @@ If you are using a proxy connection, you must define the proxy URL in an env var - `HTTP_PROXY` - `http_proxy` -#### Foreground Timeout Lock - -Modern versions of Windows have a setting which prevents new application instances launching in front of other applications in quick succession, requiring over 3 minutes between activations before it will actually show the window - in the interim it launches the window minimized. - -Many test automation frameworks will completely close down a browser after a test has finished and then launch a new instance for the next test - on Windows this suffers from the timeout lock on foreground windows. This impacts on screen reader automation which need the window to activate and focus to be able to drive the screen reader on the application. - -To mitigate this the setup script updates two keys in the Windows registry under `HKCU\Control Panel\Desktop`: - -- `ForegroundLockTimeout` - Specifies the time in milliseconds, following user input, during which the system will not allow applications to force themselves into the foreground. Defaults to `200000`. -- `ForegroundFlashCount` - Determines the number of times the taskbar button will flash to notify the user that a background window has been activated by the system. If the time elapsed since the last user input exceeds the value of ForegroundLockTimeout, the window will automatically be brought to the foreground. Defaults to `3`. - -Both of these are set to `0` by the setup script. - ## Powerful Tooling Check out some of the other Guidepup modules: diff --git a/bin/setup b/bin/guidepup similarity index 100% rename from bin/setup rename to bin/guidepup diff --git a/package.json b/package.json index 41fa80f..da5f0e5 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "name": "@guidepup/setup", - "version": "0.23.0", - "description": "Setup your environment for screen-reader automation.", + "version": "0.24.0", + "description": "Configure your environment and manage screen readers for Guidepup.", "main": "lib/index.js", "typings": "lib/index.d.ts", "bin": { - "setup": "bin/setup" + "guidepup": "bin/guidepup" }, "author": "Craig Morten ", "license": "MIT", @@ -26,16 +26,15 @@ ], "scripts": { "build": "yarn clean && yarn compile", - "ci": "yarn clean && yarn lint && yarn build && yarn resolutionFix && yarn start --ci --record", - "ci:nvda-install-dir": "yarn clean && yarn lint && yarn build && yarn resolutionFix && yarn start --ci --record --nvda-install-dir %userprofile%\\nvda", - "ci:ignore-tcc-db": "yarn clean && yarn lint && yarn build && yarn resolutionFix && yarn start --ci --record --ignore-tcc-db", + "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", "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": "node ./bin/setup", + "start:setup": "node ./bin/guidepup setup", "prepublish": "yarn build" }, "devDependencies": { @@ -53,6 +52,7 @@ }, "dependencies": { "chalk": "^4.0.0", + "commander": "^15.0.0", "decompress": "^4.2.1", "https-proxy-agent": "^7.0.5", "regedit": "5.0.1", diff --git a/spec.md b/spec.md new file mode 100644 index 0000000..ce8a4ea --- /dev/null +++ b/spec.md @@ -0,0 +1,336 @@ +CLI Specification + +Summary + +This document defines the public behaviour of the @guidepup/setup CLI. + +The CLI has two primary responsibilities: + +- Configure the local environment required by Guidepup. +- Install screen reader assets required by an installed version of @guidepup/guidepup. + +The CLI intentionally mirrors the Playwright CLI where appropriate, providing a familiar developer experience while remaining independent from the implementation details of individual screen readers. + +⸻ + +Goals + +- Simple, predictable command structure. +- Separate environment configuration from screen reader installation. +- Reproducible installations. +- Deterministic asset selection. +- Shared per-user cache. +- Offline operation when all required assets already exist. +- Future extensibility without breaking existing users. + +⸻ + +Non-goals + +The following are explicitly out of scope for this specification: + +- Updating installed screen readers. +- Listing installed screen readers. +- Uninstalling screen readers. +- Pruning unused assets. +- Defining the manifest schema (covered separately). + +⸻ + +CLI + +Commands + +Setup + +Configures the local environment required by Guidepup. + +guidepup setup + +This command performs environment configuration only. + +It MUST NOT: + +- download screen readers +- install screen readers +- invoke the install command implicitly + +⸻ + +Install + +Installs screen reader assets. + +guidepup install +guidepup install + +Examples: + +guidepup install +guidepup install nvda +guidepup install voiceover + +When no screen reader is specified, the CLI installs all screen readers that: + +- are compatible with the current operating system +- have defaultDownload: true in the manifest + +When a screen reader is specified, only that screen reader is processed. + +If the requested screen reader is incompatible with the current operating system, the CLI MUST: + +- print a clear error explaining the incompatibility +- exit with status code 1 + +⸻ + +Help + +guidepup --help +guidepup install --help + +⸻ + +Version + +guidepup --version + +⸻ + +Manifest + +Source of truth + +@guidepup/guidepup owns compatibility between Guidepup and screen reader assets. + +@guidepup/setup MUST NOT contain hardcoded knowledge of compatible screen reader versions. + +Instead it reads a manifest supplied by the installed @guidepup/guidepup package. + +The manifest is the public contract between the runtime package and the CLI. + +⸻ + +Location + +The manifest MUST exist at: + +@guidepup/guidepup/ +manifest.json + +⸻ + +Discovery + +The CLI discovers the installed Guidepup package using normal Node.js module resolution. + +The nearest installed @guidepup/guidepup package is used. + +No CLI flags are required to locate the manifest. + +⸻ + +Missing package + +If no local installation of @guidepup/guidepup can be found, the CLI MUST fail with a clear error. + +Example: + +No local installation of @guidepup/guidepup was found. +Install @guidepup/guidepup before running: +guidepup install + +Exact wording is implementation defined. + +Exit status: + +1 + +⸻ + +Manifest validation + +The CLI MUST validate that: + +- the manifest exists +- the manifest is valid JSON +- required fields exist +- the schema version is supported + +If validation fails, the CLI MUST fail with a clear error explaining that the installation appears invalid or corrupted. + +The CLI MUST NOT attempt to recover from an invalid manifest. + +Exit status: + +1 + +⸻ + +Manifest schema version + +The manifest contains a schema version. + +The CLI supports one or more schema versions. + +If the CLI encounters an unsupported schema version it MUST fail with an error instructing the user to upgrade @guidepup/setup. + +Compatibility is determined by the manifest schema version—not by the versions of either npm package. + +⸻ + +Asset Resolution + +The manifest defines: + +- supported screen readers +- compatible operating systems +- downloadable asset information +- checksum +- default download behaviour + +The CLI consumes this information without embedding compatibility logic. + +⸻ + +Cache + +Default location + +Downloaded assets are stored in a per-user cache. + +Platform defaults: + +macOS + +~/Library/Caches/guidepup + +Linux + +~/.cache/guidepup + +Windows + +%LOCALAPPDATA%\guidepup + +⸻ + +Override + +The cache location may be overridden using: + +GUIDEPUP_SCREEN_READERS_PATH + +This path may point anywhere accessible to the current user. + +The CLI does not distinguish between user and machine-wide installations. + +⸻ + +Installation Behaviour + +For every required asset: + +1. Resolve the asset from the manifest. +2. Check whether the required version already exists. +3. Validate the local asset against the checksum in the manifest. +4. If valid: + - perform no download + - report success (wording implementation defined) +5. If missing or invalid: + - obtain the asset + - verify download integrity + - install the asset + - validate the installed result + +The CLI should be self-healing. + +Corrupt or incomplete installations are automatically replaced. + +⸻ + +Integrity + +The manifest is the trusted description of every asset. + +It contains sufficient information to validate both downloaded and installed assets. + +The download mechanism is intentionally abstract. + +Initially the CLI may construct GitHub Release download URLs. + +Future implementations may instead use: + +- GitHub APIs +- mirrors +- CDNs +- dedicated artifact services +- CLI tooling + +without requiring manifest changes. + +⸻ + +Offline Behaviour + +If every required asset already exists locally and passes checksum validation: + +- no network requests are required +- the install command succeeds +- the command behaves as a no-op + +Internet access is only required when an asset must be downloaded or replaced. + +⸻ + +Multiple Assets + +Each asset installation is independent. + +If one asset fails to install, remaining assets should still be attempted. + +At completion: + +If every asset succeeds: + +Exit status: + +0 + +If one or more assets fail: + +- report failures +- exit with status code + +1 + +⸻ + +Asset Lifetime + +Installing assets never removes existing assets. + +The install command only: + +- installs missing assets +- repairs corrupted assets + +Automatic pruning is out of scope. + +Future CLI commands may introduce explicit cache management. + +⸻ + +Design Principles + +The CLI follows these principles: + +- Guidepup owns compatibility. +- The manifest is the contract. +- The CLI executes the manifest. +- Assets are shared across projects. +- Existing installations are reused whenever possible. +- Installs are reproducible. +- Offline operation is supported. +- Commands perform one responsibility only. +- Destructive operations are always explicit. diff --git a/src/commands/install/index.ts b/src/commands/install/index.ts new file mode 100644 index 0000000..3d46dd1 --- /dev/null +++ b/src/commands/install/index.ts @@ -0,0 +1,44 @@ +import { Command } from "commander"; +import { + handleInstallComplete, + handleInstallError, + logInfo, +} from "../../logging"; +import { + type InstallTarget, + resolveGuidepupManifest, + selectInstallTargets, +} from "./manifest"; + +async function install(screenreader?: string): Promise { + let targets: InstallTarget[]; + + console.log({ screenreader }); + + try { + const manifest = resolveGuidepupManifest(); + + console.log({ manifest }); + + targets = selectInstallTargets(manifest, screenreader); + } catch (error) { + handleInstallError(error); + } + + if (targets.length === 0) { + logInfo("No installable screen readers were found for this environment"); + } else { + logInfo( + `Resolved ${targets.length} install target(s): ${targets.map((target) => target.name).join(", ")}`, + ); + } + + handleInstallComplete(); +} + +export function installCommand() { + return new Command("install") + .description("Install screen readers.") + .argument("[screenreader]", "screen readers to install") + .action(install); +} diff --git a/src/commands/install/manifest.ts b/src/commands/install/manifest.ts new file mode 100644 index 0000000..4e24433 --- /dev/null +++ b/src/commands/install/manifest.ts @@ -0,0 +1,165 @@ +import { existsSync, readFileSync } from "node:fs"; +import * as path from "node:path"; +import { platform } from "node:os"; +import { + ERR_INSTALL_GUIDEPUP_MANIFEST_INVALID, + ERR_INSTALL_GUIDEPUP_MANIFEST_UNSUPPORTED_SCHEMA, + ERR_INSTALL_GUIDEPUP_PACKAGE_NOT_FOUND, + ERR_INSTALL_GUIDEPUP_SCREEN_READER_UNAVAILABLE, +} from "../../errors"; + +const SUPPORTED_SCHEMA_VERSIONS = [1]; + +interface ManifestScreenReader { + id?: string; + name?: string; + platforms?: string[]; + defaultDownload?: boolean; + assets?: unknown[]; + [key: string]: unknown; +} + +export interface InstallTarget { + name: string; + data: ManifestScreenReader; +} + +function isCompatibleWithCurrentPlatform( + screenReader: ManifestScreenReader, +): boolean { + const compatiblePlatforms = screenReader.platforms; + + if (!compatiblePlatforms || compatiblePlatforms.length === 0) { + return true; + } + + const currentPlatform = platform(); + + console.log({ currentPlatform }); + + return compatiblePlatforms.some((candidate) => { + if (typeof candidate !== "string") { + return false; + } + + return candidate.toLowerCase() === currentPlatform; + }); +} + +function getScreenReaders( + manifest: Record, +): Record { + const screenReaders = manifest.screenReaders; + + if (!Array.isArray(screenReaders) || screenReaders.length === 0) { + throw new Error(ERR_INSTALL_GUIDEPUP_MANIFEST_INVALID); + } + + return screenReaders.reduce>( + (accumulator, screenReader) => { + if (!screenReader || typeof screenReader !== "object") { + return accumulator; + } + + const screenReaderRecord = screenReader as Record; + const id = screenReaderRecord.id; + + if (typeof id === "string" && id.length > 0) { + accumulator[id] = screenReaderRecord as ManifestScreenReader; + } + + return accumulator; + }, + {}, + ); +} + +export function resolveGuidepupManifest(): Record { + let packageJsonPath: string; + + try { + packageJsonPath = require.resolve("@guidepup/guidepup/package.json", { + paths: [process.cwd()], + }); + } catch { + throw new Error(ERR_INSTALL_GUIDEPUP_PACKAGE_NOT_FOUND); + } + + const manifestPath = path.join( + path.dirname(packageJsonPath), + "manifest.json", + ); + + if (!existsSync(manifestPath)) { + throw new Error(ERR_INSTALL_GUIDEPUP_MANIFEST_INVALID); + } + + let manifest: unknown; + + try { + manifest = JSON.parse(readFileSync(manifestPath, "utf8")); + } catch { + throw new Error(ERR_INSTALL_GUIDEPUP_MANIFEST_INVALID); + } + + if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) { + throw new Error(ERR_INSTALL_GUIDEPUP_MANIFEST_INVALID); + } + + const manifestRecord = manifest as Record; + const schemaVersion = manifestRecord.version; + + if ( + typeof schemaVersion !== "number" || + !SUPPORTED_SCHEMA_VERSIONS.includes(schemaVersion) + ) { + throw new Error(ERR_INSTALL_GUIDEPUP_MANIFEST_UNSUPPORTED_SCHEMA); + } + + return manifestRecord; +} + +export function selectInstallTargets( + manifest: Record, + requestedScreenReader?: string, +): InstallTarget[] { + const screenReaders = getScreenReaders(manifest); + + console.log({ screenReaders }); + + const selectedNames = requestedScreenReader + ? [requestedScreenReader] + : Object.keys(screenReaders).filter((name) => { + const screenReader = screenReaders[name]; + + return ( + screenReader?.defaultDownload === true && + isCompatibleWithCurrentPlatform(screenReader) + ); + }); + + console.log({ selectedNames }); + + const targets = selectedNames + .map((name) => ({ + name, + data: screenReaders[name], + })) + .filter((target) => { + if (!target.data) { + return false; + } + + return isCompatibleWithCurrentPlatform(target.data); + }); + + console.log({ targets }); + + if (requestedScreenReader && targets.length === 0) { + throw new Error( + `${ERR_INSTALL_GUIDEPUP_SCREEN_READER_UNAVAILABLE}: ${requestedScreenReader}`, + ); + } + + return targets; +} diff --git a/src/commands/setup/index.ts b/src/commands/setup/index.ts new file mode 100644 index 0000000..1aaafec --- /dev/null +++ b/src/commands/setup/index.ts @@ -0,0 +1,46 @@ +import { platform } from "node:os"; +import { Command } from "commander"; +import { setup as setupMacOS } from "./macOS/setup"; +import { ERR_SETUP_UNSUPPORTED_OS } from "../../errors"; +import { handleSetupError, handleSetupComplete } from "../../logging"; + +interface SetupCommandOptions { + ci: boolean; + macosIgnoreTccDb: boolean; + macosRecord: boolean; +} + +async function setup(options: SetupCommandOptions): Promise { + const currentPlatform = platform(); + + try { + switch (currentPlatform) { + case "win32": { + break; + } + case "darwin": { + await setupMacOS(options); + + break; + } + default: { + throw new Error(ERR_SETUP_UNSUPPORTED_OS); + } + } + } catch (error) { + handleSetupError(error); + } + + handleSetupComplete(); +} + +export function setupCommand() { + return new Command("setup") + .description( + "Configure the local environment for screen reader automation.", + ) + .option("--ci", "Enable CI-specific behavior during setup.") + .option("--macos-ignore-tcc-db", "Skip macOS TCC database updates.") + .option("--macos-record", "Screen record the macOS setup.") + .action(setup); +} diff --git a/src/macOS/checkVersion.ts b/src/commands/setup/macOS/checkVersion.ts similarity index 71% rename from src/macOS/checkVersion.ts rename to src/commands/setup/macOS/checkVersion.ts index 0e071c3..75e730b 100644 --- a/src/macOS/checkVersion.ts +++ b/src/commands/setup/macOS/checkVersion.ts @@ -1,12 +1,12 @@ // Derived from https://github.com/sindresorhus/macos-version // MIT License Copyright (c) Sindre Sorhus -import { readFileSync } from "fs"; +import { readFileSync } from "node:fs"; import { satisfies } from "semver"; import { - ERR_MACOS_UNABLE_TO_VERIFY_VERSION, - ERR_MACOS_UNSUPPORTED_VERSION, -} from "../errors"; + ERR_SETUP_MACOS_UNABLE_TO_VERIFY_VERSION, + ERR_SETUP_MACOS_UNSUPPORTED_VERSION, +} from "../../../errors"; function clean(version: string): string { const { length } = version.split("."); @@ -23,18 +23,18 @@ function clean(version: string): string { export function checkVersion(): void { const plist = readFileSync( "/System/Library/CoreServices/SystemVersion.plist", - "utf8" + "utf8", ); const matches = /ProductVersion<\/key>\s*([\d.]+)<\/string>/.exec(plist); if (!matches) { - throw new Error(ERR_MACOS_UNABLE_TO_VERIFY_VERSION); + throw new Error(ERR_SETUP_MACOS_UNABLE_TO_VERIFY_VERSION); } const version = clean(matches[1].replace("10.16", "11")); if (!satisfies(version, ">=11.0.0")) { - throw new Error(ERR_MACOS_UNSUPPORTED_VERSION); + throw new Error(ERR_SETUP_MACOS_UNSUPPORTED_VERSION); } } diff --git a/src/macOS/disableDictationInputAutoEnable.ts b/src/commands/setup/macOS/disableDictationInputAutoEnable.ts similarity index 51% rename from src/macOS/disableDictationInputAutoEnable.ts rename to src/commands/setup/macOS/disableDictationInputAutoEnable.ts index e584799..8558788 100644 --- a/src/macOS/disableDictationInputAutoEnable.ts +++ b/src/commands/setup/macOS/disableDictationInputAutoEnable.ts @@ -1,15 +1,13 @@ import { execSync } from "child_process"; -import { ERR_MACOS_UNABLE_UPDATE_SYSTEM_DEFAULTS } from "../errors"; +import { ERR_SETUP_MACOS_UNABLE_UPDATE_SYSTEM_DEFAULTS } from "../../../errors"; export function disableDictationInputAutoEnable(): void { try { execSync( "defaults write com.apple.HIToolbox AppleDictationAutoEnable -bool false", - { encoding: "utf8" } - ); - } catch (e) { - throw new Error( - `${ERR_MACOS_UNABLE_UPDATE_SYSTEM_DEFAULTS}\n\n${e.message}` + { encoding: "utf8" }, ); + } catch (cause) { + throw new Error(ERR_SETUP_MACOS_UNABLE_UPDATE_SYSTEM_DEFAULTS, { cause }); } } diff --git a/src/macOS/disableSplashScreenSystemDefaults.ts b/src/commands/setup/macOS/disableSplashScreenSystemDefaults.ts similarity index 51% rename from src/macOS/disableSplashScreenSystemDefaults.ts rename to src/commands/setup/macOS/disableSplashScreenSystemDefaults.ts index 80ee8ff..f94edd5 100644 --- a/src/macOS/disableSplashScreenSystemDefaults.ts +++ b/src/commands/setup/macOS/disableSplashScreenSystemDefaults.ts @@ -1,15 +1,13 @@ import { execSync } from "child_process"; -import { ERR_MACOS_UNABLE_UPDATE_SYSTEM_DEFAULTS } from "../errors"; +import { ERR_SETUP_MACOS_UNABLE_UPDATE_SYSTEM_DEFAULTS } from "../../../errors"; export function disableSplashScreenSystemDefaults(): void { try { execSync( "defaults write com.apple.VoiceOverTraining doNotShowSplashScreen -bool true", - { encoding: "utf8" } - ); - } catch (e) { - throw new Error( - `${ERR_MACOS_UNABLE_UPDATE_SYSTEM_DEFAULTS}\n\n${e.message}` + { encoding: "utf8" }, ); + } catch (cause) { + throw new Error(ERR_SETUP_MACOS_UNABLE_UPDATE_SYSTEM_DEFAULTS, { cause }); } } diff --git a/src/macOS/enableAppleScriptControlSystemDefaults.ts b/src/commands/setup/macOS/enableAppleScriptControlSystemDefaults.ts similarity index 59% rename from src/macOS/enableAppleScriptControlSystemDefaults.ts rename to src/commands/setup/macOS/enableAppleScriptControlSystemDefaults.ts index 3b16cb3..9468d0f 100644 --- a/src/macOS/enableAppleScriptControlSystemDefaults.ts +++ b/src/commands/setup/macOS/enableAppleScriptControlSystemDefaults.ts @@ -1,5 +1,5 @@ import { execSync } from "child_process"; -import { ERR_MACOS_UNABLE_UPDATE_SYSTEM_DEFAULTS } from "../errors"; +import { ERR_SETUP_MACOS_UNABLE_UPDATE_SYSTEM_DEFAULTS } from "../../../errors"; export function enableAppleScriptControlSystemDefaults(): void { try { @@ -9,9 +9,7 @@ export function enableAppleScriptControlSystemDefaults(): void { ); return; - } catch (e) { - throw new Error( - `${ERR_MACOS_UNABLE_UPDATE_SYSTEM_DEFAULTS}\n\n${e.message}`, - ); + } catch (cause) { + throw new Error(ERR_SETUP_MACOS_UNABLE_UPDATE_SYSTEM_DEFAULTS, { cause }); } } diff --git a/src/macOS/enableDoNotDisturb.ts b/src/commands/setup/macOS/enableDoNotDisturb.ts similarity index 92% rename from src/macOS/enableDoNotDisturb.ts rename to src/commands/setup/macOS/enableDoNotDisturb.ts index b9e791f..4f059d7 100644 --- a/src/macOS/enableDoNotDisturb.ts +++ b/src/commands/setup/macOS/enableDoNotDisturb.ts @@ -1,6 +1,6 @@ import { exec } from "child_process"; import { promisify } from "util"; -import { ERR_MACOS_FAILED_TO_ENABLE_DO_NOT_DISTURB } from "../errors"; +import { ERR_SETUP_MACOS_FAILED_TO_ENABLE_DO_NOT_DISTURB } from "../../../errors"; import { runAppleScript } from "./runAppleScript"; import { retryOnError } from "./retryOnError"; import { getPlatformVersionMajor } from "./getPlatformVersionMajor"; @@ -103,21 +103,19 @@ export async function enableDoNotDisturb() { if (platformMajor <= 20) { await promisify(exec)(enableFocusModeShellscript); } else if (platformMajor === 21) { - // From MacOS 12 Monterey (Darwin 21) there is no known way to enable DND via system defaults + // From macOS 12 Monterey (Darwin 21) there is no known way to enable DND via system defaults await retryOnError(() => runAppleScript(enableFocusModeAppleScript)); } else { const { stdout: locale } = await promisify(exec)(getLocale); - // From MacOS 13 Ventura (Darwin 22) there is no known way to enable DND via system settings + // From macOS 13 Ventura (Darwin 22) there is no known way to enable DND via system settings await retryOnError(() => runAppleScript( enableFocusModeVenturaAppleScript(locale, platformMajor), ), ); } - } catch (e) { - throw new Error( - `${ERR_MACOS_FAILED_TO_ENABLE_DO_NOT_DISTURB}\n\n${e.message}`, - ); + } catch (cause) { + throw new Error(ERR_SETUP_MACOS_FAILED_TO_ENABLE_DO_NOT_DISTURB, { cause }); } } diff --git a/src/macOS/getPlatformVersionMajor.ts b/src/commands/setup/macOS/getPlatformVersionMajor.ts similarity index 100% rename from src/macOS/getPlatformVersionMajor.ts rename to src/commands/setup/macOS/getPlatformVersionMajor.ts diff --git a/src/macOS/isAppleScriptControlEnabled/enabledDbFile.ts b/src/commands/setup/macOS/isAppleScriptControlEnabled/enabledDbFile.ts similarity index 100% rename from src/macOS/isAppleScriptControlEnabled/enabledDbFile.ts rename to src/commands/setup/macOS/isAppleScriptControlEnabled/enabledDbFile.ts diff --git a/src/macOS/isAppleScriptControlEnabled/enabledDefaults.ts b/src/commands/setup/macOS/isAppleScriptControlEnabled/enabledDefaults.ts similarity index 100% rename from src/macOS/isAppleScriptControlEnabled/enabledDefaults.ts rename to src/commands/setup/macOS/isAppleScriptControlEnabled/enabledDefaults.ts diff --git a/src/macOS/isAppleScriptControlEnabled/index.ts b/src/commands/setup/macOS/isAppleScriptControlEnabled/index.ts similarity index 100% rename from src/macOS/isAppleScriptControlEnabled/index.ts rename to src/commands/setup/macOS/isAppleScriptControlEnabled/index.ts diff --git a/src/macOS/isSipEnabled.ts b/src/commands/setup/macOS/isSipEnabled.ts similarity index 60% rename from src/macOS/isSipEnabled.ts rename to src/commands/setup/macOS/isSipEnabled.ts index 469fbc6..885509f 100644 --- a/src/macOS/isSipEnabled.ts +++ b/src/commands/setup/macOS/isSipEnabled.ts @@ -1,13 +1,13 @@ import { execSync } from "child_process"; -import { ERR_MACOS_UNABLE_TO_VERIFY_SIP } from "../errors"; +import { ERR_SETUP_MACOS_UNABLE_TO_VERIFY_SIP } from "../../../errors"; export function isSipEnabled(): boolean { let commandResult: string; try { commandResult = execSync("csrutil status", { encoding: "utf8" }); - } catch (e) { - throw new Error(`${ERR_MACOS_UNABLE_TO_VERIFY_SIP}\n\n${e.message}`); + } catch (cause) { + throw new Error(ERR_SETUP_MACOS_UNABLE_TO_VERIFY_SIP, { cause }); } return commandResult.includes("enabled"); diff --git a/src/macOS/retryOnError.ts b/src/commands/setup/macOS/retryOnError.ts similarity index 100% rename from src/macOS/retryOnError.ts rename to src/commands/setup/macOS/retryOnError.ts diff --git a/src/macOS/runAppleScript.ts b/src/commands/setup/macOS/runAppleScript.ts similarity index 100% rename from src/macOS/runAppleScript.ts rename to src/commands/setup/macOS/runAppleScript.ts diff --git a/src/macOS/setup.ts b/src/commands/setup/macOS/setup.ts similarity index 69% rename from src/macOS/setup.ts rename to src/commands/setup/macOS/setup.ts index 3ec9d3f..9880503 100644 --- a/src/macOS/setup.ts +++ b/src/commands/setup/macOS/setup.ts @@ -1,5 +1,4 @@ -import { platform, release } from "os"; -import chalk from "chalk"; +import { platform, release } from "node:os"; import { checkVersion } from "./checkVersion"; import { enableAppleScriptControlSystemDefaults } from "./enableAppleScriptControlSystemDefaults"; import { disableSplashScreenSystemDefaults } from "./disableSplashScreenSystemDefaults"; @@ -8,21 +7,27 @@ import { isSipEnabled } from "./isSipEnabled"; import { writeDatabaseFile } from "./writeDatabaseFile"; import { SYSTEM_PATH, USER_PATH, updateTccDb } from "./updateTccDb"; import { isAppleScriptControlEnabled } from "./isAppleScriptControlEnabled"; -import { handleWarning, logInfo } from "../logging"; -import { ERR_MACOS_REQUIRES_MANUAL_USER_INTERACTION } from "../errors"; +import { handleSetupManualRequired, handleWarning } from "../../../logging"; +import { ERR_SETUP_MACOS_REQUIRES_MANUAL_USER_INTERACTION } from "../../../errors"; import { enableDoNotDisturb } from "./enableDoNotDisturb"; import { enabledDbFile } from "./isAppleScriptControlEnabled/enabledDbFile"; -const isCi = process.argv.includes("--ci"); -const ignoreTccDb = process.argv.includes("--ignore-tcc-db"); -const isRecorded = process.argv.includes("--record"); +interface MacOSSetupOptions { + ci?: boolean; + macosIgnoreTccDb?: boolean; + macosRecord?: boolean; +} -export async function setup(): Promise { - if (!ignoreTccDb) { +export async function setup({ + ci = false, + macosIgnoreTccDb = false, + macosRecord = false, +}: MacOSSetupOptions = {}): Promise { + if (!macosIgnoreTccDb) { try { updateTccDb(USER_PATH); } catch (e) { - if (isCi) { + if (ci) { throw e; } } @@ -34,7 +39,7 @@ export async function setup(): Promise { } } else { handleWarning( - "Ignoring TCC.db updates", + "Ignoring TCC database updates", "If the necessary permissions have not been granted by other means, using this flag may result in your environment not being set up for reliable screen reader automation.", ); } @@ -44,7 +49,7 @@ export async function setup(): Promise { let stopRecording: () => void = () => null; - if (isRecorded) { + if (macosRecord) { try { const { macOSRecord } = await import("@guidepup/record"); @@ -54,7 +59,7 @@ export async function setup(): Promise { } catch { handleWarning( "@guidepup/record not available", - "Recording will be skipped. This is expected on platforms without ffmpeg support (e.g., Windows ARM64).", + "Recording will be skipped. This is expected on platforms without ffmpeg support.", ); } } @@ -65,7 +70,7 @@ export async function setup(): Promise { disableSplashScreenSystemDefaults(); disableDictationInputAutoEnable(); - if (isCi) { + if (ci) { await enableDoNotDisturb(); } @@ -79,18 +84,11 @@ export async function setup(): Promise { return; } - if (isCi) { - throw new Error(ERR_MACOS_REQUIRES_MANUAL_USER_INTERACTION); + if (ci) { + throw new Error(ERR_SETUP_MACOS_REQUIRES_MANUAL_USER_INTERACTION); } - logInfo( - "Please complete remaining setup by following this guide:\n\n--> " + - chalk.underline( - chalk.bold( - "https://www.guidepup.dev/docs/guides/manual-voiceover-setup", - ), - ), - ); + handleSetupManualRequired(); } finally { stopRecording(); } diff --git a/src/macOS/updateTccDb.ts b/src/commands/setup/macOS/updateTccDb.ts similarity index 97% rename from src/macOS/updateTccDb.ts rename to src/commands/setup/macOS/updateTccDb.ts index 85ba72c..f63d546 100644 --- a/src/macOS/updateTccDb.ts +++ b/src/commands/setup/macOS/updateTccDb.ts @@ -1,6 +1,6 @@ import { release } from "os"; import { execSync } from "child_process"; -import { ERR_MACOS_UNABLE_TO_WRITE_USER_TCC_DB } from "../errors"; +import { ERR_SETUP_MACOS_UNABLE_TO_WRITE_USER_TCC_DB } from "../../../errors"; const epoch = +Date.now(); @@ -197,10 +197,8 @@ export function updateTccDb(path: string): void { execSync(`sqlite3 "${path}" "${query}" >/dev/null 2>&1`, { encoding: "utf8", }); - } catch (e) { - throw new Error( - `${ERR_MACOS_UNABLE_TO_WRITE_USER_TCC_DB}\n\n${e.message}`, - ); + } catch (cause) { + throw new Error(ERR_SETUP_MACOS_UNABLE_TO_WRITE_USER_TCC_DB, { cause }); } } } diff --git a/src/commands/setup/macOS/writeDatabaseFile.ts b/src/commands/setup/macOS/writeDatabaseFile.ts new file mode 100644 index 0000000..73e413a --- /dev/null +++ b/src/commands/setup/macOS/writeDatabaseFile.ts @@ -0,0 +1,13 @@ +import { writeFileSync } from "fs"; +import { ERR_SETUP_MACOS_UNABLE_TO_WRITE_DATABASE_FILE } from "../../../errors"; + +export function writeDatabaseFile(): void { + try { + writeFileSync( + "/private/var/db/Accessibility/.VoiceOverAppleScriptEnabled", + "a", + ); + } catch (cause) { + throw new Error(ERR_SETUP_MACOS_UNABLE_TO_WRITE_DATABASE_FILE, { cause }); + } +} diff --git a/src/errors.ts b/src/errors.ts index feece95..fb649a2 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,31 +1,28 @@ -export const ERR_UNSUPPORTED_OS = +export const ERR_SETUP_UNSUPPORTED_OS = "Unsupported OS - consider contributing to Guidepup?"; -export const ERR_MACOS_UNABLE_TO_VERIFY_VERSION = +export const ERR_SETUP_MACOS_UNABLE_TO_VERIFY_VERSION = "Unable to verify macOS version"; -export const ERR_MACOS_UNSUPPORTED_VERSION = +export const ERR_SETUP_MACOS_UNSUPPORTED_VERSION = "Require macOS version 11 or later"; -export const ERR_MACOS_UNABLE_UPDATE_SYSTEM_DEFAULTS = +export const ERR_SETUP_MACOS_UNABLE_UPDATE_SYSTEM_DEFAULTS = "Unable to update system defaults"; -export const ERR_MACOS_UNABLE_TO_VERIFY_SIP = +export const ERR_SETUP_MACOS_UNABLE_TO_VERIFY_SIP = "Unable to verify macOS SIP status"; -export const ERR_MACOS_UNABLE_TO_WRITE_DATABASE_FILE = - "Unable to write to the VoiceOver database file - SIP might not be disabled"; -export const ERR_MACOS_UNABLE_TO_WRITE_USER_TCC_DB = - "Unable to write to the user TCC.db - SIP might not be disabled"; -export const ERR_MACOS_UI_PROMPT_FAILURE = - "Unable to retrieve user input from prompt"; -export const ERR_MACOS_UI_CONTROL_NOT_CONSENTED = - "Consent not given for UI control"; -export const ERR_MACOS_REQUIRES_MANUAL_USER_INTERACTION = - "Unable to setup environment without manual user interaction"; -export const ERR_MACOS_FAILED_TO_ENABLE_DO_NOT_DISTURB = +export const ERR_SETUP_MACOS_UNABLE_TO_WRITE_DATABASE_FILE = + "Unable to write to the VoiceOver database file\n\nEnsure that SIP is disabled or pass '--macos-ignore-tcc-db' to ignore TCC database updates"; +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 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_FAILED_TO_ENABLE_DO_NOT_DISTURB = 'Failed to enable "Do not disturb" mode'; -export const ERR_WINDOWS_UNABLE_TO_ACCESS_REGISTRY = - "Unable to access Windows registry"; -export const ERR_WINDOWS_UNABLE_TO_UPDATE_REGISTRY = - "Unable to update Windows registry"; -export const ERR_WINDOWS_FAILED_TO_INSTALL_NVDA = "Unable to install NVDA"; -export const ERR_WINDOWS_FAILED_TO_RESTART_EXPLORER = - "Unable to restart explorer.exe"; +export const ERR_INSTALL_GUIDEPUP_PACKAGE_NOT_FOUND = + "No local installation of '@guidepup/guidepup' was found\n\nPlease install '@guidepup/guidepup' first before running the CLI install"; +export const ERR_INSTALL_GUIDEPUP_MANIFEST_INVALID = + "The installed '@guidepup/guidepup' manifest appears to be invalid or corrupted\n\nPlease re-install '@guidepup/guidepup'"; +export const ERR_INSTALL_GUIDEPUP_MANIFEST_UNSUPPORTED_SCHEMA = + "Unsupported manifest schema version\n\nPlease upgrade '@guidepup/setup'"; +export const ERR_INSTALL_GUIDEPUP_SCREEN_READER_UNAVAILABLE = + "The requested screen reader is not available for this platform"; diff --git a/src/index.ts b/src/index.ts index 39276aa..c8f2dc1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,28 +1,25 @@ -import { setup as setupMacOS } from "./macOS/setup"; -import { setup as setupWindows } from "./windows/setup"; -import { handleError, handleComplete } from "./logging"; -import { ERR_UNSUPPORTED_OS } from "./errors"; +import { Command } from "commander"; +import { setupCommand } from "./commands/setup"; +import { installCommand } from "./commands/install"; -async function run(): Promise { - try { - switch (process.platform) { - case "darwin": { - await setupMacOS(); - break; - } - case "win32": { - await setupWindows(); - break; - } - default: { - throw new Error(ERR_UNSUPPORTED_OS); - } - } - } catch (e) { - handleError(e); - } +// eslint-disable-next-line @typescript-eslint/no-require-imports +const { version } = require("../package.json"); - handleComplete(); +function createProgram(): Command { + const program = new Command(); + + program + .name("guidepup") + .description( + "Configure the local environment and manage screen readers for Guidepup.", + ) + .version(version, "-v, --version", "Display the CLI version."); + + program.addCommand(setupCommand()); + + program.addCommand(installCommand()); + + return program; } -run(); +createProgram().parseAsync(process.argv); diff --git a/src/logging.ts b/src/logging.ts index fcf1c0f..5fe5d00 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -4,7 +4,15 @@ export const logInfo = console.info.bind(console); export const logWarn = console.warn.bind(console); export const logError = console.error.bind(console); -export function handleComplete(): never { +export function handleInstallComplete(): never { + logInfo(""); + logInfo(chalk.green("Installation complete 🎉")); + logInfo(""); + + process.exit(0); +} + +export function handleSetupComplete(): never { logInfo(""); logInfo(chalk.green("Environment setup complete 🎉")); logInfo(""); @@ -12,6 +20,17 @@ export function handleComplete(): never { process.exit(0); } +export function handleSetupManualRequired(): void { + logInfo( + "Please complete remaining setup by following this guide:\n\n--> " + + chalk.underline( + chalk.bold( + "https://www.guidepup.dev/docs/guides/manual-voiceover-setup", + ), + ), + ); +} + export function handleWarning(title: string, subtitle: string): void { logWarn(""); logWarn(chalk.bold(chalk.yellow(`[!] Warning: ${chalk.bold(title)}`))); @@ -20,7 +39,30 @@ export function handleWarning(title: string, subtitle: string): void { logError(""); } -export function handleError(err: Error): never { +export function handleInstallError(err: Error): never { + let message = err.message; + + if (err.name) { + message = `${err.name}: ${message}`; + } + + logError(""); + logError(chalk.bold(chalk.red(`[!] ${chalk.bold(message.toString())}`))); + logError(""); + logError("Unable to complete screen reader installation"); + logError(""); + logError( + chalk.dim( + "Please raise new issues at: " + + chalk.underline("https://github.com/guidepup/setup/issues"), + ), + ); + logError(""); + + process.exit(1); +} + +export function handleSetupError(err: Error): never { let message = err.message; if (err.name) { @@ -35,8 +77,8 @@ export function handleError(err: Error): never { logError( chalk.dim( "Please raise new issues at: " + - chalk.underline("https://github.com/guidepup/setup/issues") - ) + chalk.underline("https://github.com/guidepup/setup/issues"), + ), ); logError(""); diff --git a/src/macOS/writeDatabaseFile.ts b/src/macOS/writeDatabaseFile.ts deleted file mode 100644 index e73b0c1..0000000 --- a/src/macOS/writeDatabaseFile.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { writeFileSync } from "fs"; -import { ERR_MACOS_UNABLE_TO_WRITE_DATABASE_FILE } from "../errors"; - -export function writeDatabaseFile(): void { - try { - writeFileSync( - "/private/var/db/Accessibility/.VoiceOverAppleScriptEnabled", - "a" - ); - } catch (e) { - throw new Error(`${ERR_MACOS_UNABLE_TO_WRITE_DATABASE_FILE}\n\n${e.message}`); - } -} diff --git a/src/windows/constants.ts b/src/windows/constants.ts deleted file mode 100644 index bb03920..0000000 --- a/src/windows/constants.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -/* eslint-disable @typescript-eslint/no-require-imports */ - -const { guidepupNvdaVersion } = require("../../package.json"); - -export const GUIDEPUP_NVDA_VERSION = guidepupNvdaVersion; -export const SUB_KEY_GUIDEPUP_NVDA = "HKCU\\Software\\Guidepup\\Nvda"; -export const VERSIONED_KEY = `guidepup_nvda_${GUIDEPUP_NVDA_VERSION}`; - -export const SUB_KEY_CONTROL_PANEL_DESKTOP = "HKCU\\Control Panel\\Desktop"; -export const FOREGROUND_LOCK_TIMEOUT_KEY = "ForegroundLockTimeout"; -export const FOREGROUND_FLASH_COUNT_KEY = "ForegroundFlashCount"; diff --git a/src/windows/createNvdaRegistryKey.ts b/src/windows/createNvdaRegistryKey.ts deleted file mode 100644 index 9232615..0000000 --- a/src/windows/createNvdaRegistryKey.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { promisified as regedit } from "regedit"; -import { SUB_KEY_GUIDEPUP_NVDA } from "./constants"; -import { ERR_WINDOWS_UNABLE_TO_UPDATE_REGISTRY } from "../errors"; - -export async function createNvdaRegistryKey() { - try { - await regedit.createKey([SUB_KEY_GUIDEPUP_NVDA]); - } catch (e) { - throw new Error(`${ERR_WINDOWS_UNABLE_TO_UPDATE_REGISTRY}\n\n${e.message}`); - } -} diff --git a/src/windows/getNvdaRegistryData.ts b/src/windows/getNvdaRegistryData.ts deleted file mode 100644 index 533f4be..0000000 --- a/src/windows/getNvdaRegistryData.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { promisified as regedit } from "regedit"; -import { SUB_KEY_GUIDEPUP_NVDA } from "./constants"; -import { ERR_WINDOWS_UNABLE_TO_ACCESS_REGISTRY } from "../errors"; - -export async function getNvdaRegistryData() { - try { - const { - [SUB_KEY_GUIDEPUP_NVDA]: { exists, values }, - } = await regedit.list([SUB_KEY_GUIDEPUP_NVDA]); - - return { exists, values }; - } catch (e) { - throw new Error(`${ERR_WINDOWS_UNABLE_TO_ACCESS_REGISTRY}\n\n${e.message}`); - } -} diff --git a/src/windows/installNvda.ts b/src/windows/installNvda.ts deleted file mode 100644 index 1407aa3..0000000 --- a/src/windows/installNvda.ts +++ /dev/null @@ -1,88 +0,0 @@ -import decompress from "decompress"; -import { get } from "https"; -import { createWriteStream, mkdirSync, mkdtempSync, rmSync } from "fs"; -import { join } from "path"; -import { tmpdir } from "os"; -import { ERR_WINDOWS_FAILED_TO_INSTALL_NVDA } from "../errors"; -import { GUIDEPUP_NVDA_VERSION } from "./constants"; -import { HttpsProxyAgent } from 'https-proxy-agent'; - -const appName = "guidepup_nvda"; -const sourceUrl = `https://codeload.github.com/guidepup/nvda/zip/refs/tags/${GUIDEPUP_NVDA_VERSION}`; - -export async function installNvda({ - userProvidedInstallDirectory, -}: { - userProvidedInstallDirectory: string; -}): Promise { - if (userProvidedInstallDirectory) { - mkdirSync(userProvidedInstallDirectory, { recursive: true }); - } - - const destinationBaseDirectory = - userProvidedInstallDirectory ?? mkdtempSync(join(tmpdir(), `${appName}_`)); - const destinationZip = join(destinationBaseDirectory, `${appName}.zip`); - const fileZip = createWriteStream(destinationZip); - - function removeAll() { - try { - rmSync(destinationBaseDirectory, { recursive: true }); - } catch { - // swallow - } - } - - function removeZip() { - try { - rmSync(destinationZip, { recursive: true }); - } catch { - // swallow - } - } - - let agent: HttpsProxyAgent | undefined - - const proxyUrl = ( - process.env.HTTPS_PROXY || - process.env.https_proxy || - process.env.HTTP_PROXY || - process.env.http_proxy - ) - - if (proxyUrl) { - agent = new HttpsProxyAgent(proxyUrl) - } - - try { - await new Promise((resolve, reject) => { - function onSuccess() { - fileZip.close((error) => { - if (error) { - return reject(error); - } - - resolve(); - }); - } - - const request = get(sourceUrl, { agent }, (response) => response.pipe(fileZip)); - request.on("error", reject); - fileZip.on("finish", onSuccess); - fileZip.on("error", reject); - }); - - await decompress(destinationZip, destinationBaseDirectory); - - removeZip(); - } catch (e) { - removeAll(); - - throw new Error(`${ERR_WINDOWS_FAILED_TO_INSTALL_NVDA}\n\n${e.message}`); - } - - return join( - destinationBaseDirectory, - `nvda-${GUIDEPUP_NVDA_VERSION}`, - "nvda" - ); -} diff --git a/src/windows/isNvdaInstalled.ts b/src/windows/isNvdaInstalled.ts deleted file mode 100644 index c2b67a6..0000000 --- a/src/windows/isNvdaInstalled.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { existsSync } from "fs"; -import { VERSIONED_KEY } from "./constants"; -import { RegistryItemValue } from "regedit"; - -export function isNvdaInstalled({ - exists, - userProvidedInstallDirectory, - values, -}: { - exists: boolean; - userProvidedInstallDirectory: string | null; - values: { - [name: string]: RegistryItemValue; - }; -}) { - if (!exists) { - return false; - } - - const path = values[VERSIONED_KEY]?.value as string; - - if (!path) { - return false; - } - - if (userProvidedInstallDirectory !== path) { - return false; - } - - return existsSync(path); -} diff --git a/src/windows/removeForegroundLock.ts b/src/windows/removeForegroundLock.ts deleted file mode 100644 index 1113fc2..0000000 --- a/src/windows/removeForegroundLock.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { promisified as regedit } from "regedit"; -import { - SUB_KEY_CONTROL_PANEL_DESKTOP, - FOREGROUND_LOCK_TIMEOUT_KEY, - FOREGROUND_FLASH_COUNT_KEY, -} from "./constants"; -import { ERR_WINDOWS_UNABLE_TO_UPDATE_REGISTRY } from "../errors"; - -export async function removeForegroundLock() { - try { - await regedit.putValue({ - [SUB_KEY_CONTROL_PANEL_DESKTOP]: { - [FOREGROUND_LOCK_TIMEOUT_KEY]: { - value: 0, - type: "REG_DWORD", - }, - }, - }); - await regedit.putValue({ - [SUB_KEY_CONTROL_PANEL_DESKTOP]: { - [FOREGROUND_FLASH_COUNT_KEY]: { - value: 0, - type: "REG_DWORD", - }, - }, - }); - } catch (e) { - throw new Error(`${ERR_WINDOWS_UNABLE_TO_UPDATE_REGISTRY}\n\n${e.message}`); - } -} diff --git a/src/windows/restartExplorer.ts b/src/windows/restartExplorer.ts deleted file mode 100644 index 409647c..0000000 --- a/src/windows/restartExplorer.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ERR_WINDOWS_FAILED_TO_RESTART_EXPLORER } from "../errors"; -import { runVbsScript } from "./runVbsScript"; - -export async function restartExplorer() { - const script = ` -SET objWMIService = GetObject("winmgmts:{impersonationLevel=impersonate}!" & chr(92) & chr(92) & "." & chr(92) & "root" & chr(92) & "cimv2") -SET colProcesses = objWMIService.ExecQuery("Select * from Win32_Process where Name = 'explorer.exe'") - -FOR EACH objProcess in colProcesses - objProcess.Terminate(1) -NEXT - -SET objWMIService = Nothing -SET colProcesses = Nothing - -SET WshShell = CreateObject("WScript.Shell") -WshShell.Run "explorer.exe" -SET WshShell = Nothing -`; - - try { - await runVbsScript(script); - } catch (e) { - throw new Error(`${ERR_WINDOWS_FAILED_TO_RESTART_EXPLORER}\n\n${e.message}`); - } -} diff --git a/src/windows/runVbsScript.ts b/src/windows/runVbsScript.ts deleted file mode 100644 index 8565f91..0000000 --- a/src/windows/runVbsScript.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { join, sep } from "path"; -import { mkdtemp, realpath, rm, writeFile } from "fs/promises"; -import { execFile } from "child_process"; -import { tmpdir } from "os"; - -const DEFAULT_MAX_BUFFER = 1000 * 1000 * 100; -const CSCRIPT = "cscript"; - -const withTempFile = async (fn) => - await withTempDir((dir: string) => fn(join(dir, "script.vbs"))); - -const withTempDir = async (fn) => { - const dir = await mkdtemp((await realpath(tmpdir())) + sep); - - try { - return await fn(dir); - } finally { - await rm(dir, { recursive: true }); - } -}; - -export async function runVbsScript(script: string): Promise { - return await withTempFile(async (filePath: string) => { - await writeFile(filePath, script); - - return new Promise((resolve, reject) => { - const child = execFile( - CSCRIPT, - [filePath], - { - maxBuffer: DEFAULT_MAX_BUFFER, - }, - (e, stdout) => { - if (e) { - return reject(e); - } - - if (!stdout) { - return resolve(); - } else { - return resolve(stdout.trim()); - } - } - ); - - child.stdin.end(); - }); - }); -} diff --git a/src/windows/setup.ts b/src/windows/setup.ts deleted file mode 100644 index f4cb484..0000000 --- a/src/windows/setup.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { getNvdaRegistryData } from "./getNvdaRegistryData"; -import { isNvdaInstalled } from "./isNvdaInstalled"; -import { createNvdaRegistryKey } from "./createNvdaRegistryKey"; -import { installNvda } from "./installNvda"; -import { updateNvdaRegistryData } from "./updateNvdaRegistryData"; -import { removeForegroundLock } from "./removeForegroundLock"; -import { restartExplorer } from "./restartExplorer"; -import { resolve } from "path"; - -export async function setup(): Promise { - const userProvidedInstallDirectoryFlagIndex = - process.argv.indexOf("--nvda-install-dir"); - const userProvidedInstallDirectoryRaw = - userProvidedInstallDirectoryFlagIndex > -1 - ? process.argv.at(userProvidedInstallDirectoryFlagIndex + 1) ?? null - : null; - const userProvidedInstallDirectory = userProvidedInstallDirectoryRaw - ? resolve(userProvidedInstallDirectoryRaw) - : null; - - const { exists, values } = await getNvdaRegistryData(); - - if (isNvdaInstalled({ exists, userProvidedInstallDirectory, values })) { - return; - } - - if (!exists) { - await createNvdaRegistryKey(); - } - - const nvdaDirectory = await installNvda({ userProvidedInstallDirectory }); - - await updateNvdaRegistryData({ nvdaDirectory }); - await removeForegroundLock(); - await restartExplorer(); -} diff --git a/src/windows/updateNvdaRegistryData.ts b/src/windows/updateNvdaRegistryData.ts deleted file mode 100644 index b257dc2..0000000 --- a/src/windows/updateNvdaRegistryData.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { promisified as regedit } from "regedit"; -import { SUB_KEY_GUIDEPUP_NVDA, VERSIONED_KEY } from "./constants"; -import { ERR_WINDOWS_UNABLE_TO_UPDATE_REGISTRY } from "../errors"; - -export async function updateNvdaRegistryData({ nvdaDirectory }) { - try { - await regedit.putValue({ - [SUB_KEY_GUIDEPUP_NVDA]: { - [VERSIONED_KEY]: { - value: nvdaDirectory, - type: "REG_SZ", - }, - }, - }); - } catch (e) { - throw new Error(`${ERR_WINDOWS_UNABLE_TO_UPDATE_REGISTRY}\n\n${e.message}`); - } -} diff --git a/yarn.lock b/yarn.lock index 5758c53..7863f63 100644 --- a/yarn.lock +++ b/yarn.lock @@ -455,6 +455,11 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +commander@^15.0.0: + version "15.0.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-15.0.0.tgz#96f3961f12adac1799ef3fbd8bc61d40572d1b11" + integrity sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg== + commander@^2.8.1: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"