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",
|
"inquirer": "^8.2.2",
|
||||||
"nanoid": "^3.3.4",
|
"nanoid": "^3.3.4",
|
||||||
"ora": "^5.0.0",
|
"ora": "^5.0.0",
|
||||||
|
"p-retry": "^4.6.1",
|
||||||
"roarr": "^7.11.0",
|
"roarr": "^7.11.0",
|
||||||
"semver": "^7.3.7",
|
"semver": "^7.3.7",
|
||||||
"slonik": "^30.0.0",
|
"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 yargs from 'yargs';
|
||||||
import { hideBin } from 'yargs/helpers';
|
import { hideBin } from 'yargs/helpers';
|
||||||
|
|
||||||
|
import connector from './commands/connector';
|
||||||
import database from './commands/database';
|
import database from './commands/database';
|
||||||
import install from './commands/install';
|
import install from './commands/install';
|
||||||
import { cliConfig, ConfigKey } from './utilities';
|
import { cliConfig, ConfigKey } from './utilities';
|
||||||
|
@ -29,6 +30,7 @@ void yargs(hideBin(process.argv))
|
||||||
})
|
})
|
||||||
.command(install)
|
.command(install)
|
||||||
.command(database)
|
.command(database)
|
||||||
|
.command(connector)
|
||||||
.demandCommand(1)
|
.demandCommand(1)
|
||||||
.showHelpOnFail(false, `Specify ${chalk.green('--help')} for available options`)
|
.showHelpOnFail(false, `Specify ${chalk.green('--help')} for available options`)
|
||||||
.strict()
|
.strict()
|
||||||
|
|
|
@ -46,6 +46,7 @@ importers:
|
||||||
lint-staged: ^13.0.0
|
lint-staged: ^13.0.0
|
||||||
nanoid: ^3.3.4
|
nanoid: ^3.3.4
|
||||||
ora: ^5.0.0
|
ora: ^5.0.0
|
||||||
|
p-retry: ^4.6.1
|
||||||
prettier: ^2.7.1
|
prettier: ^2.7.1
|
||||||
rimraf: ^3.0.2
|
rimraf: ^3.0.2
|
||||||
roarr: ^7.11.0
|
roarr: ^7.11.0
|
||||||
|
@ -72,6 +73,7 @@ importers:
|
||||||
inquirer: 8.2.2
|
inquirer: 8.2.2
|
||||||
nanoid: 3.3.4
|
nanoid: 3.3.4
|
||||||
ora: 5.4.1
|
ora: 5.4.1
|
||||||
|
p-retry: 4.6.1
|
||||||
roarr: 7.11.0
|
roarr: 7.11.0
|
||||||
semver: 7.3.7
|
semver: 7.3.7
|
||||||
slonik: 30.1.2
|
slonik: 30.1.2
|
||||||
|
|
Loading…
Reference in a new issue