@lark-project/cli
Version:
飞书项目插件开发工具
188 lines (187 loc) • 12.7 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.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;