UNPKG

@lark-project/cli

Version:

飞书项目插件开发工具

188 lines (187 loc) 12.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.updateProject = void 0; const path_1 = __importDefault(require("path")); const fs_extra_1 = require("fs-extra"); const get_plugin_info_1 = require("../../../api/get-plugin-info"); const local_plugin_config_1 = require("../../../local-plugin-config"); const with_loading_spinner_1 = require("../../../utils/with-loading-spinner"); const download_codebase_1 = require("../init/download-codebase"); const logger_1 = require("../../../utils/logger"); const utils_1 = require("../../../v1/utils"); const file_1 = require("../utils/file"); const crypto_1 = require("crypto"); const apply_local_point_config_1 = require("../../../api/tools/apply-local-point-config"); const write_local_point_config_1 = require("../../../utils/write-local-point-config"); const validate_runtime_urls_1 = require("../../../utils/validate-runtime-urls"); const local_config_1 = require("../local-config"); const fill_webhook_tokens_1 = require("../../../utils/transform/fill-webhook-tokens"); const deletion_gate_1 = require("../deletion-gate"); const online_write_app_type_allowlist_1 = require("../../../utils/online-write-app-type-allowlist"); const types_1 = require("../../../types"); const downloadTemplate = async (templateTargetDir) => { await (0, with_loading_spinner_1.withLoadingSpinner)(download_codebase_1.downloadCodeBase, 'Code template downloading...', templateTargetDir, 'react-initialized'); }; const updateProject = async (payload) => { // Keep the service boundary guarded as well as the commander entry point. // updateProject is imported directly by tests and may be reused by future // callers that do not pass through the CLI command action. const initialPluginConfig = (0, local_plugin_config_1.getLocalPluginConfig)(); if (initialPluginConfig) { (0, online_write_app_type_allowlist_1.assertAppTypeAllowedForOnlineWrite)(types_1.ECommandName.update, initialPluginConfig.app_type); } // Track local-sync success so the "next: publish" hint emits only after the // whole command (including fall-through template pull) finishes — emitting // mid-flow risks a misleading next-step if downstream steps then fail. let syncedLocal = false; const emitPublishHintIfSynced = () => { if (syncedLocal) { process.stderr.write('next: run `lpm publish` to ship a new version with this configuration.\n'); } }; if ((payload === null || payload === void 0 ? void 0 : payload.source_type) === 'local') { logger_1.logger.info('Syncing local configuration to remote...'); try { const localPluginConfig = (0, local_plugin_config_1.getLocalPluginConfig)(); if (!localPluginConfig) { logger_1.logger.error('Please init project first'); return; } const localFeatures = await (0, write_local_point_config_1.getLocalPointConfig)(); // Runtime URL check before pushing to remote. Fabricated-looking // placeholders are NOT blocked — we only warn (the user swaps in the real // URL later / in the developer console). Structurally invalid URLs // (missing / empty / not http(s)) still hard-block. const { invalid, placeholders } = (0, validate_runtime_urls_1.splitViolations)((0, validate_runtime_urls_1.validateRuntimeUrls)(localFeatures).violations); if (placeholders.length > 0) { process.stderr.write((0, validate_runtime_urls_1.formatPlaceholderNotice)(placeholders) + '\n'); } if (invalid.length > 0) { process.stderr.write((0, validate_runtime_urls_1.formatViolations)(invalid) + '\n'); process.exit(1); } // Pre-push deletion gate (fail-closed, the irreversible action). If the local // config drops whole points OR deletes properties inside a point still present on // remote, pushing would delete them — block (exit 2) unless a one-time `lpm // allow-delete <nonce>` grant or a project/global standing allow is present. This // is the load-bearing gate: it does not trust that `local-config diff` was run // first. If the remote diff can't be computed (network), it stays advisory — the // push itself hits the same backend and will surface the error rather than // silently deleting. let diffs; try { diffs = await (0, local_config_1.computeLocalVsRemoteDiff)(); } catch (diffErr) { // Diff fetch is best-effort; a network failure here must not block the push // (the push hits the same backend and will surface the error itself). logger_1.logger.debug('Pre-push deletion check failed (non-fatal):', diffErr); } // exit outside the fetch try/catch so the block is never swallowed as a "fetch failure". if (diffs) { // pluginId 进 nonce → 授权按工程隔离(与 set / diff 用同一真值,nonce 三处对齐)。 // grant token:用户在另一个文件系统(如沙箱外的主机终端)授权后回传的凭证,命令行 --delete-grant // 优先,其次环境变量 LPM_DELETE_GRANT——让授权信息跨沙箱靠 copy-paste 而非本地 grant 文件。 const grantToken = (payload === null || payload === void 0 ? void 0 : payload.delete_grant) || process.env.LPM_DELETE_GRANT; const gate = (0, deletion_gate_1.tryPassDeletionGate)(diffs, localPluginConfig.pluginId, process.cwd(), { grantToken }); if (!gate.pass) { (0, deletion_gate_1.writeDeletionBlockNotice)(diffs, gate.nonce, localPluginConfig.pluginId); // notice 自带顶部横幅 + token process.exit(2); } if (gate.hasDeletion) process.stderr.write((0, deletion_gate_1.deletionBanner)(diffs) + '\n'); // 放行也打横幅(感知) } // Webhook token 两端 preserve(push 前):token 是后端代码从 point.config.local.json 读取做验签的 // 密钥。本地缺 token 时复用远端已有那份(护线上存量、绝不 randomUUID churn 掉合法 token),两端皆空 // 才生成;解析出的 token 回写本地,后端才拿得到。best-effort:远端不可达就跳过(push 的正向转换仍会 // 兜底生成,只是这次不回写)。 try { let remoteCfg = {}; // remoteOk 区分「远端确实为空」与「远端不可达」——两者都让 remoteCfg 落到 {}。只有 fetch 成功才允许 // 生成:不可达时 generate 不下传,空 token 原样不动、不回写(避免拿随机数 churn 掉远端真实存量 // token 并「黏住」永不再对账)。下次远端可达时再对账补齐。 let remoteOk = false; try { remoteCfg = await (0, local_config_1.fetchRemoteConfig)(); remoteOk = true; } catch (e) { logger_1.logger.debug('Fetch remote for token preserve failed (non-fatal):', e); } const { filledFromRemote, generated } = (0, fill_webhook_tokens_1.fillWebhookTokens)(localFeatures, remoteCfg, remoteOk ? crypto_1.randomUUID : undefined); if (filledFromRemote.length || generated.length) { await (0, write_local_point_config_1.writeLocalPointConfig)(localFeatures, true, /* quiet */ true); // 自带下面的 token 提示,抑制误导的「Pulling remote…」 process.stderr.write(`ℹ️ 已将 ${filledFromRemote.length + generated.length} 个点位的 webhook token 补写进本地配置` + '(point.config.local.json)——仅补 token,未改动你的其它配置。建议 `git diff point.config.local.json` ' + '复核一眼,确认你自己配置的数据没有被意外改动。\n'); } } catch (e) { logger_1.logger.debug('Token preserve/writeback failed (non-fatal):', e); } await (0, apply_local_point_config_1.applyLocalPointConfig)({ pluginId: localPluginConfig.pluginId, siteDomain: localPluginConfig.siteDomain, pointInfoMap: localFeatures, saveSnapshot: payload.save_snapshot, caseId: payload.case_id, }); logger_1.logger.success('Local configuration synced successfully.'); syncedLocal = true; // fall through to remote pull logic: pushes + pulls templates in one command // (the "next: publish" hint is emitted at the end of this function, after // fall-through completes — so a downstream failure doesn't leave a stale hint.) } catch (error) { logger_1.logger.error('Failed to sync local configuration:', error.message); process.exit(1); } } // config-only workspace (no ./src — no frontend toolchain): nothing to scaffold // or pull. Skip the template download + resource-code generation fall-through so // we don't write .template/ or src/features/ into a config-only handle. Detection // is automatic (a normal v2 project always has src/), so the caller never needs a // flag — keeps the skill's push step a single command. // cwd is the project root here (the dispatcher's plugin-project guard enforces it), // so this cwd-based check is not subdir-safe by design and doesn't need to be. if (!(0, fs_extra_1.existsSync)(path_1.default.join(process.cwd(), 'src'))) { emitPublishHintIfSynced(); return; } const templateTargetDir = path_1.default.join(process.cwd(), '.template'); await downloadTemplate(templateTargetDir); logger_1.logger.info('Start fetching project latest data...'); const localConfig = await (0, local_plugin_config_1.getLocalPluginConfig)(); if (!localConfig) { // No publish hint here even if the local sync succeeded above: reaching this // means the plugin config vanished mid-command — an anomalous state where a // "next: publish" hint would mislead more than help. logger_1.logger.error('Please init project first'); return; } const remoteConfig = await (0, get_plugin_info_1.getPluginInfo)(localConfig.pluginId, localConfig.pluginSecret, localConfig.siteDomain); const { list, newResourceRelation, syncResourceIds, unSyncResourceIds, needRemoveResourceIds } = await (0, file_1.generateResourceCode)({ remoteConfig, localConfig, targetDir: process.cwd(), siteDomain: localConfig.siteDomain, }); if (!list.length) { logger_1.logger.info('No new features'); emitPublishHintIfSynced(); return; } await (0, with_loading_spinner_1.withLoadingSpinner)(async () => { await Promise.all(list); (0, local_plugin_config_1.setLocalPluginConfig)(Object.assign(Object.assign({}, localConfig), { pluginSecret: (0, utils_1.encrypt)(localConfig.pluginSecret), resources: localConfig.resources.concat(newResourceRelation) })); }, 'Pulling code templates for the new features is in progress...'); await (0, fs_extra_1.remove)(templateTargetDir); (syncResourceIds === null || syncResourceIds === void 0 ? void 0 : syncResourceIds.length) && logger_1.logger.info(`Some new features were synchronized successfully and sample templates were generated:\n${syncResourceIds.join('\n')}`); (unSyncResourceIds === null || unSyncResourceIds === void 0 ? void 0 : unSyncResourceIds.length) && logger_1.logger.error(`Some features need to manually specify the entry file, which needs to be handled in plugin.config.json:\n${unSyncResourceIds.join('\n')}`); (needRemoveResourceIds === null || needRemoveResourceIds === void 0 ? void 0 : needRemoveResourceIds.length) && logger_1.logger.warn(`Some features were removed and need to be manually removed from plugin.config.json:\n${needRemoveResourceIds.join('\n')}`); emitPublishHintIfSynced(); }; exports.updateProject = updateProject;