diff --git a/packages/cli/src/commands/connector/add.ts b/packages/cli/src/commands/connector/add.ts index 0e59c6ffd..931957187 100644 --- a/packages/cli/src/commands/connector/add.ts +++ b/packages/cli/src/commands/connector/add.ts @@ -3,16 +3,19 @@ import { CommandModule } from 'yargs'; import { log } from '../../utilities'; import { addConnectors, addOfficialConnectors, inquireInstancePath } from './utils'; -const add: CommandModule = { +const add: CommandModule< + { path?: string }, + { 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', + describe: 'The connector package names to add', type: 'string', array: true, - default: [], + default: undefined, }) .option('official', { alias: 'o', @@ -21,17 +24,19 @@ const add: CommandModule { const instancePath = await inquireInstancePath(path); if (official) { await addOfficialConnectors(instancePath); + } else { + if (!packageNames?.length) { + log.error('No connector name provided'); + } + await addConnectors(instancePath, packageNames); } - await addConnectors(instancePath, packageNames); - log.info('Restart your Logto instance to get the changes reflected.'); }, }; diff --git a/packages/cli/src/commands/connector/index.ts b/packages/cli/src/commands/connector/index.ts index 0e30e34dd..56ae4bcef 100644 --- a/packages/cli/src/commands/connector/index.ts +++ b/packages/cli/src/commands/connector/index.ts @@ -2,11 +2,23 @@ import { noop } from '@silverhand/essentials'; import { CommandModule } from 'yargs'; import add from './add'; +import list from './list'; +import remove from './remove'; const connector: CommandModule = { command: ['connector', 'c'], describe: 'Command for Logto connectors', - builder: (yargs) => yargs.command(add).demandCommand(1), + builder: (yargs) => + yargs + .option('path', { + alias: 'p', + type: 'string', + describe: 'The path to your Logto instance directory', + }) + .command(add) + .command(list) + .command(remove) + .demandCommand(1), handler: noop, }; diff --git a/packages/cli/src/commands/connector/list.ts b/packages/cli/src/commands/connector/list.ts new file mode 100644 index 000000000..ae2af5f11 --- /dev/null +++ b/packages/cli/src/commands/connector/list.ts @@ -0,0 +1,30 @@ +import chalk from 'chalk'; +import { CommandModule } from 'yargs'; + +import { getConnectorPackagesFrom, isOfficialConnector } from './utils'; + +const logConnectorNames = (type: string, names: string[]) => { + if (names.length === 0) { + return; + } + + console.log(); + console.log(chalk.blue(type)); + console.log(names.map((value) => ' ' + value).join('\n')); +}; + +const list: CommandModule<{ path?: string }, { path?: string }> = { + command: ['list', 'l'], + describe: 'List added Logto connectors', + handler: async ({ path: inputPath }) => { + const packages = await getConnectorPackagesFrom(inputPath); + const packageNames = packages.map(({ name }) => name); + const officialPackages = packageNames.filter((name) => isOfficialConnector(name)); + const thirdPartyPackages = packageNames.filter((name) => !isOfficialConnector(name)); + + logConnectorNames('official'.toUpperCase(), officialPackages); + logConnectorNames('3rd-party'.toUpperCase(), thirdPartyPackages); + }, +}; + +export default list; diff --git a/packages/cli/src/commands/connector/remove.ts b/packages/cli/src/commands/connector/remove.ts new file mode 100644 index 000000000..c3a4f37da --- /dev/null +++ b/packages/cli/src/commands/connector/remove.ts @@ -0,0 +1,59 @@ +import chalk from 'chalk'; +import fsExtra from 'fs-extra'; +import { CommandModule } from 'yargs'; + +import { log } from '../../utilities'; +import { getConnectorPackagesFrom } from './utils'; + +const remove: CommandModule<{ path?: string }, { path?: string; packages?: string[] }> = { + command: ['remove [packages...]', 'rm', 'delete'], + describe: 'Remove existing Logto connectors', + builder: (yargs) => + yargs.positional('packages', { + describe: 'The connector package names to remove', + type: 'string', + array: true, + default: undefined, + }), + handler: async ({ path: inputPath, packages: packageNames }) => { + if (!packageNames?.length) { + log.error('No connector name provided'); + } + + const existingPackages = await getConnectorPackagesFrom(inputPath); + const notFoundPackageNames = packageNames.filter( + (current) => !existingPackages.some(({ name }) => current === name) + ); + + if (notFoundPackageNames.length > 0) { + log.error( + `Cannot remove ${notFoundPackageNames + .map((name) => chalk.green(name)) + .join(', ')}: not found in your Logto instance directory` + ); + } + + const okSymbol = Symbol('Connector removed'); + const result = await Promise.all( + packageNames.map(async (current) => { + const packageInfo = existingPackages.find(({ name }) => name === current); + + try { + await fsExtra.remove(packageInfo?.path ?? ''); + + return okSymbol; + } catch (error: unknown) { + log.warn(`Error while removing ${chalk.green(packageInfo?.name)}`); + log.warn(error); + + return error; + } + }) + ); + const errorCount = result.filter((value) => value !== okSymbol).length; + + log.info(`Removed ${result.length - errorCount} connectors`); + }, +}; + +export default remove; diff --git a/packages/cli/src/commands/connector/utils.ts b/packages/cli/src/commands/connector/utils.ts index 9fbe72fdf..48e876c7c 100644 --- a/packages/cli/src/commands/connector/utils.ts +++ b/packages/cli/src/commands/connector/utils.ts @@ -1,6 +1,6 @@ import { exec } from 'child_process'; import { existsSync } from 'fs'; -import { readFile, mkdir, unlink } from 'fs/promises'; +import { readFile, mkdir, unlink, readdir } from 'fs/promises'; import path from 'path'; import { promisify } from 'util'; @@ -86,8 +86,50 @@ export const normalizePackageName = (name: string) => ) .join('/'); +const getConnectorDirectory = (instancePath: string) => + path.join(instancePath, coreDirectory, connectorDirectory); + +export const isOfficialConnector = (packageName: string) => + packageName.startsWith('@logto/connector-'); + +const getConnectorPackageName = async (directory: string) => { + const filePath = path.join(directory, 'package.json'); + + if (!existsSync(filePath)) { + return; + } + + const json = await readFile(filePath, 'utf8'); + const { name } = z.object({ name: z.string() }).parse(JSON.parse(json)); + + if (name.startsWith('connector-') || Boolean(name.split('/')[1]?.startsWith('connector-'))) { + return name; + } +}; + +export type ConnectorPackage = { + name: string; + path: string; +}; + +export const getConnectorPackagesFrom = async (instancePath?: string) => { + const directory = getConnectorDirectory(await inquireInstancePath(instancePath)); + const content = await readdir(directory, 'utf8'); + const rawPackages = await Promise.all( + content.map(async (value) => { + const currentDirectory = path.join(directory, value); + + return { name: await getConnectorPackageName(currentDirectory), path: currentDirectory }; + }) + ); + + return rawPackages.filter( + (packageInfo): packageInfo is ConnectorPackage => typeof packageInfo.name === 'string' + ); +}; + export const addConnectors = async (instancePath: string, packageNames: string[]) => { - const cwd = path.join(instancePath, coreDirectory, connectorDirectory); + const cwd = getConnectorDirectory(instancePath); if (!existsSync(cwd)) { await mkdir(cwd);