0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00

feat(cli): command init/i/install

This commit is contained in:
Gao Sun 2022-10-04 13:45:27 +08:00
parent 8a0a46380d
commit f05691b431
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
4 changed files with 198 additions and 140 deletions

View file

@ -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",

View file

@ -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;

View file

@ -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(
[
void yargs(hideBin(process.argv))
.command(
['init', 'i', 'install'],
'Download and run the latest Logto release',
{
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,
path: {
alias: 'p',
describe: 'Path of Logto, must be a non-existing path',
type: 'string',
},
{
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';
silent: {
alias: 's',
describe: 'Entering non-interactive mode',
type: 'boolean',
},
format: (previous) => {
if (!previous) {
log.error('Logto requires a Postgres instance to run.');
},
async ({ path, silent }) => {
await install({ path, silent });
}
},
},
],
{
onCancel: () => {
log.error('Operation cancelled');
},
}
);
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();

View file

@ -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==}