import chalk from "chalk"; import { $q, commandExists, isFile, readFile } from "complete-node"; import path from "node:path"; import yaml from "yaml"; import { HOME_DIR, PROJECT_NAME, PROJECT_VERSION } from "./constants.js"; import type { GitHubCLIHostsYAML } from "./interfaces/GitHubCLIHostsYAML.js"; import { getInputString, getInputYesNo, promptLog } from "./prompt.js"; /** * If the GitHub CLI is installed, we can derive the user's GitHub username from their YAML * configuration. */ export async function getGitHubUsername(): Promise { const ghExists = await commandExists("gh"); if (!ghExists) { return undefined; } const githubCLIHostsPath = getGithubCLIHostsPath(); if (githubCLIHostsPath === undefined) { return undefined; } const hostsPathExists = await isFile(githubCLIHostsPath); if (!hostsPathExists) { return undefined; } const configYAMLRaw = await readFile(githubCLIHostsPath); const configYAML = yaml.parse(configYAMLRaw) as GitHubCLIHostsYAML; const githubCom = configYAML["github.com"]; if (githubCom === undefined) { return undefined; } const { user } = githubCom; if (user === undefined || user === "") { return undefined; } return user; } function getGithubCLIHostsPath(): string | undefined { if (process.platform === "win32") { const appData = process.env["APPDATA"]; if (appData === undefined || appData === "") { return undefined; } return path.join(appData, "GitHub CLI", "hosts.yml"); } // The location is the same on both macOS and Linux. return path.join(HOME_DIR, ".config", "gh", "hosts.yml"); } // eslint-disable-next-line unicorn/comment-content /** @returns The Git remote URL. For example: git@github.com:alice/foo.git */ export async function promptGitHubRepoOrGitRemoteURL( projectName: string, yes: boolean, skipGit: boolean, ): Promise { if (skipGit) { return undefined; } // Hard-code certain project names as never causing a Git repository to be initialized. if (projectName.startsWith("test") || projectName === "foo") { return undefined; } // We do not need to prompt the user if they do not have Git installed. const gitExists = await commandExists("git"); if (!gitExists) { promptLog( 'Git does not seem to be installed. (The "git" command is not in the path.) Skipping Git-related things.', ); return undefined; } const gitHubUsername = await getGitHubUsername(); if (gitHubUsername === undefined) { const gitRemoteURL = await getInputString( `Paste in the remote Git URL for your project. For example, if you have an SSH key, it would be something like: ${chalk.green("git@github.com:Alice/some-repository.git")} If you do not have an SSH key, it would be something like: ${chalk.green("https://github.com/Alice/some-repository.git")} If you do not want to initialize a Git repository for this project, press enter to skip.`, undefined, true, ); return gitRemoteURL === "" ? undefined : gitRemoteURL; } const url = `https://github.com/${gitHubUsername}/${projectName}`; let gitHubRepositoryExists = true; try { await $q`gh repo view ${projectName}`; } catch { gitHubRepositoryExists = false; } if (gitHubRepositoryExists) { promptLog(`Detected an existing GitHub repository at: ${chalk.green(url)}`); const guessedRemoteURL = getGitRemoteURL(projectName, gitHubUsername); if (yes) { promptLog(`Using a Git remote URL of: ${chalk.green(guessedRemoteURL)}`); return guessedRemoteURL; } const shouldUseGuessedURL = await getInputYesNo( `Do you want to use a Git remote URL of: ${chalk.green( guessedRemoteURL, )}`, ); if (shouldUseGuessedURL) { return guessedRemoteURL; } // Assume that since they do not want to connect this project to the existing GitHub repository, // they do not want to initialize a remote Git URL at all. return undefined; } if (yes) { await $q`gh repo create ${projectName} --public`; promptLog(`Created a new GitHub repository at: ${chalk.green(url)}`); return getGitRemoteURL(projectName, gitHubUsername); } const shouldCreateNewGitHubRepo = await getInputYesNo( `Would you like to create a new GitHub repository at: ${chalk.green(url)}`, ); if (shouldCreateNewGitHubRepo) { await $q`gh repo create ${projectName} --public`; promptLog("Successfully created a new GitHub repository."); return getGitRemoteURL(projectName, gitHubUsername); } // Assume that since they do not want to create a new GitHub repository, they do not want to // initialize a remote Git URL at all. return undefined; } function getGitRemoteURL(projectName: string, gitHubUsername: string) { return `git@github.com:${gitHubUsername}/${projectName}.git`; } export async function initGitRepository( projectPath: string, gitRemoteURL: string | undefined, ): Promise { const gitExists = await commandExists("git"); if (!gitExists) { return; } if (gitRemoteURL === undefined) { return; } const $$q = $q({ cwd: projectPath }); await $$q`git init --initial-branch main`; await $$q`git remote add origin ${gitRemoteURL}`; const gitNameAndEmailConfigured = await isGitNameAndEmailConfigured(); if (gitNameAndEmailConfigured) { await $$q`git add --all`; const commitMessage = `chore: add files from ${PROJECT_NAME} ${PROJECT_VERSION} template`; await $$q`git commit --message ${commitMessage}`; await $$q`git push --set-upstream origin main`; } } async function isGitNameAndEmailConfigured(): Promise { const { exitCode: nameExitCode } = await $q`git config --global user.name`; const { exitCode: emailExitCode } = await $q`git config --global user.email`; return nameExitCode === 0 && emailExitCode === 0; }