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:
parent
8a0a46380d
commit
f05691b431
4 changed files with 198 additions and 140 deletions
|
@ -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",
|
||||
|
|
125
packages/cli/src/commands/install.ts
Normal file
125
packages/cli/src/commands/install.ts
Normal 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;
|
|
@ -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();
|
||||
|
|
|
@ -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==}
|
||||
|
|
Loading…
Reference in a new issue