587 lines
18 KiB
TypeScript
587 lines
18 KiB
TypeScript
import * as crypto from 'crypto';
|
||
import * as fs from 'fs';
|
||
import * as path from 'path';
|
||
import {
|
||
parseQqbotPluginManifest,
|
||
type QqbotPluginManifest,
|
||
} from '../../src/modules/qqbot/plugin-platform/domain/manifest';
|
||
|
||
export type QqbotPluginCliCommand =
|
||
| 'create'
|
||
| 'install-local'
|
||
| 'pack'
|
||
| 'validate';
|
||
|
||
export type QqbotPluginCliOptions = {
|
||
cwd?: string;
|
||
stderr?: (message: string) => void;
|
||
stdout?: (message: string) => void;
|
||
};
|
||
|
||
export type QqbotPluginCliResult = {
|
||
command: QqbotPluginCliCommand;
|
||
exitCode: number;
|
||
installationId?: string;
|
||
packageHash?: string;
|
||
packagePath?: string;
|
||
pluginKey?: string;
|
||
pluginRoot?: string;
|
||
version?: string;
|
||
versionId?: string;
|
||
};
|
||
|
||
type PackedPluginFile = {
|
||
path: string;
|
||
sha256: string;
|
||
};
|
||
|
||
type PackedPlugin = {
|
||
contentHash: string;
|
||
files: PackedPluginFile[];
|
||
manifest: QqbotPluginManifest;
|
||
};
|
||
|
||
const templateRoot = path.join(__dirname, 'templates', 'basic');
|
||
const packageExtension = '.qqbot-plugin.json';
|
||
const maxPackageFileBytes = 5 * 1024 * 1024;
|
||
const pluginKeyPattern = /^[a-z][a-z0-9-]{2,63}$/;
|
||
const allowedTopLevelEntries = new Set([
|
||
'LICENSE',
|
||
'LICENSE.txt',
|
||
'README.md',
|
||
'assets',
|
||
'migrations',
|
||
'plugin.json',
|
||
'src',
|
||
'tests',
|
||
]);
|
||
|
||
/**
|
||
* 执行 脚本或 CLI流程。
|
||
* @param content - 待处理内容;驱动 `crypto.createHash()` 的 CLI步骤。
|
||
*/
|
||
const sha256 = (content: Buffer | string) =>
|
||
crypto.createHash('sha256').update(content).digest('hex');
|
||
|
||
/**
|
||
* 执行 脚本或 CLI流程。
|
||
* @param value - 待稳定序列化的值;转换 CLI列表项。
|
||
* @returns 脚本或 CLI渲染后的图片、画布或文本。
|
||
*/
|
||
const stableStringify = (value: unknown): string => {
|
||
if (Array.isArray(value)) {
|
||
return `[${value.map((item) => stableStringify(item)).join(',')}]`;
|
||
}
|
||
if (value && typeof value === 'object') {
|
||
const record = value as Record<string, unknown>;
|
||
return `{${Object.keys(record)
|
||
.filter((key) => record[key] !== undefined)
|
||
.sort()
|
||
.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`)
|
||
.join(',')}}`;
|
||
}
|
||
|
||
return JSON.stringify(value);
|
||
};
|
||
|
||
/**
|
||
* 转换 脚本或 CLI输入。
|
||
* @param pluginKey - pluginKey 输入;影响 formatPluginName 的返回值。
|
||
*/
|
||
const formatPluginName = (pluginKey: string) => {
|
||
return pluginKey
|
||
.split('-')
|
||
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
|
||
.join(' ');
|
||
};
|
||
|
||
/**
|
||
* 查询 脚本或 CLI数据。
|
||
* @param argv - CLI 参数数组;执行 `argv.indexOf()` 对应的 CLI步骤。
|
||
* @param optionName - optionName 输入;驱动 `argv.indexOf()` 的 CLI步骤。
|
||
*/
|
||
const getOptionValue = (argv: string[], optionName: string) => {
|
||
const optionIndex = argv.indexOf(optionName);
|
||
if (optionIndex < 0) return undefined;
|
||
|
||
const value = argv[optionIndex + 1];
|
||
if (!value || value.startsWith('--')) {
|
||
throw new Error(`Usage: ${optionName} <path>`);
|
||
}
|
||
return value;
|
||
};
|
||
|
||
/**
|
||
* 判断 脚本或 CLI条件。
|
||
* @param parent - parent 输入;定位文件系统路径。
|
||
* @param child - child 输入;定位文件系统路径。
|
||
*/
|
||
const isInsideDirectory = (parent: string, child: string) => {
|
||
const relativePath = path.relative(parent, child);
|
||
return (
|
||
relativePath === '' ||
|
||
(!!relativePath &&
|
||
!relativePath.startsWith('..') &&
|
||
!path.isAbsolute(relativePath))
|
||
);
|
||
};
|
||
|
||
/**
|
||
* 查询 脚本或 CLI数据。
|
||
* @param cwd - 当前工作目录;定位文件系统路径。
|
||
*/
|
||
const findWorkspaceRoot = (cwd: string) => {
|
||
let current = path.resolve(cwd);
|
||
while (true) {
|
||
if (fs.existsSync(path.join(current, 'AGENTS.md'))) return current;
|
||
const parent = path.dirname(current);
|
||
if (parent === current) return null;
|
||
current = parent;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 查询 脚本或 CLI数据。
|
||
* @param options - CLI列表;使用 `cwd` 字段生成结果。
|
||
*/
|
||
const getControlledRoots = (options: Required<QqbotPluginCliOptions>) => {
|
||
const cwd = path.resolve(options.cwd);
|
||
const roots = [cwd];
|
||
|
||
if (fs.existsSync(path.join(cwd, 'package.json'))) {
|
||
const workspaceRoot = findWorkspaceRoot(cwd);
|
||
const artifactRoot = workspaceRoot || path.resolve(cwd, '..', '..');
|
||
roots.push(path.join(artifactRoot, '.kt-workspace'));
|
||
}
|
||
|
||
return roots;
|
||
};
|
||
|
||
/**
|
||
* 解析Controlled Path。
|
||
* @param pathArg - CLI路径;定位文件系统路径。
|
||
* @param options - CLI列表;使用 `cwd` 字段生成结果。
|
||
* @param label - 字典展示文本;影响 resolveControlledPath 的返回值。
|
||
*/
|
||
const resolveControlledPath = (
|
||
pathArg: string,
|
||
options: Required<QqbotPluginCliOptions>,
|
||
label: string,
|
||
) => {
|
||
const resolvedPath = path.resolve(options.cwd, pathArg);
|
||
const allowed = getControlledRoots(options).some((root) =>
|
||
isInsideDirectory(root, resolvedPath),
|
||
);
|
||
if (!allowed) {
|
||
throw new Error(`Unsafe ${label} output path: ${resolvedPath}`);
|
||
}
|
||
return resolvedPath;
|
||
};
|
||
|
||
/**
|
||
* 执行 脚本或 CLI流程。
|
||
* @param pluginKey - pluginKey 输入;决定 CLI条件分支。
|
||
* @returns 脚本或 CLI渲染后的图片、画布或文本。
|
||
*/
|
||
function assertPluginKey(
|
||
pluginKey: string | undefined,
|
||
): asserts pluginKey is string {
|
||
if (!pluginKey) throw new Error('Usage: qqbot-plugin create <pluginKey>');
|
||
if (!pluginKeyPattern.test(pluginKey)) {
|
||
throw new Error(`Invalid QQBot plugin key: ${pluginKey}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 读取 脚本或 CLI资源。
|
||
* @param pluginRoot - pluginRoot 输入;定位文件系统路径。
|
||
*/
|
||
const readManifest = (pluginRoot: string) => {
|
||
const manifestPath = path.join(pluginRoot, 'plugin.json');
|
||
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as unknown;
|
||
|
||
return parseQqbotPluginManifest(manifest, { pluginRoot });
|
||
};
|
||
|
||
/**
|
||
* 执行 脚本或 CLI流程。
|
||
* @param sourcePath - CLI路径;读取本地文件内容。
|
||
* @param targetPath - CLI路径;驱动 `fs.mkdirSync()`、`fs.writeFileSync()` 的 CLI步骤。
|
||
* @param replacements - CLI列表;驱动 `Object.entries()` 的 CLI步骤。
|
||
*/
|
||
const copyTemplateFile = (
|
||
sourcePath: string,
|
||
targetPath: string,
|
||
replacements: Record<string, string>,
|
||
) => {
|
||
const content = fs.readFileSync(sourcePath);
|
||
const textContent = content.toString('utf8');
|
||
const rendered = Object.entries(replacements).reduce(
|
||
(result, [key, value]) => result.replaceAll(key, value),
|
||
textContent,
|
||
);
|
||
|
||
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
||
fs.writeFileSync(targetPath, rendered);
|
||
};
|
||
|
||
/**
|
||
* 执行 脚本或 CLI流程。
|
||
* @param sourceRoot - sourceRoot 输入;定位文件系统路径。
|
||
* @param targetRoot - targetRoot 输入;定位文件系统路径。
|
||
* @param replacements - CLI列表;驱动 `copyTemplateDirectory()`、`copyTemplateFile()` 的 CLI步骤。
|
||
*/
|
||
const copyTemplateDirectory = (
|
||
sourceRoot: string,
|
||
targetRoot: string,
|
||
replacements: Record<string, string>,
|
||
) => {
|
||
for (const entry of fs.readdirSync(sourceRoot, { withFileTypes: true })) {
|
||
const sourcePath = path.join(sourceRoot, entry.name);
|
||
const targetPath = path.join(targetRoot, entry.name);
|
||
|
||
if (entry.isDirectory()) {
|
||
copyTemplateDirectory(sourcePath, targetPath, replacements);
|
||
continue;
|
||
}
|
||
|
||
copyTemplateFile(sourcePath, targetPath, replacements);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 列出Package Files。
|
||
* @param pluginRoot - pluginRoot 输入;驱动 `relative()`、`visit()` 的 CLI步骤。
|
||
* @returns 脚本或 CLI查询结果。
|
||
*/
|
||
const listPackageFiles = (pluginRoot: string): PackedPluginFile[] => {
|
||
const files: PackedPluginFile[] = [];
|
||
const ignoredRoots = new Set(['dist', 'node_modules']);
|
||
|
||
/**
|
||
* 遍历 脚本或 CLI结构。
|
||
* @param current - current 输入;定位文件系统路径。
|
||
*/
|
||
const visit = (current: string) => {
|
||
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
||
if (current === pluginRoot && ignoredRoots.has(entry.name)) continue;
|
||
if (entry.name.startsWith('.')) {
|
||
throw new Error(
|
||
`Hidden files are not allowed in plugin packages: ${entry.name}`,
|
||
);
|
||
}
|
||
if (entry.isSymbolicLink()) {
|
||
throw new Error(
|
||
`Symbolic links are not allowed in plugin packages: ${entry.name}`,
|
||
);
|
||
}
|
||
|
||
const absolutePath = path.join(current, entry.name);
|
||
const relativePath = path
|
||
.relative(pluginRoot, absolutePath)
|
||
.replace(/\\/g, '/');
|
||
const topLevelEntry = relativePath.split('/')[0];
|
||
if (!allowedTopLevelEntries.has(topLevelEntry)) {
|
||
throw new Error(
|
||
`Plugin package path is not whitelisted: ${relativePath}`,
|
||
);
|
||
}
|
||
if (entry.isDirectory()) {
|
||
visit(absolutePath);
|
||
continue;
|
||
}
|
||
const fileSize = fs.statSync(absolutePath).size;
|
||
if (fileSize > maxPackageFileBytes) {
|
||
throw new Error(`Plugin package file is too large: ${relativePath}`);
|
||
}
|
||
|
||
files.push({
|
||
path: relativePath,
|
||
sha256: sha256(fs.readFileSync(absolutePath)),
|
||
});
|
||
}
|
||
};
|
||
|
||
visit(pluginRoot);
|
||
return files.sort((a, b) => a.path.localeCompare(b.path));
|
||
};
|
||
|
||
/**
|
||
* 执行 脚本或 CLI流程。
|
||
* @param pluginRoot - pluginRoot 输入;定位文件系统路径。
|
||
*/
|
||
const assertPluginSourceBoundary = (pluginRoot: string) => {
|
||
const forbiddenPatterns: Array<[RegExp, string]> = [
|
||
[/\bfrom\s+['"]@nestjs\//, '@nestjs imports'],
|
||
[/\bfrom\s+['"](?:node:)?fs['"]/, 'fs imports'],
|
||
[/\brequire\(['"](?:node:)?fs['"]\)/, 'fs require'],
|
||
[/\bfrom\s+['"]axios['"]/, 'axios imports'],
|
||
[/\brequire\(['"]axios['"]\)/, 'axios require'],
|
||
[/\bprocess\.env\b/, 'process.env access'],
|
||
];
|
||
|
||
for (const file of listPackageFiles(pluginRoot)) {
|
||
if (!/\.[cm]?[tj]sx?$/.test(file.path)) continue;
|
||
const filePath = path.join(pluginRoot, file.path);
|
||
const source = fs.readFileSync(filePath, 'utf8');
|
||
const violation = forbiddenPatterns.find(([pattern]) =>
|
||
pattern.test(source),
|
||
);
|
||
if (violation) {
|
||
throw new Error(
|
||
`Forbidden plugin source boundary violation in ${file.path}: ${violation[1]}`,
|
||
);
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 创建 脚本或 CLI对象或配置。
|
||
* @param pluginRoot - pluginRoot 输入;驱动 `readManifest()`、`assertPluginSourceBoundary()`、`listPackageFiles()` 的 CLI步骤。
|
||
* @returns 创建后的 脚本或 CLI对象或配置。
|
||
*/
|
||
const buildPackedPlugin = (pluginRoot: string): PackedPlugin => {
|
||
const manifest = readManifest(pluginRoot);
|
||
assertPluginSourceBoundary(pluginRoot);
|
||
const files = listPackageFiles(pluginRoot);
|
||
const contentHash = sha256(
|
||
stableStringify({
|
||
files,
|
||
manifest,
|
||
}),
|
||
);
|
||
|
||
return {
|
||
contentHash,
|
||
files,
|
||
manifest,
|
||
};
|
||
};
|
||
|
||
/**
|
||
* 执行 脚本或 CLI流程。
|
||
* @param packedPlugin - packedPlugin 输入;使用 `files`、`manifest`、`contentHash` 字段生成结果。
|
||
*/
|
||
const assertPackageIntegrity = (packedPlugin: PackedPlugin) => {
|
||
const expectedHash = sha256(
|
||
stableStringify({
|
||
files: packedPlugin.files,
|
||
manifest: packedPlugin.manifest,
|
||
}),
|
||
);
|
||
|
||
if (packedPlugin.contentHash !== expectedHash) {
|
||
throw new Error('QQBot plugin package content hash mismatch.');
|
||
}
|
||
|
||
parseQqbotPluginManifest(packedPlugin.manifest);
|
||
};
|
||
|
||
/**
|
||
* 创建 脚本或 CLI对象或配置。
|
||
* @param pluginKey - pluginKey 输入;驱动 `assertPluginKey()`、`resolveControlledPath()`、`formatPluginName()` 的 CLI步骤。
|
||
* @param options - CLI列表;使用 `cwd` 字段生成结果。
|
||
* @param outputPathArg - CLI路径;驱动 `resolveControlledPath()` 的 CLI步骤。
|
||
* @returns 创建后的 脚本或 CLI对象或配置。
|
||
*/
|
||
const createPlugin = (
|
||
pluginKey: string | undefined,
|
||
options: Required<QqbotPluginCliOptions>,
|
||
outputPathArg?: string,
|
||
): QqbotPluginCliResult => {
|
||
assertPluginKey(pluginKey);
|
||
|
||
const pluginRoot = outputPathArg
|
||
? resolveControlledPath(outputPathArg, options, 'create')
|
||
: path.join(options.cwd, 'plugins', pluginKey);
|
||
if (fs.existsSync(pluginRoot)) {
|
||
throw new Error(`Plugin already exists: ${pluginRoot}`);
|
||
}
|
||
|
||
copyTemplateDirectory(templateRoot, pluginRoot, {
|
||
__PLUGIN_KEY__: pluginKey,
|
||
__PLUGIN_NAME__: formatPluginName(pluginKey),
|
||
});
|
||
fs.mkdirSync(path.join(pluginRoot, 'assets'), { recursive: true });
|
||
fs.mkdirSync(path.join(pluginRoot, 'migrations'), { recursive: true });
|
||
fs.mkdirSync(path.join(pluginRoot, 'src', 'application'), {
|
||
recursive: true,
|
||
});
|
||
fs.mkdirSync(path.join(pluginRoot, 'src', 'domain'), { recursive: true });
|
||
fs.mkdirSync(path.join(pluginRoot, 'src', 'events'), { recursive: true });
|
||
fs.mkdirSync(path.join(pluginRoot, 'src', 'infrastructure'), {
|
||
recursive: true,
|
||
});
|
||
fs.mkdirSync(path.join(pluginRoot, 'src', 'operations'), { recursive: true });
|
||
fs.mkdirSync(path.join(pluginRoot, 'tests'), { recursive: true });
|
||
readManifest(pluginRoot);
|
||
|
||
options.stdout(`Created QQBot plugin: ${pluginRoot}`);
|
||
return {
|
||
command: 'create',
|
||
exitCode: 0,
|
||
pluginKey,
|
||
pluginRoot,
|
||
version: '0.1.0',
|
||
};
|
||
};
|
||
|
||
/**
|
||
* 判断 脚本或 CLI条件。
|
||
* @param pluginRootArg - pluginRootArg 输入;定位文件系统路径。
|
||
* @param options - CLI列表;使用 `cwd` 字段生成结果。
|
||
* @returns 脚本或 CLI产出的 QqbotPluginCliResult。
|
||
*/
|
||
const validatePlugin = (
|
||
pluginRootArg: string | undefined,
|
||
options: Required<QqbotPluginCliOptions>,
|
||
): QqbotPluginCliResult => {
|
||
if (!pluginRootArg) throw new Error('Usage: qqbot-plugin validate <path>');
|
||
|
||
const pluginRoot = path.resolve(options.cwd, pluginRootArg);
|
||
const manifest = readManifest(pluginRoot);
|
||
assertPluginSourceBoundary(pluginRoot);
|
||
options.stdout(
|
||
`Validated QQBot plugin: ${manifest.pluginKey}@${manifest.version}`,
|
||
);
|
||
|
||
return {
|
||
command: 'validate',
|
||
exitCode: 0,
|
||
pluginKey: manifest.pluginKey,
|
||
pluginRoot,
|
||
version: manifest.version,
|
||
};
|
||
};
|
||
|
||
/**
|
||
* 执行 脚本或 CLI流程。
|
||
* @param pluginRootArg - pluginRootArg 输入;定位文件系统路径。
|
||
* @param options - CLI列表;使用 `cwd` 字段生成结果。
|
||
* @param outputPathArg - CLI路径;驱动 `resolveControlledPath()` 的 CLI步骤。
|
||
* @returns 脚本或 CLI产出的 QqbotPluginCliResult。
|
||
*/
|
||
const packPlugin = (
|
||
pluginRootArg: string | undefined,
|
||
options: Required<QqbotPluginCliOptions>,
|
||
outputPathArg?: string,
|
||
): QqbotPluginCliResult => {
|
||
if (!pluginRootArg) throw new Error('Usage: qqbot-plugin pack <path>');
|
||
|
||
const pluginRoot = path.resolve(options.cwd, pluginRootArg);
|
||
const packedPlugin = buildPackedPlugin(pluginRoot);
|
||
const shortHash = packedPlugin.contentHash.slice(0, 12);
|
||
const packageRoot = outputPathArg
|
||
? resolveControlledPath(outputPathArg, options, 'pack')
|
||
: path.join(pluginRoot, 'dist');
|
||
const packagePath = path.join(
|
||
packageRoot,
|
||
`${packedPlugin.manifest.pluginKey}-${packedPlugin.manifest.version}-${shortHash}${packageExtension}`,
|
||
);
|
||
|
||
fs.mkdirSync(path.dirname(packagePath), { recursive: true });
|
||
fs.writeFileSync(packagePath, `${JSON.stringify(packedPlugin, null, 2)}\n`);
|
||
options.stdout(`Packed QQBot plugin: ${packagePath}`);
|
||
|
||
return {
|
||
command: 'pack',
|
||
exitCode: 0,
|
||
packageHash: packedPlugin.contentHash,
|
||
packagePath,
|
||
pluginKey: packedPlugin.manifest.pluginKey,
|
||
pluginRoot,
|
||
version: packedPlugin.manifest.version,
|
||
};
|
||
};
|
||
|
||
/**
|
||
* 执行 脚本或 CLI流程。
|
||
* @param packagePathArg - CLI路径;定位文件系统路径。
|
||
* @param options - CLI列表;使用 `cwd` 字段生成结果。
|
||
* @returns 脚本或 CLI产出的 QqbotPluginCliResult。
|
||
*/
|
||
const installLocalPlugin = (
|
||
packagePathArg: string | undefined,
|
||
options: Required<QqbotPluginCliOptions>,
|
||
): QqbotPluginCliResult => {
|
||
if (!packagePathArg) {
|
||
throw new Error('Usage: qqbot-plugin install-local <package>');
|
||
}
|
||
|
||
const packagePath = path.resolve(options.cwd, packagePathArg);
|
||
const packedPlugin = JSON.parse(
|
||
fs.readFileSync(packagePath, 'utf8'),
|
||
) as PackedPlugin;
|
||
assertPackageIntegrity(packedPlugin);
|
||
const versionId = sha256(
|
||
`version:${packedPlugin.manifest.pluginKey}:${packedPlugin.manifest.version}:${packedPlugin.contentHash}`,
|
||
).slice(0, 16);
|
||
const installationId = sha256(
|
||
`installation:${packagePath}:${packedPlugin.contentHash}`,
|
||
).slice(0, 16);
|
||
options.stdout(
|
||
`Installed local QQBot plugin package: ${packedPlugin.manifest.pluginKey}@${packedPlugin.manifest.version}`,
|
||
);
|
||
|
||
return {
|
||
command: 'install-local',
|
||
exitCode: 0,
|
||
installationId,
|
||
packageHash: packedPlugin.contentHash,
|
||
packagePath,
|
||
pluginKey: packedPlugin.manifest.pluginKey,
|
||
version: packedPlugin.manifest.version,
|
||
versionId,
|
||
};
|
||
};
|
||
|
||
/**
|
||
* 执行Qqbot Plugin Cli。
|
||
* @param argv - CLI 参数数组;驱动 `createPlugin()`、`installLocalPlugin()`、`packPlugin()`、`validatePlugin()` 的 CLI步骤。
|
||
* @param options - CLI列表;使用 `cwd`、`stderr`、`stdout` 字段生成结果。
|
||
* @returns 异步完成后的 脚本或 CLI结果。
|
||
*/
|
||
export const runQqbotPluginCli = async (
|
||
argv: string[],
|
||
options: QqbotPluginCliOptions = {},
|
||
): Promise<QqbotPluginCliResult> => {
|
||
const command = argv[0] as QqbotPluginCliCommand | undefined;
|
||
const resolvedOptions: Required<QqbotPluginCliOptions> = {
|
||
cwd: options.cwd || process.cwd(),
|
||
stderr:
|
||
options.stderr || ((message) => process.stderr.write(`${message}\n`)),
|
||
stdout:
|
||
options.stdout || ((message) => process.stdout.write(`${message}\n`)),
|
||
};
|
||
|
||
switch (command) {
|
||
case 'create':
|
||
return createPlugin(
|
||
argv[1],
|
||
resolvedOptions,
|
||
getOptionValue(argv, '--out'),
|
||
);
|
||
case 'install-local':
|
||
return installLocalPlugin(argv[1], resolvedOptions);
|
||
case 'pack':
|
||
return packPlugin(
|
||
argv[1],
|
||
resolvedOptions,
|
||
getOptionValue(argv, '--out'),
|
||
);
|
||
case 'validate':
|
||
return validatePlugin(argv[1], resolvedOptions);
|
||
default:
|
||
throw new Error(
|
||
'Usage: qqbot-plugin <create|validate|pack|install-local> [args]',
|
||
);
|
||
}
|
||
};
|
||
|
||
if (require.main === module) {
|
||
runQqbotPluginCli(process.argv.slice(2)).catch((error) => {
|
||
console.error(error instanceof Error ? error.message : error);
|
||
process.exitCode = 1;
|
||
});
|
||
}
|