0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-27 21:39:16 -05:00
logto/packages/cli/src/utils.ts

213 lines
5.5 KiB
TypeScript
Raw Normal View History

2022-10-03 17:52:28 +08:00
import { execSync } from 'child_process';
import { createWriteStream, existsSync } from 'fs';
import { readdir, readFile } from 'fs/promises';
2022-11-21 19:18:32 +08:00
import { createRequire } from 'module';
import path from 'path';
2022-10-03 17:52:28 +08:00
2022-10-21 13:14:17 +08:00
import type { Optional } from '@silverhand/essentials';
import { conditionalString } from '@silverhand/essentials';
2022-10-03 17:52:28 +08:00
import chalk from 'chalk';
2022-10-21 13:14:17 +08:00
import type { Progress } from 'got';
2022-11-21 19:18:32 +08:00
import { got } from 'got';
2022-10-03 19:00:44 +08:00
import { HttpsProxyAgent } from 'hpagent';
2022-10-08 23:27:43 +08:00
import inquirer from 'inquirer';
2022-12-12 13:43:23 +08:00
import type { Options } from 'ora';
2022-10-03 17:52:28 +08:00
import ora from 'ora';
import { z } from 'zod';
2022-10-03 17:52:28 +08:00
export const safeExecSync = (command: string) => {
try {
return execSync(command, { encoding: 'utf8', stdio: 'pipe' });
} catch {}
};
type Log = Readonly<{
info: typeof console.log;
2022-10-08 23:27:43 +08:00
succeed: typeof console.log;
2022-10-03 17:52:28 +08:00
warn: typeof console.log;
2022-10-05 22:46:52 +08:00
error: (...args: Parameters<typeof console.log>) => never;
2022-10-03 17:52:28 +08:00
}>;
export const log: Log = Object.freeze({
info: (...args) => {
console.log(chalk.blue('[info]'), ...args);
},
2022-10-08 23:27:43 +08:00
succeed: (...args) => {
log.info(chalk.green('✔'), ...args);
2022-10-08 23:27:43 +08:00
},
2022-10-03 17:52:28 +08:00
warn: (...args) => {
2022-10-08 23:27:43 +08:00
console.warn(chalk.yellow('[warn]'), ...args);
2022-10-03 17:52:28 +08:00
},
error: (...args) => {
2022-10-08 23:27:43 +08:00
console.error(chalk.red('[error]'), ...args);
2022-10-03 17:52:28 +08:00
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
},
});
export const downloadFile = async (url: string, destination: string) => {
2022-10-03 19:00:44 +08:00
const { HTTPS_PROXY, HTTP_PROXY, https_proxy, http_proxy } = process.env;
2022-10-03 17:52:28 +08:00
const file = createWriteStream(destination);
2022-10-03 19:00:44 +08:00
const proxy = HTTPS_PROXY ?? https_proxy ?? HTTP_PROXY ?? http_proxy;
const stream = got.stream(url, {
...(proxy && { agent: { https: new HttpsProxyAgent({ proxy }) } }),
});
2022-10-03 17:52:28 +08:00
const spinner = ora({
text: 'Connecting',
prefixText: chalk.blue('[info]'),
}).start();
stream.pipe(file);
return new Promise((resolve, reject) => {
stream.on('downloadProgress', ({ total, percent }: Progress) => {
if (!total) {
return;
}
// eslint-disable-next-line @silverhand/fp/no-mutation
spinner.text = `${(percent * 100).toFixed(1)}%`;
});
file.on('error', (error) => {
spinner.fail();
reject(error.message);
});
file.on('finish', () => {
file.close();
spinner.succeed();
resolve(file);
});
});
};
2022-10-05 02:30:37 +08:00
export const getPathInModule = (moduleName: string, relativePath = '/') =>
// https://stackoverflow.com/a/49455609/12514940
path.join(
2023-01-09 19:39:56 +08:00
path.dirname(createRequire(import.meta.url).resolve(`${moduleName}/package.json`)),
relativePath
);
2022-10-07 23:31:13 +08:00
export const oraPromise = async <T>(
promise: PromiseLike<T>,
2022-12-12 13:43:23 +08:00
options?: Options,
2022-10-07 23:31:13 +08:00
exitOnError = false
) => {
const spinner = ora(options).start();
try {
const result = await promise;
spinner.succeed();
return result;
} catch (error: unknown) {
spinner.fail();
if (exitOnError) {
log.error(error);
}
throw error;
}
};
export const isTty = () => process.stdin.isTTY;
2022-10-10 23:55:07 +08:00
export enum ConfigKey {
DatabaseUrl = 'DB_URL',
}
2022-10-10 23:55:07 +08:00
export const cliConfig = new Map<ConfigKey, Optional<string>>();
export type GetCliConfigWithPrompt = {
key: ConfigKey;
2022-10-08 23:27:43 +08:00
readableKey: string;
comments?: string;
defaultValue?: string;
};
2022-10-10 23:55:07 +08:00
export const getCliConfigWithPrompt = async ({
key,
readableKey,
comments,
defaultValue,
}: GetCliConfigWithPrompt) => {
if (cliConfig.has(key) || !isTty()) {
return cliConfig.get(key);
}
const { input } = await inquirer.prompt<{ input?: string }>({
type: 'input',
name: 'input',
message: `Enter your ${readableKey}${conditionalString(comments && ' ' + comments)}`,
default: defaultValue,
});
2022-10-08 23:27:43 +08:00
2022-10-10 23:55:07 +08:00
cliConfig.set(key, input);
2022-10-10 23:55:07 +08:00
return input;
2022-10-08 23:27:43 +08:00
};
2022-10-12 23:07:57 +08:00
// https://stackoverflow.com/a/53187807/12514940
/**
* Returns the index of the last element in the array where predicate is true, and -1
* otherwise.
* @param array The source array to search in
* @param predicate find calls predicate once for each element of the array, in descending
* order, until it finds one where predicate returns true. If such an element is found,
* findLastIndex immediately returns that element index. Otherwise, findLastIndex returns -1.
*/
export function findLastIndex<T>(
array: readonly T[],
predicate: (value: T, index: number, object: readonly T[]) => boolean
): number {
// eslint-disable-next-line @silverhand/fp/no-let
let { length } = array;
// eslint-disable-next-line @silverhand/fp/no-mutation
while (length--) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (predicate(array[length]!, length, array)) {
return length;
}
}
return -1;
}
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 getConnectorPackagesFromDirectory = async (directory: string) => {
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'
);
};