-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: allow selecting the account or organization to create the repos…
…itory under (#34)
- Loading branch information
Showing
4 changed files
with
84 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
47 changes: 47 additions & 0 deletions
47
packages/core/installMachine/installSteps/github/fetchOrganizations.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { execAsync } from '../../../utils/execAsync'; | ||
import { logger } from '../../../utils/logger'; | ||
|
||
export const fetchOrganizations = async (): Promise<{ name: string; writable: boolean }[]> => { | ||
return await logger.withSpinner('github', 'Fetching organizations you belong to...', async (spinner) => { | ||
try { | ||
// Fetch all organizations the user belongs to | ||
const orgsOutput = await execAsync(`gh api user/orgs --jq '[.[] | {name: .login, repos_url: .repos_url}]'`); | ||
const orgs = JSON.parse(orgsOutput.stdout); | ||
|
||
// Process each organization | ||
const orgsWithPermissions = await Promise.all( | ||
orgs.map(async (org: { name: string; repos_url: string }) => { | ||
try { | ||
// Fetch repositories in the organization | ||
const reposOutput = await execAsync(`gh api ${org.repos_url} --jq '[.[] | {permissions: .permissions}]'`); | ||
const repos = JSON.parse(reposOutput.stdout); | ||
|
||
if (repos.length === 0) { | ||
// Organization has no repositories, assume writable since we can't yet determine permissions | ||
return { name: org.name, writable: true }; | ||
} | ||
|
||
// Check if user has write access to any repository in the organization | ||
const hasWriteAccess = repos.some((repo: any) => repo.permissions?.push || repo.permissions?.admin); | ||
return { name: org.name, writable: hasWriteAccess }; | ||
} catch (error: any) { | ||
if (error.message.includes('HTTP 403')) { | ||
return { name: org.name, writable: false }; // Mark as inaccessible | ||
} | ||
|
||
// For other errors, log and continue | ||
console.error(`Error processing organization: ${org.name}`, error); | ||
return { name: org.name, writable: false }; | ||
} | ||
}), | ||
); | ||
|
||
spinner.succeed('Fetched organizations successfully.'); | ||
return orgsWithPermissions; | ||
} catch (error) { | ||
spinner.fail('Failed to fetch organizations.'); | ||
console.error(error); | ||
return []; | ||
} | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ import chalk from 'chalk'; | |
import { logger } from '../../../utils/logger'; | ||
import { execAsync } from '../../../utils/execAsync'; | ||
import { InstallMachineContext } from '../../../types'; | ||
import { fetchOrganizations } from './fetchOrganizations'; | ||
|
||
const generateUniqueRepoName = async (baseName: string): Promise<string> => { | ||
const cleanBaseName = baseName.replace(/-\d+$/, ''); // Clean base name | ||
|
@@ -72,36 +73,57 @@ export const createGitHubRepository = async ( | |
stateData: InstallMachineContext['stateData'], | ||
) => { | ||
let repoName = projectName; | ||
stateData.githubCandidateName = repoName; // Update state with confirmed name | ||
|
||
// Fetch organizations and build choices for the prompt | ||
const organizations = await fetchOrganizations(); | ||
const accountChoices = [ | ||
{ name: `${username} (personal account)`, value: username }, | ||
...organizations.map((org: { writable: any; name: any }) => ({ | ||
name: org.writable ? org.name : chalk.gray(`${org.name} (read-only)`), | ||
value: org.name, | ||
disabled: org.writable ? false : 'No write access', | ||
})), | ||
]; | ||
|
||
// Prompt the user to select an account or organization | ||
const { selectedAccount } = await inquirer.prompt([ | ||
{ | ||
type: 'list', | ||
name: 'selectedAccount', | ||
message: 'Select the account or organization to create the repository under:', | ||
choices: accountChoices, | ||
}, | ||
]); | ||
stateData.selectedAccount = selectedAccount; // Update state with selected account | ||
|
||
await logger.withSpinner('github', 'Checking if repository already exists...', async (spinner) => { | ||
try { | ||
const repoCheckCommand = `echo "$(gh repo view ${username}/${projectName} --json name)"`; | ||
const existingRepo = execAsync(repoCheckCommand).toString().trim(); | ||
const repoNameJSON = await execAsync(`echo "$(gh repo view ${selectedAccount}/${projectName} --json name)"`); | ||
const repoExists = repoNameJSON.stdout.trim().includes(`{"name":"${projectName}"}`); | ||
|
||
if (existingRepo) { | ||
if (repoExists) { | ||
spinner.stop(); | ||
const newRepoName = await generateUniqueRepoName(projectName); | ||
const { confirmedName } = await inquirer.prompt([ | ||
{ | ||
type: 'input', | ||
name: 'confirmedName', | ||
message: 'Please confirm or modify the repository name:', | ||
message: 'The repository already exists. Please confirm or modify the repository name:', | ||
default: newRepoName, | ||
validate: (input: string) => /^[a-zA-Z0-9._-]+$/.test(input) || 'Invalid repository name.', | ||
}, | ||
]); | ||
repoName = confirmedName; | ||
// Update the state with the confirmed repository name in Xstate | ||
stateData.githubCandidateName = confirmedName; | ||
stateData.githubCandidateName = confirmedName; // Update state with confirmed name | ||
} | ||
spinner.stop(); | ||
} catch (error) { | ||
spinner.fail('Error checking repository existence'); | ||
spinner.fail('Error checking repository existence.'); | ||
console.error(error); | ||
} | ||
}); | ||
|
||
await logger.withSpinner('github', `Creating repository: ${repoName}...`, async (spinner) => { | ||
await logger.withSpinner('github', `Creating repository: ${selectedAccount}/${repoName}...`, async (spinner) => { | ||
try { | ||
spinner.stop(); | ||
const { repositoryVisibility } = await inquirer.prompt([ | ||
|
@@ -114,7 +136,7 @@ export const createGitHubRepository = async ( | |
}, | ||
]); | ||
const visibilityFlag = repositoryVisibility === 'public' ? '--public' : '--private'; | ||
const command = `gh repo create ${repoName} ${visibilityFlag}`; | ||
const command = `gh repo create ${selectedAccount}/${repoName} ${visibilityFlag}`; | ||
await execAsync(command); | ||
spinner.succeed(`Repository created: ${chalk.cyan(repoName)}`); | ||
return repoName; | ||
|
@@ -147,13 +169,13 @@ export const setupGitRepository = async () => { | |
}); | ||
}; | ||
|
||
export const pushToGitHub = async (projectName: string) => { | ||
const username = await fetchGitHubUsername(); | ||
export const pushToGitHub = async (selectedAccount: string, githubCandidateName: string) => { | ||
|
||
await logger.withSpinner('github', 'Pushing changes...', async (spinner) => { | ||
const commands = [ | ||
`git add .`, | ||
`git branch -M main`, | ||
`git remote add origin [email protected]:${username}/${projectName}.git`, | ||
`git remote add origin [email protected]:${selectedAccount}/${githubCandidateName}.git`, | ||
`git commit -m "feat: initial commit"`, | ||
`git push -u origin main`, | ||
]; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters