File size: 6,809 Bytes
ac68ae4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
import * as p from "@clack/prompts";
import pc from "picocolors";
import { runInherit } from "./exec.js";
import { detectCapabilities } from "./platform.js";
import { detectPython, type DetectedPython } from "./python.js";
import { askConfirm, pickOne } from "./prompts.js";

interface PythonOption {
  value: string;
  label: string;
  command?: { bin: string; args: string[] };
  manualInstructions?: string;
  openUrl?: string;
}

const MAX_RETRIES = 3;

async function openUrl(url: string): Promise<void> {
  const cmd = process.platform === "darwin"
    ? { bin: "open", args: [url] }
    : process.platform === "win32"
      ? { bin: "cmd", args: ["/c", "start", "", url] }
      : { bin: "xdg-open", args: [url] };
  await runInherit(cmd.bin, cmd.args);
}

async function buildOptions(): Promise<PythonOption[]> {
  const caps = await detectCapabilities();
  const options: PythonOption[] = [];

  if (caps.platform === "darwin") {
    if (caps.hasBrew) {
      options.push({
        value: "brew",
        label: "Install via Homebrew (brew install python@3.12)",
        command: { bin: "brew", args: ["install", "python@3.12"] },
      });
    }
    options.push({
      value: "pyenv-mac",
      label: caps.hasPyenv
        ? "Install via pyenv (pyenv install 3.12)"
        : "Install pyenv + Python 3.12 (recommended for multiple versions)",
      manualInstructions: caps.hasPyenv
        ? "pyenv install 3.12.7\npyenv global 3.12.7"
        : "brew install pyenv\npyenv install 3.12.7\npyenv global 3.12.7",
    });
    options.push({
      value: "python-org-mac",
      label: "Open python.org downloads in browser",
      openUrl: "https://www.python.org/downloads/macos/",
    });
  } else if (caps.platform === "linux") {
    for (const pm of caps.linuxPackageManagers) {
      const installArgs = pm === "pacman"
        ? ["sudo", pm, "-S", "--noconfirm", "python"]
        : pm === "apk"
          ? ["sudo", pm, "add", "python3", "py3-pip"]
          : ["sudo", pm, "install", "-y", "python3.12", "python3.12-venv", "python3-pip"];
      options.push({
        value: `pm-${pm}`,
        label: `Install via ${pm} (${installArgs.slice(1).join(" ")})`,
        command: { bin: installArgs[0]!, args: installArgs.slice(1) },
      });
    }
    if (caps.hasPyenv) {
      options.push({
        value: "pyenv-linux",
        label: "Install via pyenv (pyenv install 3.12)",
        manualInstructions: "pyenv install 3.12.7\npyenv global 3.12.7",
      });
    }
    options.push({
      value: "python-org-linux",
      label: "Open python.org downloads in browser",
      openUrl: "https://www.python.org/downloads/source/",
    });
  } else if (caps.platform === "win32") {
    if (caps.hasWinget) {
      options.push({
        value: "winget",
        label: "Install via winget (winget install Python.Python.3.12)",
        command: { bin: "winget", args: ["install", "Python.Python.3.12"] },
      });
    }
    if (caps.hasChoco) {
      options.push({
        value: "choco",
        label: "Install via Chocolatey (choco install python --version=3.12)",
        command: { bin: "choco", args: ["install", "python", "--version=3.12"] },
      });
    }
    options.push({
      value: "python-org-win",
      label: "Open python.org downloads in browser",
      openUrl: "https://www.python.org/downloads/windows/",
    });
  }

  options.push({
    value: "manual",
    label: "I'll install it manually — wait and retry",
  });
  options.push({
    value: "cancel",
    label: "Cancel — I'll re-run `brainapi init` later",
  });

  return options;
}

function manualInstructionsFor(): string {
  if (process.platform === "darwin") {
    return [
      "# Option A: Homebrew",
      "brew install python@3.12",
      "",
      "# Option B: pyenv (recommended if you juggle Python versions)",
      "brew install pyenv",
      "pyenv install 3.12.7",
      "pyenv global 3.12.7",
      "",
      "# Option C: Download installer",
      "open https://www.python.org/downloads/macos/",
    ].join("\n");
  }
  if (process.platform === "linux") {
    return [
      "# Debian/Ubuntu",
      "sudo apt-get update && sudo apt-get install -y python3.12 python3.12-venv python3-pip",
      "",
      "# Fedora/RHEL",
      "sudo dnf install -y python3.12",
      "",
      "# Arch",
      "sudo pacman -S python",
      "",
      "# Or via pyenv",
      "curl https://pyenv.run | bash",
      "pyenv install 3.12.7 && pyenv global 3.12.7",
    ].join("\n");
  }
  return [
    "winget install Python.Python.3.12",
    "",
    "# Or download:",
    "https://www.python.org/downloads/windows/",
  ].join("\n");
}

export async function recoverPython(): Promise<DetectedPython> {
  let attempt = 0;
  while (attempt < MAX_RETRIES) {
    attempt += 1;
    p.log.warn(
      attempt === 1
        ? "Python 3.11+ was not found on your PATH."
        : `Still no Python 3.11+ detected (attempt ${attempt}/${MAX_RETRIES}).`,
    );
    const options = await buildOptions();
    const choice = await pickOne<string>({
      message: "How would you like to install Python?",
      options: options.map((o) => ({ value: o.value, label: o.label })),
    });
    const selected = options.find((o) => o.value === choice);
    if (!selected) continue;

    if (selected.value === "cancel") {
      p.cancel("Setup cancelled. Re-run `brainapi init` once Python is installed.");
      process.exit(1);
    }

    if (selected.openUrl) {
      await openUrl(selected.openUrl);
    } else if (selected.command) {
      const cmdStr = `${selected.command.bin} ${selected.command.args.join(" ")}`;
      p.note(cmdStr, "Command to run");
      const confirmRun = await askConfirm({
        message: "Run this for me now?",
        initialValue: true,
      });
      if (confirmRun) {
        await runInherit(selected.command.bin, selected.command.args);
      }
    } else if (selected.value === "manual") {
      p.note(manualInstructionsFor(), "Manual install — copy and run");
    } else if (selected.manualInstructions) {
      p.note(selected.manualInstructions, "Copy and run");
    }

    await askConfirm({
      message: "Press enter when Python is installed and on your PATH",
      initialValue: true,
    });

    const detected = await detectPython();
    if (detected) {
      p.log.success(
        `Found ${pc.cyan(detected.bin)} (Python ${detected.version.join(".")})`,
      );
      return detected;
    }
  }

  p.cancel(
    "Could not detect Python 3.11+ after several attempts. Open a fresh shell so PATH refreshes, then re-run `brainapi init`. Any cloned source is preserved.",
  );
  process.exit(1);
}

export async function ensurePython(): Promise<DetectedPython> {
  const detected = await detectPython();
  if (detected) return detected;
  return recoverPython();
}