@lark-project/cli
Version:
飞书项目插件开发工具
341 lines (327 loc) • 18 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.addAiCommand = void 0;
// ⚠️ 改命令名 / flag / alias 时,同步 grep `lpm` skills/ .claude/skills/ 修文档引用
const path_1 = __importDefault(require("path"));
const types_1 = require("../../types");
const run_script_1 = __importDefault(require("../../utils/run-script"));
const logger_1 = require("../../utils/logger");
/**
* `lpm ai` — AI agent utilities used by meegle-plugin-* skills.
*
* 设计原则:
* - 所有"只给 AI 用"的命令收在此命名空间,与开发者命令(create/start/build/…)隔离
* - 开发者在 `lpm --help` 顶层只看到 `ai` 一个入口,不会被 peek/checkpoint 等噪音干扰
* - AI 通过 skill 文档学习 `lpm ai <sub>` 形式;弱 AI 只需记 `lpm ai peek <file> <name>` 一条公式
* - 跨 OS / 跨 agent 工作(node 实现,不依赖 jq/rg/sed)
*/
function addAiCommand(program) {
const ai = program
.command('ai')
.description('AI agent tools (used by meegle-plugin-* skills; developers rarely need these directly)');
// =================================================================
// lpm ai peek — 大文件切片
// =================================================================
const peek = ai
.command('peek')
.description('Slice a large file by symbol / path / section; falls back to line ranges');
peek
.argument('<file>', 'File to peek (JSON / YAML / .d.ts / .md / plain text)')
.argument('[name]', 'Symbol / path / section title to locate; CLI auto-detects type by file extension')
.option('--line <n>', 'Positional mode: start line (1-based)', parseInt)
.option('--limit <n>', 'Positional mode: number of lines (works only with --line or --match)', parseInt)
.option('--match <pattern>', 'Match mode: regex to search')
.option('--after <n>', 'Match mode: lines to show after match', parseInt)
.option('--before <n>', 'Match mode: lines to show before match', parseInt)
.option('--head <n>', 'Take first N lines', parseInt)
.option('--tail <n>', 'Take last N lines', parseInt)
.option('--index', 'Structural mode: list all top-level symbols / point types')
.option('--list', 'Structural mode: list all symbols (class/type/interface for .d.ts; headings for .md)')
.option('--summary', 'Structural mode: TOC for config-like JSON (buckets of keyed objects) — ~1k token global view')
.option('--no-follow', 'Do not follow $ref (JSON/YAML) or type references (.d.ts); default is follow')
.option('--max-depth <n>', 'Max recursion depth for --follow (default 5)', parseInt, 5)
.option('--descriptions-only', 'Structural mode: return only description fields, skip validation noise')
.option('--format <fmt>', 'Output format: text | json', 'text')
.action((file, name, opts) => {
// 互斥参数校验:结构模式不允许 --limit
if ((opts.limit || opts.line) && (name || opts.index || opts.list || opts.descriptionsOnly)) {
// 允许 --limit 仅与 --match 搭配;其他结构模式禁止 --limit
if (!opts.match) {
logger_1.logger.error('--limit/--line 不能与结构模式(<name> / --index / --list / --descriptions-only)同时使用');
logger_1.logger.error('结构模式自动取完整单元,不需要截断;位置切片请单独使用 --line + --limit');
process.exit(1);
}
}
(0, run_script_1.default)(path_1.default.join(__dirname, '../dispatcher'), [
'--command',
types_1.ECommandName.ai,
'--payload',
JSON.stringify({
action: 'peek',
file,
name,
options: opts,
}),
]);
});
// =================================================================
// lpm ai checkpoint — 工作流事件日志
//
// 采用 append-only 的 events.jsonl(每步一行 JSON),避免 state.json
// 并发写冲突;status/get 从 jsonl 重放得到当前状态。
// =================================================================
const checkpoint = ai
.command('checkpoint')
.description('Workflow event log (append-only .lpm-cache/events.jsonl)');
checkpoint
.command('append')
.description('Append a workflow event to .lpm-cache/events.jsonl')
.argument('<event>', 'JSON event payload, e.g. \'{"phase":1,"step":"1.1","status":"success"}\'')
.action((event) => {
// 验证是合法 JSON
try {
JSON.parse(event);
}
catch (e) {
logger_1.logger.error(`event payload must be valid JSON: ${e.message}`);
process.exit(1);
}
(0, run_script_1.default)(path_1.default.join(__dirname, '../dispatcher'), [
'--command',
types_1.ECommandName.ai,
'--payload',
JSON.stringify({ action: 'checkpoint.append', event }),
]);
});
checkpoint
.command('status')
.description('Show current workflow phase/step summary (replayed from events.jsonl)')
.option('--format <fmt>', 'Output format: text | json', 'text')
.action((opts) => {
(0, run_script_1.default)(path_1.default.join(__dirname, '../dispatcher'), [
'--command',
types_1.ECommandName.ai,
'--payload',
JSON.stringify({ action: 'checkpoint.status', options: opts }),
]);
});
checkpoint
.command('get')
.description('Get full checkpoint state as JSON (replayed from events.jsonl)')
.option('--phase <n>', 'Filter by phase', parseInt)
.option('--step <s>', 'Filter by step key')
.action((opts) => {
(0, run_script_1.default)(path_1.default.join(__dirname, '../dispatcher'), [
'--command',
types_1.ECommandName.ai,
'--payload',
JSON.stringify({ action: 'checkpoint.get', options: opts }),
]);
});
// =================================================================
// lpm ai state — 工作流断点状态(.lpm-cache/state.json)
//
// 与 checkpoint(events.jsonl) 互补:state 保存单一的"上下文 + 进度"快照
// (projectRoot / pluginId / nextStep 等),路径经 workspacePaths() 锚定到
// 插件工程根。配合 `lpm --cwd <dir>` 可在任意目录写对位置,取代裸 `cat >`。
// =================================================================
const state = ai
.command('state')
.description('Read/write the workflow checkpoint state (.lpm-cache/state.json), resolved to the plugin project root');
state
.command('set')
.description('Write the full checkpoint state JSON to .lpm-cache/state.json (anchor the project root with `lpm --cwd <dir>`)')
.argument('<json>', 'State JSON object — inline, @file, or @- to read stdin')
.action((json) => {
(0, run_script_1.default)(path_1.default.join(__dirname, '../dispatcher'), [
'--command',
types_1.ECommandName.ai,
'--payload',
JSON.stringify({ action: 'state.set', value: json }),
]);
});
state
.command('get')
.description('Print .lpm-cache/state.json to stdout (empty output if absent)')
.action(() => {
(0, run_script_1.default)(path_1.default.join(__dirname, '../dispatcher'), [
'--command',
types_1.ECommandName.ai,
'--payload',
JSON.stringify({ action: 'state.get' }),
]);
});
// =================================================================
// lpm ai init-draft — 从 remote 快照一步建 draft(cp + 路径校验合并)
//
// 笨 AI 最易忘记 `cp remote → draft`,这一步 CLI 兜底:
// - 自动拼 `.lpm-cache/config/draft-<ts>.json` 文件名
// - 拒绝把 draft 命名成非 draft-* 前缀(防止误覆盖 remote / point.config.local)
// - stdout 回显 draft 路径,方便后续 peek / patch-json 链式使用
// =================================================================
ai
.command('init-draft')
.description('Initialize a draft from .lpm-cache/config/remote.json (cp + path validation in one step)')
.option('--from <path>', 'Source file (default: .lpm-cache/config/remote.json)')
.option('--to <path>', 'Explicit draft path (must start with "draft-"); default auto-generates by timestamp')
.option('--format <fmt>', 'Output format: text | json', 'text')
.action((opts) => {
(0, run_script_1.default)(path_1.default.join(__dirname, '../dispatcher'), [
'--command',
types_1.ECommandName.ai,
'--payload',
JSON.stringify({ action: 'init-draft', options: opts }),
]);
});
// =================================================================
// lpm ai patch-json — 磁盘层结构化 patch,避免把整份 JSON 塞进 context
//
// 核心价值:配 `cp remote.json draft.json` 使用时,draft 保证全量、AI 只
// 通过 path 精准动几处——解决"整读 lint"和"全量提交约束"之间的张力。
// =================================================================
ai
.command('patch-json')
.description('Surgically patch a JSON file by path (compatible with peek syntax)')
.argument('<file>', 'JSON file to patch (will be modified in place)')
.option('--set-path <path>', 'Path to replace (pair with --set-value)')
.option('--set-value <json>', 'New JSON value (@file or @- also OK)')
.option('--add-path <path>', 'Array path to append into (pair with --add-value)')
.option('--add-value <json>', 'JSON value to append')
.option('--delete <path>', 'Path to delete')
.option('--merge-path <path>', 'Object path to merge fields into (pair with --merge-value)')
.option('--merge-value <json>', 'Partial object (JSON) to shallow-merge')
.option('--patches <file>', 'Batch mode: apply JSON Patch-like list [{op,path,value?}, ...]')
.option('--format <fmt>', 'Output format: text | json', 'text')
.addHelpText('after', `
重要提示(给 AI 用):
patch-json 只修改本地 draft-*.json 文件,\x1b[1m不会\x1b[0m把改动推到后台。
真正提交变更必须用: lpm local-config set <draft-path>
set 阶段会对比远端基线,发现减少点位时会要求二次确认。
不要把 patch-json 当作"已提交"——它只是让 draft 在磁盘上保持全量、
context 里只装 delta,减少整读压力。
提交流程:init-draft → patch-json (一次或多次) → local-config set
`)
.action((file, opts) => {
(0, run_script_1.default)(path_1.default.join(__dirname, '../dispatcher'), [
'--command',
types_1.ECommandName.ai,
'--payload',
JSON.stringify({
action: 'patch-json',
file,
options: opts,
}),
]);
});
// =================================================================
// lpm ai audit-jssdk — 校验 window.JSSDK.<ns>.<method>(…) 调用在 types 里存在
//
// 原先 skill 里的 A1.5 审计步骤用 bash + grep + awk 实现,非 Windows 环境/
// 非标准 shell 跑不起来;这里下沉成原生 Node 子命令,任意 agent 任意 OS 均可调。
// =================================================================
ai
.command('audit-jssdk')
.description('Audit window.JSSDK.<ns>.<method>( calls against a .d.ts to catch fabricated method names')
.option('--types <path>', 'Path to js-sdk .d.ts', 'node_modules/@lark-project/js-sdk/dist/types/index.d.ts')
.option('--files <glob>', 'Explicit file list (comma-separated). Default: `git diff` changed → fallback src/features/**/*.{ts,tsx}')
.option('--format <fmt>', 'Output format: text | json', 'text')
.action((opts) => {
(0, run_script_1.default)(path_1.default.join(__dirname, '../dispatcher'), [
'--command',
types_1.ECommandName.ai,
'--payload',
JSON.stringify({ action: 'audit-jssdk', options: opts }),
]);
});
// =================================================================
// lpm ai validate — 校验 point.config.local.json 是否符合 schema
//
// weak AI 反复在 prop_type 命名上凭空(multiSelect camelCase /
// workItemTypeWithField 不在 enum)+ 漏 outputs / 漏 multi_select 的
// options。让用户当 QA 是错位——schema 是权威,机械跑一次就能定位。
// =================================================================
ai
.command('validate')
.description('Validate point.config.local.json against the plugin schema')
.argument('[file]', 'Path to point.config.local.json (default: ./point.config.local.json)')
.option('--format <fmt>', 'Output format: text | json', 'text')
.action((file, opts) => {
(0, run_script_1.default)(path_1.default.join(__dirname, '../dispatcher'), [
'--command',
types_1.ECommandName.ai,
'--payload',
JSON.stringify({ action: 'validate', file, options: opts }),
]);
});
// =================================================================
// lpm ai docs — 检索官方插件开发文档(替代 feishu-project-knowledge MCP)
//
// 原先 skill 直接调宿主的 MCP tool search_meegle_plugin_docs,硬依赖宿主
// 配了那个 MCP server。这里下沉成 CLI 命令,用插件工程已 login 的 token 调
// ${siteDomain}/mcp_server/knowledge,跨 agent / 跨机器都能用,不依赖宿主 MCP。
// 结果写 .lpm-cache/mcp/<slug>.md,沿用既有缓存 + lpm ai peek 读法。
//
// hidden:功能尚未发版、skill 也未改(仍引导直调 MCP)。命令可跑但暂不在
// `lpm ai --help` 列出,避免把人引到本机 CLI 还没有的命令。待发版 + 改 skill
// 时去掉 hidden。
// =================================================================
ai
.command('docs', { hidden: true })
.description('Search official Feishu Project / Meego plugin dev docs (replaces the feishu-project-knowledge MCP; uses your lpm-login token)')
.argument('<query>', 'Search keywords / question, e.g. "如何订阅工作项字段变更"')
.option('--format <fmt>', 'Output format: text | json', 'text')
.action((query, opts) => {
(0, run_script_1.default)(path_1.default.join(__dirname, '../dispatcher'), [
'--command',
types_1.ECommandName.ai,
'--payload',
JSON.stringify({ action: 'docs', query, options: opts }),
]);
});
// =================================================================
// 顶层 help:手写 usage example 让 AI(尤其弱模型)能 pattern match
// =================================================================
ai.on('--help', () => {
console.log(`
Examples (mostly for AI skill authors / debugging):
# 查 SDK 类型定义(.d.ts 自动按 class/type/interface 名定位 + 追引用链)
lpm ai peek node_modules/@lark-project/js-sdk/dist/types/index.d.ts Navigation
# 查点位 schema 完整规则(JSON 自动按 key 定位 + 追 $ref)
lpm ai peek .lpm-cache/schema/point-schema.json LiteAppComponentPoint
# 列所有点位类型(--index 取轻量目录,用于选型)
lpm ai peek .lpm-cache/schema/point-schema.json --index
# 只看 description(过滤校验噪音)
lpm ai peek .lpm-cache/schema/point-schema.json LiteAppComponentPoint --descriptions-only
# 查 MCP 缓存文档某一章节
lpm ai peek .lpm-cache/mcp/lite-app-api.md "订阅属性"
# 位置切片(兜底,无结构文件)
lpm ai peek build.log --tail 50
lpm ai peek build.log --match ERROR --before 2 --after 10
# Checkpoint 事件日志(workflow 每步一行)
lpm ai checkpoint append '{"phase":1,"step":"1.1","status":"success"}'
lpm ai checkpoint status
lpm ai checkpoint get --phase 2
# 审计 JSSDK 调用(method 是不是都在 types 里;替代 bash+grep+awk)
lpm ai audit-jssdk
lpm ai audit-jssdk --format json
lpm ai audit-jssdk --files src/features/foo/index.tsx,src/features/bar/index.tsx
# 校验 point.config.local.json 是否符合 schema(prop_type enum / required / x-cross-unique)
lpm ai validate
lpm ai validate --format json
lpm ai validate path/to/point.config.local.json
# 配置文件 TOC(config-like JSON 的 summary 视图,给 AI 做全局决策用)
lpm ai peek .lpm-cache/config/remote.json --summary
# 从 remote 一步建 draft(cp + 自动命名 + 目标校验)
DRAFT=$(lpm ai init-draft)
# 磁盘层 JSON patch(patch 只认 draft-*.json;改动停在点位级别)
lpm ai patch-json $DRAFT --add-path 'data[point_type=button].point_config' --add-value @/tmp/new-point.json
lpm ai patch-json $DRAFT --set-path 'data[point_type=button].point_config[key=foo]' --set-value @/tmp/updated-point.json
lpm ai patch-json $DRAFT --merge-path 'data[point_type=board].point_config[key=foo]' --merge-value '{"name":"新名字"}'
lpm ai patch-json $DRAFT --delete 'data[point_type=button].point_config[key=old]'
`);
});
}
exports.addAiCommand = addAiCommand;