UNPKG

@lark-project/cli

Version:

飞书项目插件开发工具

389 lines (388 loc) 18.9 kB
"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); }