Skip to content

Commit

Permalink
feat: allow selecting the account or organization to create the repos…
Browse files Browse the repository at this point in the history
…itory under (#34)
  • Loading branch information
maneike authored Nov 19, 2024
1 parent b5557da commit 71af0f5
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 14 deletions.
2 changes: 1 addition & 1 deletion packages/core/installMachine/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ const createInstallMachine = (initialContext: InstallMachineContext) => {
pushToGitHubActor: createStepMachine(
fromPromise<void, InstallMachineContext, AnyEventObject>(async ({ input }) => {
try {
await pushToGitHub(input.stateData.githubCandidateName);
await pushToGitHub(input.stateData.selectedAccount, input.stateData.githubCandidateName);
input.stateData.stepsCompleted.pushToGitHub = true;
saveStateToRcFile(input.stateData, input.projectDir);
} catch (error) {
Expand Down
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 [];
}
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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([
Expand All @@ -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;
Expand Down Expand Up @@ -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`,
];
Expand Down
1 change: 1 addition & 0 deletions packages/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface StaplerState {
stepsCompleted: StepsCompleted;
options: ProjectOptions;
githubCandidateName: string;
selectedAccount: string;
}

export interface InstallMachineContext {
Expand Down

0 comments on commit 71af0f5

Please sign in to comment.