From f05691b4319279a49bf0bc87ba656b7990d52e53 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Tue, 4 Oct 2022 13:45:27 +0800 Subject: [PATCH] feat(cli): command `init/i/install` --- packages/cli/package.json | 8 +- packages/cli/src/commands/install.ts | 125 ++++++++++++++++++++++ packages/cli/src/index.ts | 152 +++++---------------------- pnpm-lock.yaml | 53 +++++++--- 4 files changed, 198 insertions(+), 140 deletions(-) create mode 100644 packages/cli/src/commands/install.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index df2c04f04..56516eee8 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -37,19 +37,21 @@ "chalk": "^4.1.2", "got": "^11.8.2", "hpagent": "^1.0.0", + "inquirer": "^8.2.2", "ora": "^5.0.0", - "prompts": "^2.4.2", "semver": "^7.3.7", - "tar": "^6.1.11" + "tar": "^6.1.11", + "yargs": "^17.6.0" }, "devDependencies": { "@silverhand/eslint-config": "1.0.0", "@silverhand/ts-config": "1.0.0", "@types/decompress": "^4.2.4", + "@types/inquirer": "^8.2.1", "@types/node": "^16.0.0", - "@types/prompts": "^2.0.14", "@types/semver": "^7.3.12", "@types/tar": "^6.1.2", + "@types/yargs": "^17.0.13", "eslint": "^8.21.0", "lint-staged": "^13.0.0", "prettier": "^2.7.1", diff --git a/packages/cli/src/commands/install.ts b/packages/cli/src/commands/install.ts new file mode 100644 index 000000000..04602d718 --- /dev/null +++ b/packages/cli/src/commands/install.ts @@ -0,0 +1,125 @@ +import { execSync } from 'child_process'; +import { existsSync } from 'fs'; +import { mkdir } from 'fs/promises'; +import os from 'os'; +import path from 'path'; + +import chalk from 'chalk'; +import inquirer from 'inquirer'; +import ora from 'ora'; +import * as semver from 'semver'; +import tar from 'tar'; + +import { downloadFile, log, safeExecSync } from '../utilities'; + +export type InstallArgs = { + path?: string; + silent?: boolean; +}; + +const defaultPath = path.join(os.homedir(), 'logto'); +const pgRequired = new semver.SemVer('14.0.0'); + +const validateNodeVersion = () => { + const required = new semver.SemVer('16.0.0'); + const current = new semver.SemVer(execSync('node -v', { encoding: 'utf8', stdio: 'pipe' })); + + if (required.compare(current) > 0) { + log.error(`Logto requires NodeJS >=${required.version}, but ${current.version} found.`); + } + + if (current.major > required.major) { + log.warn( + `Logto is tested under NodeJS ^${required.version}, but version ${current.version} found.` + ); + } +}; + +const validatePath = (value: string) => + existsSync(path.resolve(value)) + ? `The path ${chalk.green(value)} already exists, please try another.` + : true; + +const getInstancePath = async () => { + const { hasPostgresUrl } = await inquirer.prompt<{ hasPostgresUrl?: boolean }>({ + name: 'hasPostgresUrl', + message: `Logto requires PostgreSQL >=${pgRequired.version} but cannot find in the current environment.\n Do you have a remote PostgreSQL instance ready?`, + type: 'confirm', + when: () => { + const pgOutput = safeExecSync('postgres --version') ?? ''; + // Filter out all brackets in the output since Homebrew will append `(Homebrew)`. + const pgArray = pgOutput.split(' ').filter((value) => !value.startsWith('(')); + const pgCurrent = semver.coerce(pgArray[pgArray.length - 1]); + + return !pgCurrent || pgCurrent.compare(pgRequired) < 0; + }, + }); + + if (hasPostgresUrl === false) { + log.error('Logto requires a Postgres instance to run.'); + } + + const { instancePath } = await inquirer.prompt<{ instancePath: string }>({ + name: 'instancePath', + message: 'Where should we create your Logto instance?', + type: 'input', + default: defaultPath, + filter: (value: string) => value.trim(), + validate: validatePath, + }); + + return instancePath; +}; + +const downloadRelease = async () => { + const tarFilePath = path.resolve(os.tmpdir(), './logto.tar.gz'); + + log.info(`Download Logto to ${tarFilePath}`); + await downloadFile( + 'https://github.com/logto-io/logto/releases/latest/download/logto.tar.gz', + tarFilePath + ); + + return tarFilePath; +}; + +const decompress = async (toPath: string, tarPath: string) => { + const decompressSpinner = ora({ + text: `Decompress to ${toPath}`, + prefixText: chalk.blue('[info]'), + }).start(); + + try { + await mkdir(toPath); + await tar.extract({ file: tarPath, cwd: toPath, strip: 1 }); + } catch (error: unknown) { + decompressSpinner.fail(); + log.error(error); + + return; + } + + decompressSpinner.succeed(); +}; + +const install = async ({ path: pathArgument = defaultPath, silent = false }: InstallArgs) => { + validateNodeVersion(); + + const instancePath = (!silent && (await getInstancePath())) || pathArgument; + const isValidPath = validatePath(instancePath); + + if (isValidPath !== true) { + log.error(isValidPath); + } + + const tarPath = await downloadRelease(); + + await decompress(instancePath, tarPath); + + const startCommand = `cd ${instancePath} && npm start`; + log.info( + `Use the command below to start Logto. Happy hacking!\n\n ${chalk.green(startCommand)}` + ); +}; + +export default install; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 1f3f6f4f9..7ddb8b74a 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,130 +1,32 @@ -import { execSync } from 'child_process'; -import { existsSync } from 'fs'; -import { mkdir } from 'fs/promises'; -import os from 'os'; -import path from 'path'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; -import chalk from 'chalk'; -import ora from 'ora'; -import * as prompts from 'prompts'; -import * as semver from 'semver'; -import tar from 'tar'; +import install from './commands/install'; -import { downloadFile, log, safeExecSync } from './utilities'; - -const pgRequired = new semver.SemVer('14.0.0'); - -const validateNodeVersion = () => { - const required = new semver.SemVer('16.0.0'); - const current = new semver.SemVer(execSync('node -v', { encoding: 'utf8', stdio: 'pipe' })); - - if (required.compare(current) > 0) { - log.error(`Logto requires NodeJS >=${required.version}, but ${current.version} found.`); - } - - if (current.major > required.major) { - log.warn( - `Logto is tested under NodeJS ^${required.version}, but version ${current.version} found.` - ); - } -}; - -const getInstancePath = async () => { - const response = await prompts.default( - [ - { - name: 'instancePath', - message: 'Where should we create your logto instance?', - type: 'text', - initial: './logto', - format: (value: string) => path.resolve(value.trim()), - validate: (value: string) => - existsSync(value) ? 'That path already exists, please try another.' : true, - }, - { - name: 'hasPostgresUrl', - message: `Logto requires PostgreSQL >=${pgRequired.version} but cannot find in the current environment.\n Do you have a remote PostgreSQL instance ready?`, - type: () => { - const pgOutput = safeExecSync('postgres --version') ?? ''; - // Filter out all brackets in the output since Homebrew will append `(Homebrew)`. - const pgArray = pgOutput.split(' ').filter((value) => !value.startsWith('(')); - const pgCurrent = semver.coerce(pgArray[pgArray.length - 1]); - - return (!pgCurrent || pgCurrent.compare(pgRequired) < 0) && 'confirm'; - }, - format: (previous) => { - if (!previous) { - log.error('Logto requires a Postgres instance to run.'); - } - }, - }, - ], +void yargs(hideBin(process.argv)) + .command( + ['init', 'i', 'install'], + 'Download and run the latest Logto release', { - onCancel: () => { - log.error('Operation cancelled'); + path: { + alias: 'p', + describe: 'Path of Logto, must be a non-existing path', + type: 'string', }, + silent: { + alias: 's', + describe: 'Entering non-interactive mode', + type: 'boolean', + }, + }, + async ({ path, silent }) => { + await install({ path, silent }); } - ); - - return String(response.instancePath); -}; - -const tryStartInstance = async (instancePath: string) => { - const response = await prompts.default({ - name: 'startInstance', - message: 'Would you like to start Logto now?', - type: 'confirm', - initial: true, - }); - - const yes = Boolean(response.startInstance); - const startCommand = `cd ${instancePath} && npm start`; - - if (yes) { - execSync(startCommand, { stdio: 'inherit' }); - } else { - log.info(`You can use ${startCommand} to start Logto. Happy hacking!`); - } -}; - -const downloadRelease = async () => { - const tarFilePath = path.resolve(os.tmpdir(), './logto.tar.gz'); - - log.info(`Download Logto to ${tarFilePath}`); - await downloadFile( - 'https://github.com/logto-io/logto/releases/latest/download/logto.tar.gz', - tarFilePath - ); - - return tarFilePath; -}; - -const decompress = async (toPath: string, tarPath: string) => { - const decompressSpinner = ora({ - text: `Decompress to ${toPath}`, - prefixText: chalk.blue('[info]'), - }).start(); - - try { - await mkdir(toPath); - await tar.extract({ file: tarPath, cwd: toPath, strip: 1 }); - } catch { - decompressSpinner.fail(); - - return; - } - - decompressSpinner.succeed(); -}; - -const main = async () => { - validateNodeVersion(); - - const instancePath = await getInstancePath(); - const tarPath = await downloadRelease(); - - await decompress(instancePath, tarPath); - await tryStartInstance(instancePath); -}; - -void main(); + ) + .demandCommand(1) + .showHelpOnFail(true) + .strict() + .parserConfiguration({ + 'dot-notation': false, + }) + .parse(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 415f161ab..79e6f73c7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,39 +23,43 @@ importers: '@silverhand/eslint-config': 1.0.0 '@silverhand/ts-config': 1.0.0 '@types/decompress': ^4.2.4 + '@types/inquirer': ^8.2.1 '@types/node': ^16.0.0 - '@types/prompts': ^2.0.14 '@types/semver': ^7.3.12 '@types/tar': ^6.1.2 + '@types/yargs': ^17.0.13 chalk: ^4.1.2 eslint: ^8.21.0 got: ^11.8.2 hpagent: ^1.0.0 + inquirer: ^8.2.2 lint-staged: ^13.0.0 ora: ^5.0.0 prettier: ^2.7.1 - prompts: ^2.4.2 rimraf: ^3.0.2 semver: ^7.3.7 tar: ^6.1.11 ts-node: ^10.9.1 typescript: ^4.7.4 + yargs: ^17.6.0 dependencies: chalk: 4.1.2 got: 11.8.3 hpagent: 1.0.0 + inquirer: 8.2.2 ora: 5.4.1 - prompts: 2.4.2 semver: 7.3.7 tar: 6.1.11 + yargs: 17.6.0 devDependencies: '@silverhand/eslint-config': 1.0.0_swk2g7ygmfleszo5c33j4vooni '@silverhand/ts-config': 1.0.0_typescript@4.7.4 '@types/decompress': 4.2.4 + '@types/inquirer': 8.2.1 '@types/node': 16.11.12 - '@types/prompts': 2.0.14 '@types/semver': 7.3.12 '@types/tar': 6.1.2 + '@types/yargs': 17.0.13 eslint: 8.21.0 lint-staged: 13.0.0 prettier: 2.7.1 @@ -4556,12 +4560,6 @@ packages: resolution: {integrity: sha512-ekoj4qOQYp7CvjX8ZDBgN86w3MqQhLE1hczEJbEIjgFEumDy+na/4AJAbLXfgEWFNB2pKadM5rPFtuSGMWK7xA==} dev: true - /@types/prompts/2.0.14: - resolution: {integrity: sha512-HZBd99fKxRWpYCErtm2/yxUZv6/PBI9J7N4TNFffl5JbrYMHBwF25DjQGTW3b3jmXq+9P6/8fCIb2ee57BFfYA==} - dependencies: - '@types/node': 17.0.23 - dev: true - /@types/prop-types/15.7.4: resolution: {integrity: sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==} dev: true @@ -4706,6 +4704,12 @@ packages: '@types/yargs-parser': 20.2.1 dev: true + /@types/yargs/17.0.13: + resolution: {integrity: sha512-9sWaruZk2JGxIQU+IhI1fhPYRcQ0UuTNuKuCW9bR5fp7qi2Llf7WDzNa17Cy7TKnh3cdxDOiyTu6gaLS0eDatg==} + dependencies: + '@types/yargs-parser': 20.2.1 + dev: true + /@types/yauzl/2.10.0: resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==} requiresBuild: true @@ -5701,6 +5705,15 @@ packages: string-width: 4.2.3 strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + dev: true + + /cliui/8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 /clone-deep/0.2.4: resolution: {integrity: sha512-we+NuQo2DHhSl+DP6jlUiAhyAjBQrYnpOk15rN6c6JSPScjiCLh8IbSU+VTcph6YS3o7mASE8a0+gbZ7ChLpgg==} @@ -9959,6 +9972,7 @@ packages: /kleur/3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} + dev: true /kleur/4.1.4: resolution: {integrity: sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA==} @@ -12279,7 +12293,7 @@ packages: hasBin: true dependencies: shell-quote: 1.7.3 - yargs: 17.4.1 + yargs: 17.6.0 /pg-int8/1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} @@ -12739,6 +12753,7 @@ packages: dependencies: kleur: 3.0.3 sisteransi: 1.0.5 + dev: true /promzard/0.3.0: resolution: {integrity: sha1-JqXW7ox97kyxIggwWs+5O6OCqe4=} @@ -13510,7 +13525,7 @@ packages: dev: true /require-directory/2.1.1: - resolution: {integrity: sha1-jGStX9MNqxyXbiNE/+f3kqam30I=} + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} /require-from-string/2.0.2: @@ -13816,6 +13831,7 @@ packages: /sisteransi/1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + dev: true /slash/3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} @@ -15715,6 +15731,19 @@ packages: string-width: 4.2.3 y18n: 5.0.8 yargs-parser: 21.0.1 + dev: true + + /yargs/17.6.0: + resolution: {integrity: sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==} + engines: {node: '>=12'} + dependencies: + cliui: 8.0.1 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.0.1 /yauzl/2.10.0: resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==}