@lark-project/cli
Version:
飞书项目插件开发工具
389 lines (388 loc) • 18.9 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.runDispatcher = void 0;
const commander_1 = require("commander");
const types_1 = require("../types");
const logger_1 = require("../utils/logger");
const is_plugin_project_1 = require("../utils/is-plugin-project");
const resolve_workspace_context_1 = require("../utils/resolve-workspace-context");
const get_plugin_info_1 = require("../api/get-plugin-info");
const types_2 = require("../v1/types");
const model_1 = require("../v1/model");
const project_1 = __importDefault(require("../v1/model/project"));
const start_1 = require("../v2/services/start");
const release_1 = require("../v2/services/release");
const build_1 = require("../v2/services/build");
const init_1 = require("../v2/services/init");
const resolve_backend_app_type_1 = require("../v2/services/utils/resolve-backend-app-type");
const create_1 = require("../v2/services/create");
const publish_1 = require("../v2/services/publish");
const config_1 = require("../v2/services/config");
const update_1 = require("../v2/services/update");
const schema_1 = require("../v2/services/schema");
const local_config_1 = require("../v2/services/local-config");
const workspace_1 = require("../v2/utils/workspace");
const check_npm_origin_1 = require("../utils/check-npm-origin");
const check_latest_version_1 = require("../utils/check-latest-version");
const require_auth_1 = require("../utils/require-auth");
const local_plugin_config_1 = require("../local-plugin-config");
const auth_config_1 = require("../v2/utils/auth-config");
const login_1 = require("../v2/services/login");
const whoami_1 = require("../v2/services/whoami");
const list_categories_1 = require("../v2/services/list-categories");
const update_description_1 = require("../v2/services/update-description");
const perm_1 = require("../v2/services/perm");
const check_diff_1 = require("../v2/services/check-diff");
const ai_1 = require("./handlers/ai");
const NoVersionLimitCommands = [types_1.ECommandName.check, types_1.ECommandName.login, types_1.ECommandName.init, types_1.ECommandName.schema, types_1.ECommandName.workspace, types_1.ECommandName.ai, types_1.ECommandName.whoami];
// Commands that require a valid user token. Preflight runs before the command
// so users get auth guidance immediately, not after parameter validation or
// deep API calls. For other auth-consuming commands (update/release), the
// fallback guidance is printed from getToolAuthHeaders when called.
const AuthRequiredCommands = [
types_1.ECommandName.create,
types_1.ECommandName.listCategories,
types_1.ECommandName.updateDescription,
types_1.ECommandName.publish,
types_1.ECommandName.schema,
types_1.ECommandName.localConfig,
types_1.ECommandName.perm,
];
function preflightAuth(command, payload) {
var _a;
if (!AuthRequiredCommands.includes(command))
return;
// create receives siteDomain via payload (no plugin project yet);
// other commands read it from plugin.config.json in cwd.
const siteDomain = command === types_1.ECommandName.create
? payload === null || payload === void 0 ? void 0 : payload.siteDomain
: (_a = (0, local_plugin_config_1.getLocalPluginConfig)()) === null || _a === void 0 ? void 0 : _a.siteDomain;
if (!siteDomain) {
process.stderr.write(command === types_1.ECommandName.create
? 'create requires --site-domain\n'
: 'Cannot resolve siteDomain from plugin.config.json\n');
process.exit(1);
}
(0, require_auth_1.requireAuth)(siteDomain);
}
/**
* Route a parsed (command, payload) pair to the right service/legacy model.
*
* Extracted from the former IIFE so it can be unit-tested by mocking the
* individual service modules and asserting which one is called. Runtime behavior
* for real CLI invocations is unchanged — see the `require.main === module`
* block at the bottom of this file.
*
* @param rawOptions forwarded to `Project.init` for the v1 init branch (which
* reads `{ command, payload }` off the object). Pass `undefined` in tests
* unless exercising v1 init explicitly.
*/
async function runDispatcher(command, payload, rawOptions) {
if (!NoVersionLimitCommands.includes(command)) {
await (0, check_latest_version_1.checkLatestVersion)();
}
// 初始化命令仅能根据 payload 查询插件信息后识别 v1 v2
if (command === types_1.ECommandName.init) {
const { projectName, pluginId, pluginSecret, siteDomain, templateId, configOnly } = payload;
// 所有 init(含 --config-only)都要求显式 <plugin-secret>——CLI 不再代查 secret,
// 开发者必须从开发者后台拿到 secret 后显式传入才能写入配置。
if (!pluginSecret) {
logger_1.logger.error('`lpm init <project> <plugin-id> <plugin-secret>` requires the plugin secret. ' +
'This applies to `--config-only` too: pass the secret explicitly, e.g. ' +
'`lpm init meegle-plugin-config <plugin-id> <plugin-secret> --site-domain <url> --config-only` ' +
'(obtain the secret from the developer site).');
process.exit(1);
}
// config-only:只为后端独立仓写一份最小 plugin.config.json。secret 必须由开发者显式提供
// (见上方校验)。仅支持 v2,且不向 v1 兜底(v1 / 私有化插件不适用),失败一律 fail-fast 给可操作提示。
if (configOnly) {
let isV2 = false;
try {
const remotePluginInfo = await (0, get_plugin_info_1.getPluginInfo)(pluginId, pluginSecret, siteDomain);
isV2 = remotePluginInfo.frameworkVersion === types_1.EPluginFrameworkVersion.v2;
}
catch (e) {
logger_1.logger.debug(e);
}
if (!isV2) {
logger_1.logger.error('`lpm init --config-only` only supports v2-framework plugins. Could not confirm this plugin is v2 ' +
'(it may be a v1 / private-deployment plugin, or the pluginId / --site-domain is wrong). ' +
'Verify the pluginId and --site-domain, or scaffold a normal project without --config-only.');
process.exit(1);
}
// app_type 以后端描述信息接口为准回填(要 user-token 鉴权;bootstrap 已前置 `lpm login`)。
// config-only 没有 requested 可兜底,拿不到就 fail-fast(onUnreachable: 'throw')。
let appType;
try {
const resolved = await (0, resolve_backend_app_type_1.resolveBackendAppType)({
siteDomain,
appKey: pluginId,
onUnreachable: 'throw',
});
if (!resolved) {
throw new Error('GetAppDescriptionInfo returned no resolvable app_type');
}
appType = resolved;
}
catch (e) {
logger_1.logger.debug(e);
logger_1.logger.error('Failed to fetch the plugin description info (needed to resolve app_type). ' +
`Make sure you are logged in for this site: run \`lpm login --site-domain ${siteDomain}\` and retry.`);
process.exit(1);
}
(0, init_1.initConfigOnlyProject)({ pluginId, pluginSecret, siteDomain, appType });
return;
}
try {
const remotePluginInfo = await (0, get_plugin_info_1.getPluginInfo)(pluginId, pluginSecret, siteDomain);
if (remotePluginInfo.frameworkVersion === types_1.EPluginFrameworkVersion.v2) {
logger_1.logger.info('Current plugin will use v2 framework.');
await (0, init_1.initProject)({
projectName,
pluginId,
pluginSecret,
siteDomain,
templateId,
});
return;
}
}
catch (e) {
// 私有化环境未部署插件框架版本查询接口会异常,我们兜成是 v1 插件
logger_1.logger.debug(e);
logger_1.logger.warn("Can't get the plugin' framework version, will be treated as version 1.0.");
}
logger_1.logger.info('Current plugin will use v1 framework.');
await project_1.default.init({
name: projectName,
pluginId,
pluginSecret,
options: rawOptions,
});
return;
}
if (command === types_1.ECommandName.create) {
preflightAuth(command, payload);
await (0, create_1.createProject)(payload);
return;
}
// Login: global auth via Device Authorization Grant or direct token
if (command === types_1.ECommandName.login) {
if (payload.token) {
await (0, login_1.loginWithToken)(payload.siteDomain, payload.token);
}
else {
await (0, login_1.login)(payload.siteDomain);
}
return;
}
// whoami: report logged-in sites; works outside plugin projects, like login.
if (command === types_1.ECommandName.whoami) {
await (0, whoami_1.whoamiService)();
return;
}
// Probe: "check auth --site-domain <url>" — skill-layer pre-check for
// orchestrators (e.g. plugin-workflow) to verify auth before running a
// chain of commands. Works outside plugin projects. exit 0 = token present,
// exit 1 with stderr guidance = not logged in.
if (command === types_1.ECommandName.check && payload.action === 'auth') {
if (!payload.siteDomain) {
process.stderr.write('check auth requires --site-domain\n');
process.exit(1);
}
let origin;
try {
origin = new URL(payload.siteDomain).origin;
}
catch (_a) {
process.stderr.write(`Invalid siteDomain: ${payload.siteDomain}\n`);
process.exit(1);
}
const profile = (0, auth_config_1.loadEffectiveProfile)(origin);
if (profile && (0, auth_config_1.resolveToken)(profile)) {
process.exit(0);
}
process.stderr.write((0, require_auth_1.buildAuthGuidance)(origin) + '\n');
process.exit(1);
}
// Probe: "check context" — skill-layer routing. Resolves cwd into a single
// token (PLUGIN_PROJECT / BACKEND_HANDLE_CWD / NONE) so the
// skill pattern-matches one token instead of running nested shell tests. Must
// run BEFORE the plugin-project guard below — its whole job is to report whether
// cwd is a plugin project / a config-only backend handle / neither.
if (command === types_1.ECommandName.check && payload.action === 'context') {
process.stdout.write((0, resolve_workspace_context_1.resolveWorkspaceContext)(process.cwd()) + '\n');
process.exit(0);
}
// check diff: 发布前全量 diff。需 plugin.config.json(getLocalPluginConfig 自检),
// 但不依赖 v1/v2 框架机制,故与 auth/context 一样在工程框架门之前早处理。
if (command === types_1.ECommandName.check && payload.action === 'diff') {
await (0, check_diff_1.checkDiffService)();
process.exit(0);
}
// ai: 独立于 v1/v2 框架,也不要求必须在插件工程内(peek 可作用于任意文件)
if (command === types_1.ECommandName.ai) {
await (0, ai_1.handleAi)(payload);
return;
}
// 以下为插件工程下的操作命令
const [isPlugin, pluginFrameworkVersion] = (0, is_plugin_project_1.isPluginProject)();
if (!isPlugin) {
logger_1.logger.error('Current working directory is not a plugin project (no plugin.config.json here). ' +
'If your plugin project is in another directory, cd into it and retry.');
process.exit(1);
}
// schema / perm / local-config get 的 stdout 供脚本/AI 直接消费(schema、local-config get
// 是单行路径;perm 是 JSON)。其余 logger 日志走 stderr/logger 通道,不会污染 stdout。
// 为避免 "Current project is v2 framework." 这种 info 行混进 stdout,这里仍然静音。
const stdoutIsMachineReadable = command === types_1.ECommandName.schema ||
command === types_1.ECommandName.perm ||
(command === types_1.ECommandName.localConfig && (payload === null || payload === void 0 ? void 0 : payload.action) === 'get');
preflightAuth(command, payload);
try {
if (pluginFrameworkVersion === types_1.EPluginFrameworkVersion.v2) {
if (!stdoutIsMachineReadable) {
logger_1.logger.info('Current project is v2 framework.');
}
switch (command) {
case types_1.ECommandName.listCategories: {
await (0, list_categories_1.listCategoriesService)(payload);
return;
}
case types_1.ECommandName.perm: {
await (0, perm_1.permService)(payload);
return;
}
case types_1.ECommandName.updateDescription: {
await (0, update_description_1.updateDescriptionService)(payload);
return;
}
case types_1.ECommandName.start: {
await (0, start_1.startProject)(payload);
return;
}
case types_1.ECommandName.build: {
await (0, build_1.buildProject)(payload);
return;
}
case types_1.ECommandName.release: {
await (0, release_1.releaseProject)(payload);
return;
}
case types_1.ECommandName.publish: {
await (0, publish_1.publishProject)(payload);
return;
}
case types_1.ECommandName.config: {
if (payload.action === 'set') {
await (0, config_1.setConfig)(payload.key, payload.value);
return;
}
if (payload.action === 'get') {
await (0, config_1.getConfig)(payload.key);
return;
}
logger_1.logger.error('Only "get" and "set" actions are supported.');
return;
}
case types_1.ECommandName.update: {
(0, update_1.updateProject)(payload);
return;
}
case types_1.ECommandName.schema: {
await (0, schema_1.generateSchema)();
return;
}
case types_1.ECommandName.localConfig: {
const { action, fromPath, allow_delete, remote, compare_snapshot, case_id } = payload;
if (action === 'set') {
await (0, local_config_1.updateLocalConfig)({ fromPath: fromPath || '', allowDelete: Boolean(allow_delete) });
}
else if (action === 'get') {
await (0, local_config_1.getLocalConfig)({
remote: Boolean(remote),
compareSnapshot: Boolean(compare_snapshot),
caseId: case_id,
});
}
else if (action === 'diff') {
await (0, local_config_1.diffLocalConfig)();
}
return;
}
case types_1.ECommandName.workspace: {
const { action, scope, include_state } = payload;
if (action === 'clean') {
(0, workspace_1.cleanWorkspace)({ scope, keepState: !include_state });
}
return;
}
case types_1.ECommandName.check: {
if (payload.action === 'npm') {
await (0, check_npm_origin_1.checkNpmOrigin)();
}
return;
}
}
}
}
catch (e) {
logger_1.logger.error(e);
// default v1 project
logger_1.logger.warn("Can't execute the command as version 2.0, will be treated as version 1.0.");
}
logger_1.logger.info('Current project is v1 framework.');
const mainArgs = payload.mainArgs;
switch (command) {
case types_1.ECommandName.start: {
project_1.default.run(types_2.ScriptCommand.start, process.argv);
return;
}
case types_1.ECommandName.build: {
const argv = payload.zip ? ['--zip'] : [];
project_1.default.run(types_2.ScriptCommand.build, argv);
return;
}
case types_1.ECommandName.release: {
// 将主进程命令参数透传给子进程消费,比如 lpm release 指定 pluginSecret
project_1.default.run(types_2.ScriptCommand.release, mainArgs);
return;
}
case types_1.ECommandName.publish: {
logger_1.logger.info('The command is only support in v2 framework.');
return;
}
case types_1.ECommandName.config: {
const ins = model_1.ConfigFactory.getConfig(types_2.EConfigType.Plugin);
await ins.run(payload.action, payload.key, payload.value);
return;
}
case types_1.ECommandName.update: {
logger_1.logger.info('The command is only support in v2 framework.');
return;
}
case types_1.ECommandName.check: {
if (payload.action === 'npm') {
await (0, check_npm_origin_1.checkNpmOrigin)();
return;
}
}
}
logger_1.logger.error(`The command ${command} is not supported.`);
process.exit(1);
}
exports.runDispatcher = runDispatcher;
// Auto-execute only when this file is the entry point (i.e. spawned by
// run-script as a child process). Test imports of `runDispatcher` do not
// trigger the CLI argv parsing path.
if (require.main === module) {
const options = (0, commander_1.createCommand)()
.option('--command <command>', 'Execute command name')
.option('--payload <payload>', 'Execute payload json')
.parse(process.argv)
.opts();
runDispatcher(options.command, JSON.parse(options.payload), options);
}