From 15affd3fb46d8908dc0c95aa0a075aaed27ee012 Mon Sep 17 00:00:00 2001 From: Jai Radhakrishnan <55522316+jairad26@users.noreply.github.com> Date: Tue, 5 Nov 2024 12:01:13 -0800 Subject: [PATCH] Jai/hyp 2574 fix async functions to use async fs (#36) * improve async functions and writing installation to settings --- src/commands/link/index.ts | 32 ++++++++++------ src/commands/login/index.ts | 30 +-------------- src/commands/logout/index.ts | 14 +++---- src/commands/org/switch.ts | 18 +++------ src/util/fs.ts | 20 ++++++++++ src/util/index.ts | 72 +++++++++++++++++++++++++++--------- 6 files changed, 109 insertions(+), 77 deletions(-) create mode 100644 src/util/fs.ts diff --git a/src/commands/link/index.ts b/src/commands/link/index.ts index 808a916..55f4d85 100644 --- a/src/commands/link/index.ts +++ b/src/commands/link/index.ts @@ -1,6 +1,6 @@ import {Command} from '@oclif/core' import chalk from 'chalk' -import * as fs from 'node:fs' +import * as fs from "../../util/fs.js"; import * as http from 'node:http' import {URL} from 'node:url' import open from 'open' @@ -10,8 +10,9 @@ import { getProjectsByOrgReq, sendCreateProjectRepoReq, sendCreateProjectReq, sendGetRepoIdReq, } from '../../util/graphql.js' import { - confirmExistingProjectLink, confirmOverwriteCiHypFile, fileExists, getCiHypFilePath, getEnvFilePath, getGitConfigFilePath, + confirmExistingProjectLink, confirmOverwriteCiHypFile, fileExists, getCiHypFilePath, getSettingsFilePath, getGitConfigFilePath, getGitRemoteUrl, getGithubWorkflowDir, promptProjectLinkSelection, promptProjectName, readSettingsJson, + writeGithubInstallationIdToSettingsFile, } from '../../util/index.js' export default class LinkIndex extends Command { @@ -103,22 +104,22 @@ export default class LinkIndex extends Command { // check if the directory has a .git/config with a remote named 'origin', if not, throw an error and ask them to set that up const gitConfigFilePath = getGitConfigFilePath() - if (!fileExists(gitConfigFilePath)) { + if (!await fileExists(gitConfigFilePath)) { throw new Error(chalk.red('No .git found in this directory. Please initialize a git repository with `git init`.')) } - const gitUrl = getGitRemoteUrl(gitConfigFilePath) + const gitUrl = await getGitRemoteUrl(gitConfigFilePath) // check the .hypermode/settings.json and see if there is a installationId with a key for the github owner. if there is, // continue, if not send them to github app installation page, and then go to callback server, and add installation id to settings.json - const envFilePath = getEnvFilePath() - if (!fileExists(envFilePath)) { + const settingsFilePath = getSettingsFilePath() + if (!await fileExists(settingsFilePath)) { this.log(chalk.red('Not logged in.') + ' Log in with `hyp login`.') return } - const settings = readSettingsJson(envFilePath) + const settings = await readSettingsJson(settingsFilePath) if (!settings.email || !settings.jwt || !settings.orgId) { this.log(chalk.red('Not logged in.') + ' Log in with `hyp login`.') @@ -129,7 +130,14 @@ export default class LinkIndex extends Command { const repoName = gitUrl.split('/')[4].replace(/\.git$/, '') - const installationId = (!settings.installationIds || !settings.installationIds[gitOwner]) ? await this.getUserInstallationThroughAuthFlow() : settings.installationIds[gitOwner] + let installationId = null + + if (!settings.installationIds || !settings.installationIds[gitOwner]) { + installationId = await this.getUserInstallationThroughAuthFlow() + await writeGithubInstallationIdToSettingsFile(gitOwner, installationId) + } else { + installationId = settings.installationIds[gitOwner] + } // call hypermode getRepoId with the installationId and the git url, if it returns a repoId, continue, if not, throw an error const repoId = await sendGetRepoIdReq(settings.jwt, installationId, gitUrl) @@ -171,13 +179,13 @@ export default class LinkIndex extends Command { const githubWorkflowDir = getGithubWorkflowDir() const ciHypFilePath = getCiHypFilePath() - if (!fileExists(githubWorkflowDir)) { + if (!await fileExists(githubWorkflowDir)) { // create the directory - fs.mkdirSync(githubWorkflowDir, {recursive: true}) + await fs.mkdir(githubWorkflowDir, {recursive: true}) } let shouldCreateCIFile = true - if (fileExists(ciHypFilePath)) { + if (await fileExists(ciHypFilePath)) { // prompt if they want to replace it const confirmOverwrite = await confirmOverwriteCiHypFile() if (!confirmOverwrite) { @@ -187,7 +195,7 @@ export default class LinkIndex extends Command { } if (shouldCreateCIFile) { - fs.writeFileSync(ciHypFilePath, ciStr) + await fs.writeFile(ciHypFilePath, ciStr, {flag: 'w'}) this.log(chalk.green('Successfully created ci-hyp.yml! 🎉')) } diff --git a/src/commands/login/index.ts b/src/commands/login/index.ts index 92cd194..33a3811 100644 --- a/src/commands/login/index.ts +++ b/src/commands/login/index.ts @@ -1,13 +1,12 @@ import {Command} from '@oclif/core' import chalk from 'chalk' -import * as fs from 'node:fs' import * as http from 'node:http' import {URL} from 'node:url' import open from 'open' import {sendGetOrgsReq} from '../../util/graphql.js' import { - fileExists, getEnvDir, getEnvFilePath, promptOrgSelection, readSettingsJson, + writeToSettingsFile, promptOrgSelection, } from '../../util/index.js' const loginHTML = ` @@ -107,7 +106,7 @@ export default class LoginIndex extends Command { try { const orgs = await sendGetOrgsReq(jwt) const selectedOrg = await promptOrgSelection(orgs) - this.writeToEnvFile(jwt, email, selectedOrg.id) + await writeToSettingsFile(jwt, email, selectedOrg.id) this.log('Successfully logged in as ' + chalk.dim(email) + '! 🎉') resolve() } catch (error) { @@ -152,29 +151,4 @@ export default class LoginIndex extends Command { }) }) } - - private async writeToEnvFile(jwt: string, email: string, orgId: string): Promise { - const envDir = getEnvDir() - const envFilePath = getEnvFilePath() - - // Create the directory if it doesn't exist - if (!fileExists(envDir)) { - fs.mkdirSync(envDir, {recursive: true}) - } - - const newEnvContent: { HYP_EMAIL: string; HYP_JWT: string; HYP_ORG_ID: string; INSTALLATION_IDS: { [key: string]: string } | null } = { - HYP_EMAIL: email, - HYP_JWT: jwt, - HYP_ORG_ID: orgId, - INSTALLATION_IDS: null, - }; - - if (fileExists(envFilePath)) { - const settings = readSettingsJson(envFilePath) - newEnvContent.INSTALLATION_IDS = settings.installationIds - } - - // Write the new content to the file, replacing any existing content - fs.writeFileSync(envFilePath, JSON.stringify(newEnvContent, null, 2), {flag: 'w'}) - } } diff --git a/src/commands/logout/index.ts b/src/commands/logout/index.ts index 91cdbb0..7b5a1a4 100644 --- a/src/commands/logout/index.ts +++ b/src/commands/logout/index.ts @@ -1,8 +1,8 @@ import {Command} from '@oclif/core' import chalk from 'chalk' -import * as fs from 'node:fs' +import * as fs from "../../util/fs.js"; -import {fileExists, getEnvFilePath, readSettingsJson} from '../../util/index.js' +import {fileExists, getSettingsFilePath, readSettingsJson} from '../../util/index.js' export default class LogoutIndex extends Command { static override args = {} @@ -14,15 +14,15 @@ export default class LogoutIndex extends Command { static override flags = {} public async run(): Promise { - const envFilePath = getEnvFilePath() + const settingsFilePath = getSettingsFilePath() // Check if .env.local file exists - if (!fileExists(envFilePath)) { + if (!await fileExists(settingsFilePath)) { this.log(chalk.red('Not logged in.') + ' Log in with `hyp login`.') return } - const res = readSettingsJson(envFilePath) + const res = await readSettingsJson(settingsFilePath) if (!res.email) { this.log(chalk.red('Not logged in.') + ' Log in with `hyp login`.') @@ -31,7 +31,7 @@ export default class LogoutIndex extends Command { console.log('Logging out of email: ' + chalk.dim(res.email)) - const newEnvContent = { + const newSettingsContent = { HYP_EMAIL: null, HYP_JWT: null, HYP_ORG_ID: null, @@ -39,6 +39,6 @@ export default class LogoutIndex extends Command { } // remove all content from settings.json - fs.writeFileSync(envFilePath, JSON.stringify(newEnvContent, null, 2), {flag: 'w'}) + await fs.writeFile(settingsFilePath, JSON.stringify(newSettingsContent, null, 2), {flag: 'w'}) } } diff --git a/src/commands/org/switch.ts b/src/commands/org/switch.ts index 7cb8e7d..64d699b 100644 --- a/src/commands/org/switch.ts +++ b/src/commands/org/switch.ts @@ -1,10 +1,10 @@ import {Command} from '@oclif/core' import chalk from 'chalk' -import * as fs from 'node:fs' import {sendGetOrgsReq} from '../../util/graphql.js' import { - fileExists, getEnvFilePath, promptOrgSelection, readSettingsJson, + fileExists, getSettingsFilePath, promptOrgSelection, readSettingsJson, + writeToSettingsFile, } from '../../util/index.js' export default class OrgSwitch extends Command { @@ -17,13 +17,13 @@ export default class OrgSwitch extends Command { static override flags = {} public async run(): Promise { - const envFilePath = getEnvFilePath() - if (!fileExists(envFilePath)) { + const settingsFilePath = getSettingsFilePath() + if (!await fileExists(settingsFilePath)) { this.log(chalk.red('Not logged in.') + ' Log in with `hyp login`.') return } - const res = readSettingsJson(envFilePath) + const res = await readSettingsJson(settingsFilePath) if (!res.email || !res.jwt || !res.orgId) { this.log(chalk.red('Not logged in.') + ' Log in with `hyp login`.') @@ -33,12 +33,6 @@ export default class OrgSwitch extends Command { const orgs = await sendGetOrgsReq(res.jwt) const selectedOrg = await promptOrgSelection(orgs) - const updatedContent = { - HYP_EMAIL: res.email, - HYP_JWT: res.jwt, - HYP_ORG_ID: selectedOrg.id, - } - - fs.writeFileSync(envFilePath, JSON.stringify(updatedContent, null, 2), {flag: 'w'}) + await writeToSettingsFile(res.jwt, res.email, selectedOrg.id) } } diff --git a/src/util/fs.ts b/src/util/fs.ts new file mode 100644 index 0000000..8c32176 --- /dev/null +++ b/src/util/fs.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2024 Hypermode Inc. + * Licensed under the terms of the Apache License, Version 2.0 + * See the LICENSE file that accompanied this code for further details. + * + * SPDX-FileCopyrightText: 2024 Hypermode Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import fs from "node:fs"; +export * from "node:fs/promises"; + +export async function exists(path: string) { + try { + await fs.promises.stat(path); + return true; + } catch { + return false; + } +} diff --git a/src/util/index.ts b/src/util/index.ts index d8a3420..2d81e5b 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -1,8 +1,8 @@ import {ExitPromptError} from '@inquirer/core' import * as inquirer from '@inquirer/prompts' import chalk from 'chalk' +import * as fs from "../util/fs.js"; import slugify from "@sindresorhus/slugify" -import * as fs from 'node:fs' import * as path from 'node:path' import {Interface} from 'node:readline' @@ -88,24 +88,24 @@ export async function confirmOverwriteCiHypFile(): Promise { }) } -export function confirmExistingProjectLink(): Promise { +export async function confirmExistingProjectLink(): Promise { return inquirer.confirm({ default: true, message: 'You have existing projects with no linked repositories. Would you like to select from these projects?', }) } -export function getEnvDir(): string { +export function getSettingsDir(): string { return path.join(process.env.HOME || '', '.hypermode') } -export function getEnvFilePath(): string { - const envDir = getEnvDir() - return path.join(envDir, 'settings.json') +export function getSettingsFilePath(): string { + const settingsDir = getSettingsDir() + return path.join(settingsDir, 'settings.json') } -export function fileExists(filePath: string): boolean { - return fs.existsSync(filePath) +export async function fileExists(filePath: string): Promise { + return fs.exists(filePath) } export function getGitDir(): string { @@ -124,8 +124,8 @@ export function getGitConfigFilePath(): string { return path.join(getGitDir(), 'config') } -export function getGitRemoteUrl(filePath: string): string { - const content = fs.readFileSync(filePath, 'utf8') +export async function getGitRemoteUrl(filePath: string): Promise { + const content = await fs.readFile(filePath, 'utf8') const remoteMatch = content.match(/\[remote "origin"]\n\s+url = (.*)/) if (!remoteMatch) { throw new Error(chalk.red('No remote origin found in .git/config, please set up a remote origin with `git remote add origin `.')) @@ -134,14 +134,8 @@ export function getGitRemoteUrl(filePath: string): string { return remoteMatch[1] } -export function readSettingsJson(filePath: string): { - content: string - email: null | string - installationIds: { [key: string]: string } | null - jwt: null | string - orgId: null | string -} { - const content = fs.readFileSync(filePath, 'utf8') +export async function readSettingsJson(filePath: string): Promise<{ content: string; email: null | string; installationIds: { [key: string]: string; } | null; jwt: null | string; orgId: null | string; }> { + const content = await fs.readFile(filePath, 'utf8') let email: null | string = null let jwt: null | string = null @@ -162,3 +156,45 @@ export function readSettingsJson(filePath: string): { content, email, installationIds, jwt, orgId, } } + +export async function writeToSettingsFile(jwt: string, email: string, orgId: string): Promise { + const settingsDir = getSettingsDir() + const settingsFilePath = getSettingsFilePath() + + // Create the directory if it doesn't exist + if (!await fileExists(settingsDir)) { + await fs.mkdir(settingsDir, {recursive: true}) + } + + const newSettingsContent: { HYP_EMAIL: string; HYP_JWT: string; HYP_ORG_ID: string; INSTALLATION_IDS: { [key: string]: string } | null } = { + HYP_EMAIL: email, + HYP_JWT: jwt, + HYP_ORG_ID: orgId, + INSTALLATION_IDS: null, +}; + + if (await fileExists(settingsFilePath)) { + const settings = await readSettingsJson(settingsFilePath) + newSettingsContent.INSTALLATION_IDS = settings.installationIds + } + + // Write the new content to the file, replacing any existing content + await fs.writeFile(settingsFilePath, JSON.stringify(newSettingsContent, null, 2), {flag: 'w'}) +} + +export async function writeGithubInstallationIdToSettingsFile(gitOwner: string, installationId: string): Promise { + const settingsFilePath = getSettingsFilePath() + const settings = await readSettingsJson(settingsFilePath) + + settings.installationIds = settings.installationIds || {} + settings.installationIds[gitOwner] = installationId + + const newSettingsContent = { + HYP_EMAIL: settings.email, + HYP_JWT: settings.jwt, + HYP_ORG_ID: settings.orgId, + INSTALLATION_IDS: settings.installationIds, + } + + await fs.writeFile(settingsFilePath, JSON.stringify(newSettingsContent, null, 2), {flag: 'w'}) +}