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:
parent
04f16ef4e4
commit
4ccbe4ac65
6 changed files with 225 additions and 0 deletions
|
@ -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",
|
||||
|
|
46
packages/cli/src/commands/connector/add.ts
Normal file
46
packages/cli/src/commands/connector/add.ts
Normal 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;
|
13
packages/cli/src/commands/connector/index.ts
Normal file
13
packages/cli/src/commands/connector/index.ts
Normal 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;
|
161
packages/cli/src/commands/connector/utils.ts
Normal file
161
packages/cli/src/commands/connector/utils.ts
Normal 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)
|
||||
)
|
||||
);
|
||||
};
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue