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

feat(cli): add connector command

This commit is contained in:
Gao Sun 2022-10-11 17:38:58 +08:00
parent 04f16ef4e4
commit 4ccbe4ac65
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
6 changed files with 225 additions and 0 deletions

View file

@ -48,6 +48,7 @@
"inquirer": "^8.2.2",
"nanoid": "^3.3.4",
"ora": "^5.0.0",
"p-retry": "^4.6.1",
"roarr": "^7.11.0",
"semver": "^7.3.7",
"slonik": "^30.0.0",

View file

@ -0,0 +1,46 @@
import chalk from 'chalk';
import { CommandModule } from 'yargs';
import { oraPromise } from '../../utilities';
import {
addConnectors,
fetchOfficialConnectorList,
inquireInstancePath,
normalizePackageName,
} from './utils';
const add: CommandModule<unknown, { packages: string[]; path?: string; official: boolean }> = {
command: ['add [packages...]', 'a', 'install', 'i'],
describe: 'Add specific Logto connectors',
builder: (yargs) =>
yargs
.positional('packages', {
describe: 'The additional connector package names',
type: 'string',
array: true,
default: [],
})
.option('official', {
alias: 'o',
type: 'boolean',
default: false,
describe:
'Add all official connectors.\n' +
"If it's true, the specified package names will be ignored.",
})
.option('path', { alias: 'p', type: 'string', describe: 'The path to your Logto instance' }),
handler: async ({ packages: packageNames, path, official }) => {
const instancePath = await inquireInstancePath(path);
const packages = official
? await oraPromise(fetchOfficialConnectorList(), {
text: 'Fetch official connector list',
prefixText: chalk.blue('[info]'),
})
: packageNames.map((name) => normalizePackageName(name));
await addConnectors(instancePath, packages);
},
};
export default add;

View file

@ -0,0 +1,13 @@
import { noop } from '@silverhand/essentials';
import { CommandModule } from 'yargs';
import add from './add';
const connector: CommandModule = {
command: ['connector', 'c'],
describe: 'Command for Logto connectors',
builder: (yargs) => yargs.command(add).demandCommand(1),
handler: noop,
};
export default connector;

View file

@ -0,0 +1,161 @@
import { exec } from 'child_process';
import { existsSync } from 'fs';
import { readFile, mkdir, unlink } from 'fs/promises';
import path from 'path';
import { promisify } from 'util';
import { conditionalString } from '@silverhand/essentials';
import chalk from 'chalk';
import { ensureDir, remove } from 'fs-extra';
import inquirer from 'inquirer';
import pRetry from 'p-retry';
import tar from 'tar';
import { z } from 'zod';
import { log } from '../../utilities';
import { defaultPath } from '../install/utils';
const coreDirectory = 'packages/core';
const execPromise = promisify(exec);
export const npmPackResultGuard = z
.object({
name: z.string(),
version: z.string(),
filename: z.string(),
})
.array();
const buildPathErrorMessage = (value: string) =>
`The path ${chalk.green(value)} does not contain a Logto instance, please try another.`;
const validatePath = async (value: string) => {
const corePackageJsonPath = path.resolve(path.join(value, coreDirectory, 'package.json'));
if (!existsSync(corePackageJsonPath)) {
return buildPathErrorMessage(value);
}
const packageJson = await readFile(corePackageJsonPath, { encoding: 'utf8' });
const packageName = await z
.object({ name: z.string() })
.parseAsync(JSON.parse(packageJson))
.then(({ name }) => name)
.catch(() => '');
if (packageName !== '@logto/core') {
return buildPathErrorMessage(value);
}
return true;
};
export const inquireInstancePath = async (initialPath?: string) => {
const { instancePath } = await inquirer.prompt<{ instancePath: string }>(
{
name: 'instancePath',
message: 'Where is your Logto instance?',
type: 'input',
default: defaultPath,
filter: (value: string) => value.trim(),
validate: validatePath,
},
{ instancePath: initialPath }
);
// Validate for initialPath
const validated = await validatePath(instancePath);
if (validated !== true) {
log.error(validated);
}
return instancePath;
};
const packagePrefix = 'connector-';
export const normalizePackageName = (name: string) =>
name
.split('/')
// Prepend prefix to the last fragment if needed
.map((fragment, index, array) =>
index === array.length - 1 && !fragment.startsWith(packagePrefix) && !fragment.startsWith('@')
? packagePrefix + fragment
: fragment
)
.join('/');
export const addConnectors = async (instancePath: string, packageNames: string[]) => {
const cwd = path.join(instancePath, coreDirectory, 'connectors');
if (!existsSync(cwd)) {
await mkdir(cwd);
}
log.info('Fetch connector metadata');
const results = await Promise.all(
packageNames.map(async (packageName) => {
const run = async () => {
const { stdout } = await execPromise(`npm pack ${packageName} --json`, { cwd });
const result = npmPackResultGuard.parse(JSON.parse(stdout));
if (!result[0]) {
throw new Error(
`Unable to execute ${chalk.green('npm pack')} on package ${chalk.green(packageName)}`
);
}
const { filename, name } = result[0];
const escapedFilename = filename.replace(/\//g, '-').replace(/@/g, '');
const tarPath = path.join(cwd, escapedFilename);
const packageDirectory = path.join(cwd, name.replace(/\//g, '-'));
await remove(packageDirectory);
await ensureDir(packageDirectory);
await tar.extract({ cwd: packageDirectory, file: tarPath, strip: 1 });
await unlink(tarPath);
log.succeed(`Added ${chalk.green(name)}`);
};
try {
await pRetry(run, { retries: 2 });
} catch (error: unknown) {
console.warn(`[${packageName}]`, error);
return packageName;
}
})
);
const errorPackages = results.filter(Boolean);
const errorCount = errorPackages.length;
log.info(
errorCount
? `Finished with ${errorCount} error${conditionalString(errorCount > 1 && 's')}.`
: 'Finished'
);
if (errorCount) {
log.warn('Failed to add ' + errorPackages.map((name) => chalk.green(name)).join(', '));
}
};
const officialConnectorPrefix = '@logto/connector-';
export const fetchOfficialConnectorList = async () => {
const { stdout } = await execPromise(`npm search ${officialConnectorPrefix} --json`);
const packages = z
.object({ name: z.string() })
.transform(({ name }) => name)
.array()
.parse(JSON.parse(stdout));
return packages.filter((name) =>
['mock', 'kit'].every(
(excluded) => !name.slice(officialConnectorPrefix.length).startsWith(excluded)
)
);
};

View file

@ -3,6 +3,7 @@ import dotenv from 'dotenv';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import connector from './commands/connector';
import database from './commands/database';
import install from './commands/install';
import { cliConfig, ConfigKey } from './utilities';
@ -29,6 +30,7 @@ void yargs(hideBin(process.argv))
})
.command(install)
.command(database)
.command(connector)
.demandCommand(1)
.showHelpOnFail(false, `Specify ${chalk.green('--help')} for available options`)
.strict()

View file

@ -46,6 +46,7 @@ importers:
lint-staged: ^13.0.0
nanoid: ^3.3.4
ora: ^5.0.0
p-retry: ^4.6.1
prettier: ^2.7.1
rimraf: ^3.0.2
roarr: ^7.11.0
@ -72,6 +73,7 @@ importers:
inquirer: 8.2.2
nanoid: 3.3.4
ora: 5.4.1
p-retry: 4.6.1
roarr: 7.11.0
semver: 7.3.7
slonik: 30.1.2