Skip to content

Commit

Permalink
[feat] Added restore command and added full mechanic of version contr…
Browse files Browse the repository at this point in the history
…ol system
  • Loading branch information
erdemkosk committed Nov 13, 2023
1 parent 4f56110 commit acc7b33
Show file tree
Hide file tree
Showing 10 changed files with 292 additions and 83 deletions.
131 changes: 79 additions & 52 deletions bin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,25 @@ import Table from 'cli-table3'
import packages from '../package.json'
import {
getBaseFolder,
getFilesRecursively,
readFile
getEnvFilesRecursively
} from '../lib/file-operations'

import {
createEnvFile,
updateEnvFile,
updateAllEnvFile,
createSymlink,
getValuesInEnv,
compareEnvFiles,
syncEnvFile,
promptForEnvVariable
promptForEnvVariable,
getUniqueEnvNames
} from '../lib/env-operations'

import {
getEnvVersions
} from '../lib/history-operations'

import { format } from 'date-fns'

const program = new Command()
inquirer.registerPrompt('autocomplete', inquirerPrompt)

Expand All @@ -32,7 +36,7 @@ program
.command('ls')
.description(`${chalk.yellow('LIST')} environment variables in an .env file for a specific service. Select a service and view its environment variables.`)
.action(async () => {
const files = await getFilesRecursively({ directory: getBaseFolder() })
const files = await getEnvFilesRecursively({ directory: getBaseFolder() })

if (files.length === 0) {
console.log(`You have not registered any service yet. Go to the file path of the request with your ${chalk.blue('.env')} file in it and run the ${chalk.blue('sync')} command.`)
Expand Down Expand Up @@ -87,6 +91,10 @@ program
name: 'envValue',
message: 'Select the env value to change:',
source: (answers: any, input: string) => {
if (input === undefined) {
return envOptions
}

return envOptions.filter(option => option.includes(input))
}
},
Expand All @@ -109,7 +117,7 @@ program
.description(`${chalk.yellow('COMPARE')} command is a handy utility for differences in two different files with the same variable.`)
.alias('comp')
.action(async () => {
const files: string [] = await getFilesRecursively({ directory: getBaseFolder() })
const files: string [] = await getEnvFilesRecursively({ directory: getBaseFolder() })

if (files.length < 2) {
console.log(`You must have a minimum of ${chalk.blue('2')} services registered to compare.`)
Expand Down Expand Up @@ -160,80 +168,99 @@ program
})

program
.command('create')
.description('CREATE a new env file')
.alias('c')
.command('update')
.description('UPDATE a single field in .env file and create a version')
.alias('u')
.action(async () => {
const answers = await inquirer.prompt([
const files = await getEnvFilesRecursively({ directory: getBaseFolder() })

const { targetPath } = await inquirer.prompt({
type: 'list',
name: 'targetPath',
message: 'Select an .env file to show:',
choices: files
})

const envOptions = await getUniqueEnvNames(targetPath)

const { envValue, newValue } = await inquirer.prompt([
{
type: 'input',
name: 'serviceName',
message: chalk.green('Enter the service name: ')
type: 'autocomplete',
name: 'envValue',
message: 'Select the env value to change:',
source: (answers: any, input: string) => {
if (input === undefined) {
return envOptions
}

return envOptions.filter(option => option.includes(input))
}
},
{
type: 'editor',
name: 'content',
message: chalk.green('Enter the env content: ')
type: 'input',
name: 'newValue',
message: 'Enter the new value:'
}
])

const { serviceName, content } = answers

try {
await createEnvFile({ serviceName, content })

console.log(`File .env created for the "${chalk.blue(serviceName)}" service.`)
await updateEnvFile({ file: targetPath, envValue, newValue })
console.log(`Environment variables updated in "${chalk.blue(targetPath)}"`)
} catch (error) {
console.error('An error occurred:', error)
}
})

program
.command('copy')
.description('COPY env file to current folder symlink')
.alias('cp')
.command('restore')
.description('Restore a field in .env file to a specific version')
.alias('r')
.action(async () => {
const files = await getFilesRecursively({ directory: getBaseFolder() })
const files = await getEnvFilesRecursively({ directory: getBaseFolder() })

const { targetPath } = await inquirer.prompt({
type: 'list',
name: 'targetPath',
message: 'Select an .env file to copy:',
message: 'Select an .env file to restore:',
choices: files
})

const symlinkPath = await createSymlink({ targetPath })
const envOptions = await getUniqueEnvNames(targetPath)

console.log(`Symbolic link created: "${chalk.blue(symlinkPath)}"`)
})

program
.command('update')
.description('UPDATE a single env file')
.alias('u')
.action(async () => {
const files = await getFilesRecursively({ directory: getBaseFolder() })

const { targetPath } = await inquirer.prompt({
type: 'list',
name: 'targetPath',
message: 'Select an .env file to show:',
choices: files
})
const { envValue } = await inquirer.prompt([
{
type: 'autocomplete',
name: 'envValue',
message: 'Select the env value to change:',
source: async (answers: any, input: string) => {
if (input === undefined) {
return envOptions
}

const existingContent = await readFile({ file: targetPath })
const filteredOptions = envOptions.filter(option => option.includes(input))

const { content } = await inquirer.prompt([
{
type: 'editor',
name: 'content',
message: chalk.green('Edit the env content:'),
default: existingContent
return filteredOptions
}
}
])

const versions = await getEnvVersions(targetPath, envValue)
const { version } = await inquirer.prompt({
type: 'list',
name: 'version',
message: 'Select a version to restore:',
choices: versions.map((version: { timestamp: any, changes: Array<{ oldValue: any }> }) => {
const formattedTimestamp = format(new Date(version.timestamp), 'yyyy-MM-dd HH:mm:ss')
return {
name: `Version ${formattedTimestamp} - ${version.changes[0].oldValue}`,
value: version
}
})
})

try {
await updateEnvFile({ file: targetPath, content })
await updateEnvFile({ file: targetPath, envValue, newValue: version.changes[0].oldValue })
console.log(`Environment variables restored in "${chalk.blue(targetPath)}"`)
} catch (error) {
console.error('An error occurred:', error)
}
Expand Down
84 changes: 78 additions & 6 deletions lib/env-operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as path from 'path'
import {
getBaseFolder,
createFolderIfDoesNotExist,
getFilesRecursively,
getEnvFilesRecursively,
readFile,
writeFile,
generateSymlink,
Expand All @@ -12,6 +12,10 @@ import {
doesFileExist
} from './file-operations'

import {
saveFieldVersion
} from './history-operations'

function getServiceNameFromUrl ({ targetPath }: { targetPath: string }): string {
const parts = targetPath.split('/')
return parts[parts.length - 2]
Expand Down Expand Up @@ -43,12 +47,40 @@ async function createEnvFile ({

async function updateEnvFile ({
file,
content
envValue,
newValue
}: {
file: string
content: string
envValue: string
newValue: string
}): Promise<void> {
await writeFile({ file, newFileContents: content })
const oldValue = await getEnvValue(file, envValue)

if (oldValue !== undefined) {
const updatedFileContent = await readFile({ file })

if (updatedFileContent !== undefined) {
const updatedLines = updatedFileContent.split('\n').map(line => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [currentEnvName, currentEnvValue] = splitEnvLine(line)
if (currentEnvName === envValue) {
return `${currentEnvName}=${newValue}`
}
return line
})

await saveFieldVersion(file, envValue, newValue)

await writeFile({
file,
newFileContents: updatedLines.join('\n')
})
} else {
console.error(`File cannot read: ${file}`)
}
} else {
console.error(`Expected ${envValue} cannot find in a file.`)
}
}

async function updateAllEnvFile ({
Expand All @@ -58,7 +90,7 @@ async function updateAllEnvFile ({
envValue: string
newValue: string
}): Promise<string[]> {
const files = await getFilesRecursively({ directory: getBaseFolder() })
const files = await getEnvFilesRecursively({ directory: getBaseFolder() })
const effectedServices: string[] = []

for (const file of files) {
Expand All @@ -72,6 +104,7 @@ async function updateAllEnvFile ({
})

if (newFileContents !== fileContents && newFileContents !== '') {
await saveFieldVersion(file, envValue, newValue)
await writeFile({ file, newFileContents })
effectedServices.push(file)
}
Expand Down Expand Up @@ -240,6 +273,43 @@ async function promptForEnvVariable (): Promise<string[]> {
return uniqueVariables
}

async function getUniqueEnvNames (targetFolder: string): Promise<string[]> {
const envNames = new Set<string>()

const fileContent = await readFile({ file: targetFolder })
if (fileContent != null) {
const sourceLines = fileContent.split('\n')

for (const line of sourceLines) {
if (line.trim() !== '') {
const [envName] = splitEnvLine(line)
envNames.add(envName)
}
}
}

const uniqueEnvNames = Array.from(envNames).sort()
return uniqueEnvNames
}

async function getEnvValue (targetFolder: string, envName: string): Promise<string | undefined> {
const fileContent = await readFile({ file: targetFolder })
if (fileContent != null) {
const sourceLines = fileContent.split('\n')

for (const line of sourceLines) {
if (line.trim() !== '') {
const [currentEnvName, value] = splitEnvLine(line)
if (currentEnvName === envName) {
return value
}
}
}
}

return undefined
}

export {
createEnvFile,
updateEnvFile,
Expand All @@ -250,5 +320,7 @@ export {
syncEnvFile,
promptForEnvVariable,
getServiceNameFromUrl,
splitEnvLine
splitEnvLine,
getUniqueEnvNames,
getEnvValue
}
38 changes: 21 additions & 17 deletions lib/file-operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,27 +35,30 @@ async function createFolderIfDoesNotExist (folderPath: fs.PathLike): Promise<voi
}
}

async function getFilesRecursively ({
directory
}: {
directory: string
}): Promise<string[]> {
const files: string[] = []
await readDir(directory, files)
return files
async function createFileIfNotExists (filePath: fs.PathLike, initialContent: string = '[]'): Promise<void> {
try {
await fs.promises.access(filePath)
} catch (error) {
await fs.promises.writeFile(filePath, initialContent, 'utf8')
}
}

async function readDir (
dir: string,
files: string[]
): Promise<void> {
async function getEnvFilesRecursively ({ directory }: { directory: string }): Promise<string[]> {
const envFiles: string[] = []
await readDir(directory, envFiles)
return envFiles
}

async function readDir (dir: string, envFiles: string[]): Promise<void> {
const dirents = await fs.promises.readdir(dir, { withFileTypes: true })

for (const dirent of dirents) {
const resolvedPath = path.resolve(dir, dirent.name)

if (dirent.isDirectory()) {
await readDir(resolvedPath, files)
} else if (dirent.isFile() && dirent.name !== '.DS_Store') {
files.push(resolvedPath)
await readDir(resolvedPath, envFiles)
} else if (dirent.isFile() && dirent.name === '.env') {
envFiles.push(resolvedPath)
}
}
}
Expand Down Expand Up @@ -115,11 +118,12 @@ export {
getBaseFolder,
readFile,
writeFile,
getFilesRecursively,
getEnvFilesRecursively,
createFolderIfDoesNotExist,
generateSymlink,
copyFile,
deleteFile,
getEnvFiles,
doesFileExist
doesFileExist,
createFileIfNotExists
}
Loading

0 comments on commit acc7b33

Please sign in to comment.