UNPKG

create-nx-workspace

Version:

Smart Repos · Fast Builds

245 lines (242 loc) • 10.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.GitHubPushSkippedError = exports.VcsPushStatus = void 0; exports.checkGitVersion = checkGitVersion; exports.isGitAvailable = isGitAvailable; exports.initializeGitRepo = initializeGitRepo; exports.pushToGitHub = pushToGitHub; const child_process_1 = require("child_process"); const default_base_1 = require("./default-base"); const output_1 = require("../output"); const child_process_utils_1 = require("../child-process-utils"); const enquirer = require("enquirer"); var VcsPushStatus; (function (VcsPushStatus) { VcsPushStatus["PushedToVcs"] = "PushedToVcs"; VcsPushStatus["OptedOutOfPushingToVcs"] = "OptedOutOfPushingToVcs"; VcsPushStatus["FailedToPushToVcs"] = "FailedToPushToVcs"; VcsPushStatus["SkippedGit"] = "SkippedGit"; })(VcsPushStatus || (exports.VcsPushStatus = VcsPushStatus = {})); class GitHubPushSkippedError extends Error { constructor(message) { super(message); this.title = 'Push your workspace'; this.name = 'GitHubPushSkippedError'; } } exports.GitHubPushSkippedError = GitHubPushSkippedError; async function checkGitVersion() { try { const result = await (0, child_process_utils_1.execAndWait)('git --version', process.cwd()); const gitVersionOutput = result.stdout.trim(); return gitVersionOutput.match(/[0-9]+\.[0-9]+\.+[0-9]+/)?.[0]; } catch { return null; } } /** * Synchronously checks if git is available on the system. * Returns true if git command can be executed, false otherwise. */ function isGitAvailable() { try { (0, child_process_1.execSync)('git --version', { stdio: 'ignore' }); return true; } catch { return false; } } async function getGitHubUsername(directory) { const result = await (0, child_process_utils_1.execAndWait)('gh api user --jq .login', directory); const username = result.stdout.trim(); if (!username) { throw new GitHubPushSkippedError('GitHub CLI is not authenticated'); } return username; } // Module-level promise for background repo fetching let existingReposPromise; function populateExistingRepos(directory) { existingReposPromise ??= getUserRepositories(directory); } async function getUserRepositories(directory) { try { const allRepos = new Set(); // Get user's personal repos and organizations concurrently // Limit to 100 repos for faster response (covers most use cases) const [userRepos, orgsResult] = await Promise.all([ (0, child_process_utils_1.execAndWait)('gh repo list --limit 100 --json nameWithOwner --jq ".[].nameWithOwner"', directory), (0, child_process_utils_1.execAndWait)('gh api user/orgs --jq ".[].login"', directory), ]); // Add user's personal repos userRepos.stdout .trim() .split('\n') .filter((repo) => repo.length > 0) .forEach((repo) => allRepos.add(repo)); // Parse organizations const orgs = orgsResult.stdout .trim() .split('\n') .filter((org) => org.length > 0); // Get repos from all organizations concurrently const orgRepoPromises = orgs.map(async (org) => { try { const orgRepos = await (0, child_process_utils_1.execAndWait)(`gh repo list ${org} --limit 100 --json nameWithOwner --jq ".[].nameWithOwner"`, directory); return orgRepos.stdout .trim() .split('\n') .filter((repo) => repo.length > 0); } catch { // Return empty array if we can't access org repos return []; } }); const orgRepoResults = await Promise.all(orgRepoPromises); // Add all org repos to the set orgRepoResults.flat().forEach((repo) => allRepos.add(repo)); return allRepos; } catch { // If we can't fetch repos, return empty set to skip validation return new Set(); } } async function initializeGitRepo(directory, options) { // Set git commit environment variables if provided if (options.commit?.name) { process.env.GIT_AUTHOR_NAME = options.commit.name; process.env.GIT_COMMITTER_NAME = options.commit.name; } if (options.commit?.email) { process.env.GIT_AUTHOR_EMAIL = options.commit.email; process.env.GIT_COMMITTER_EMAIL = options.commit.email; } const gitVersion = await checkGitVersion(); if (!gitVersion) { return; } const insideRepo = await (0, child_process_utils_1.execAndWait)('git rev-parse --is-inside-work-tree', directory, true).then(() => true, () => false); if (insideRepo) { output_1.output.log({ title: 'Directory is already under version control. Skipping initialization of git.', }); return; } const defaultBase = options.defaultBase || (0, default_base_1.deduceDefaultBase)(); const [gitMajor, gitMinor] = gitVersion.split('.'); if (+gitMajor > 2 || (+gitMajor === 2 && +gitMinor >= 28)) { await (0, child_process_utils_1.execAndWait)(`git init -b ${defaultBase}`, directory); } else { await (0, child_process_utils_1.execAndWait)('git init', directory); await (0, child_process_utils_1.execAndWait)(`git checkout -b ${defaultBase}`, directory); // Git < 2.28 doesn't support -b on git init. } await (0, child_process_utils_1.execAndWait)('git add .', directory); if (options.commit) { let message = `${options.commit.message}` || 'initial commit'; if (options.connectUrl) { message = `${message} To connect your workspace to Nx Cloud, push your repository to your git hosting provider and go to the following URL: ${options.connectUrl} `; } await (0, child_process_utils_1.execAndWait)(`git commit -m "${message}"`, directory); } } async function pushToGitHub(directory, options) { try { if (process.env['NX_SKIP_GH_PUSH'] === 'true') { throw new GitHubPushSkippedError('NX_SKIP_GH_PUSH is true so skipping GitHub push.'); } // Note: This call can throw an error even if user hasn't opted in to push yet, // which could be confusing as they haven't been asked about GitHub push at this point. // We check gh authentication early to provide a better error message. const username = await getGitHubUsername(directory); // Start fetching existing repositories in the background immediately // This runs while user is answering prompts, so validation is usually instant populateExistingRepos(directory); // First prompt: Ask if they want to push to GitHub const { push } = await enquirer.prompt([ { name: 'push', message: 'Would you like to push this workspace to GitHub?', type: 'autocomplete', choices: [{ name: 'Yes' }, { name: 'No' }], initial: 0, }, ]); if (push !== 'Yes') { return VcsPushStatus.OptedOutOfPushingToVcs; } // Create default repository name using the username we already have const defaultRepo = `${username}/${options.name}`; const createRepoUrl = `https://github.com/new?name=${encodeURIComponent(options.name)}`; // Second prompt: Ask where to create the repository with validation const { repoName } = await enquirer.prompt([ { name: 'repoName', message: 'Repository name (format: username/repo-name):', type: 'input', initial: defaultRepo, validate: async (value) => { if (!value.includes('/')) { return 'Repository name must be in format: username/repo-name'; } // Wait for background fetch to complete before validating const existingRepos = await existingReposPromise; if (existingRepos?.has(value)) { return `Repository '${value}' already exists. Choose a different name or create manually: ${createRepoUrl}`; } return true; }, }, ]); // Create GitHub repository using gh CLI from the workspace directory // This will automatically add remote origin and push the current branch output_1.output.log({ title: 'Creating GitHub repository and pushing (this may take a moment)...', }); await (0, child_process_utils_1.spawnAndWait)('gh', [ 'repo', 'create', repoName, '--private', '--push', '--source', directory, ], directory); // Get the actual repository URL from GitHub CLI (it could be different from github.com) const repoResult = await (0, child_process_utils_1.execAndWait)('gh repo view --json url -q .url', directory); const repoUrl = repoResult.stdout.trim(); output_1.output.success({ title: `Successfully pushed to GitHub repository: ${repoUrl}`, }); return VcsPushStatus.PushedToVcs; } catch (e) { const isVerbose = options.verbose || process.env.NX_VERBOSE_LOGGING === 'true'; const errorMessage = e instanceof Error ? e.message : String(e); // Error code 127 means gh wasn't installed // GitHubPushSkippedError means user hasn't opted in or we couldn't authenticate const title = e instanceof GitHubPushSkippedError || e?.code === 127 ? 'Push your workspace to GitHub.' : 'Could not push. Push repo to complete setup.'; const createRepoUrl = `https://github.com/new?name=${encodeURIComponent(options.name)}`; output_1.output.log({ title, bodyLines: isVerbose ? [ `Go to ${createRepoUrl} and push this workspace.`, 'Error details:', errorMessage, ] : [`Go to ${createRepoUrl} and push this workspace.`], }); return VcsPushStatus.FailedToPushToVcs; } }