1import spawnAsync from '@expo/spawn-async';
2import chalk from 'chalk';
3import { execSync } from 'child_process';
4
5import { delayAsync } from '../../../utils/delay';
6import { AbortCommandError } from '../../../utils/errors';
7import { confirmAsync } from '../../../utils/prompts';
8import { Prerequisite } from '../Prerequisite';
9
10async function isXcrunInstalledAsync() {
11  try {
12    execSync('xcrun --version', { stdio: 'ignore' });
13    return true;
14  } catch {
15    return false;
16  }
17}
18
19export class XcrunPrerequisite extends Prerequisite {
20  static instance = new XcrunPrerequisite();
21
22  /**
23   * Ensure Xcode CLI is installed.
24   */
25  async assertImplementation(): Promise<void> {
26    if (await isXcrunInstalledAsync()) {
27      // Run this second to ensure the Xcode version check is run.
28      return;
29    }
30
31    async function pendingAsync(): Promise<void> {
32      if (!(await isXcrunInstalledAsync())) {
33        await delayAsync(100);
34        return await pendingAsync();
35      }
36    }
37
38    // This prompt serves no purpose accept informing the user what to do next, we could just open the App Store but it could be confusing if they don't know what's going on.
39    const confirm = await confirmAsync({
40      initial: true,
41      message: chalk`Xcode {bold Command Line Tools} needs to be installed (requires {bold sudo}), continue?`,
42    });
43
44    if (confirm) {
45      try {
46        await spawnAsync('sudo', [
47          'xcode-select',
48          '--install',
49          // TODO: Is there any harm in skipping this?
50          // '--switch', '/Applications/Xcode.app'
51        ]);
52        // Most likely the user will cancel the process, but if they don't this will continue checking until the CLI is available.
53        return await pendingAsync();
54      } catch {
55        // TODO: Figure out why this might get called (cancel early, network issues, server problems)
56        // TODO: Handle me
57      }
58    }
59
60    throw new AbortCommandError();
61  }
62}
63