@pippit-dev/cli 1.0.21 → 1.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+
3
+ // Offline usage notes. The installed runtime's list/describe/schema remain the
4
+ // source of truth for available commands and their accepted input.
5
+ const GUIDES = [
6
+ {
7
+ name: "storyboard",
8
+ summary: "故事板原生脚本标签、时长与引用,以及当前 CLI 的能力边界。",
9
+ content: [
10
+ "故事板脚本保存在镜头资产 content.generation.prompt。现有数据可能是 biz/shot,或 biz/video 且 bizVariant 为 storyboard-shot;故事板 brief 另有镜头列表与顺序。普通 video 节点不等于一个完整故事板镜头。",
11
+ "<duration-ms> 中填写正数毫秒。一个 prompt 中所有有效正数标签累加;有标签总和时优先采用该总和,否则回退到正数 generation.durationMs。空值、非数值或非正数不计入总和。例子中的 2000 + 3000 = 5000 ms。",
12
+ "时间码和起止区间文字只是 prompt 内容,不是可执行的台词调度。标签求和也不保证模型精确按秒完成每个动作或台词。生成会将镜头总毫秒数向上取整为秒,再按当前故事板模型策略校验;可用时长、分辨率组合和最小时长取决于当前模型目录,不能从通用 generate-video 的帮助推断。",
13
+ "node-asset 填真实节点资产 ID,角色形象选择沿用 data-reference-selection 属性。独立媒体可以没有节点,使用 pippit-asset-id,但需已有可解析的引用草稿;只有裸 ID 不代表素材已就绪。先查询真实资产与当前引用,不要从名称猜 ID。",
14
+ "当前没有公开的故事板脚本编辑、镜头增删排序或指定镜头生成领域 command。Canvas 的 xyq.generation.update_prompt 不等同于故事板的脚本保存流程;generate-video 不会自动设置故事板身份或写回指定镜头。通用资产补丁能传输数据,不提供这些业务校验,不能作为故事板领域操作的替代。此处 XML 用于说明既有格式,不是可直接执行的故事板命令。",
15
+ "下一步先用 canvas command list 确认安装版本能力,用 canvas get 读取已知镜头资产;故事板编辑与生成继续使用现有 Web 故事板入口。所有示例 ID 都须替换为当前项目真实 ID。",
16
+ ].join("\n\n"),
17
+ next_commands: [
18
+ "pippit-tool-cli canvas command list",
19
+ "pippit-tool-cli canvas get --asset-id SHOT_ASSET_ID",
20
+ "pippit-tool-cli canvas command guide prompt-references",
21
+ "pippit-tool-cli canvas command guide time",
22
+ ],
23
+ examples: [
24
+ {
25
+ description: "现有故事板 prompt:两个正数毫秒段,总时长 5000 ms;不是 CLI 编辑指令。",
26
+ prompt:
27
+ "<duration-ms>2000</duration-ms><node-asset>ROLE_NODE_ASSET_ID</node-asset>走入雨中的街道。\n<duration-ms>3000</duration-ms><node-asset>ROLE_NODE_ASSET_ID</node-asset>停下脚步,说:我们到了。",
28
+ },
29
+ ],
30
+ related: ["prompt-references", "time"],
31
+ },
32
+ {
33
+ name: "prompt-references",
34
+ summary: "图片/视频 prompt 标签、角色选材与没有节点的独立素材。",
35
+ content: [
36
+ "先用 list 确认 xyq.generation.update_prompt 可用,再 describe 获取参数。该命令接收 nodeId、prompt、可选 dryRun;nodeId 是当前 Canvas 的目标图片或视频节点 ID。get_snapshot 可查节点,get_asset 可读节点资产与 generation.references;用 describe 查看这两个查询命令的输入,避免混淆 Canvas ID、节点 ID 与媒体 Pippit ID。",
37
+ '<node-asset>NODE_ASSET_ID</node-asset> 引用已有节点。标签 ID 来自查询结果中的真实节点资产身份;不要用展示名称代替。CLI 复用 Web 标签匹配、节点连边和角色默认选材,保留标签属性。角色已有 data-reference-selection="IMAGE_ID,VOICE_ID" 属性使用逗号分隔选择 ID,需与角色已有资料匹配,不另传 selectedIds 或 reference 数组。',
38
+ "独立上传/素材库媒体可以没有节点。对已在目标 generation.references 中的素材,用 <pippit-asset-id>MEDIA_PIPPIT_ASSET_ID</pippit-asset-id>,ID 取自该草稿的 pippitAssetId。视频目标可使用已有图片、视频、音频候选;图片目标须符合自身支持的引用类型。无须新增 referenceSource 入参。",
39
+ "只有 canvas upload 返回的 ID 还不够:本命令不会查询未知媒体并加入草稿。canvas get 只查询,不添加引用。找不到的普通标签返回 UNRESOLVED_PROMPT_REFERENCE,不写文档。相同媒体若匹配到已有 Canvas 源节点,沿用 Web 节点优先规则连边。新独立素材先通过现有 Web 上传/素材库流程建立草稿。",
40
+ "dryRun:true 在隔离文档预演,不改变原文档。成功编辑保留其他 generation 参数及 caption/title/name;空 prompt 清空文本并保留现有引用与连边。本命令不触发生成。不要浅覆盖整个 generation。故事板有自己的脚本与引用流程,见 storyboard 指南。",
41
+ ].join("\n\n"),
42
+ next_commands: [
43
+ "pippit-tool-cli canvas command describe xyq.generation.update_prompt",
44
+ "pippit-tool-cli canvas command describe get_snapshot",
45
+ "pippit-tool-cli canvas command describe get_asset",
46
+ "pippit-tool-cli canvas command describe xyq.generation.update_prompt --path properties.prompt",
47
+ ],
48
+ examples: [
49
+ {
50
+ description: "已有 Canvas 节点引用的预演;将两个节点 ID 换成查询到的真实值。",
51
+ command:
52
+ "pippit-tool-cli canvas command run xyq.generation.update_prompt --canvas-id CANVAS_ID --file ./prompt-input.json",
53
+ input: {
54
+ nodeId: "TARGET_IMAGE_NODE_ID",
55
+ prompt: "参考 <node-asset>REFERENCE_NODE_ASSET_ID</node-asset> 的人物,改为雨夜街景。",
56
+ dryRun: true,
57
+ },
58
+ },
59
+ {
60
+ description: "没有节点、但已存在于目标草稿中的独立图片引用。",
61
+ input: {
62
+ nodeId: "TARGET_IMAGE_NODE_ID",
63
+ prompt: "参考 <pippit-asset-id>PIPPIT_ASSET_ID_IN_DRAFT</pippit-asset-id> 的构图。",
64
+ dryRun: true,
65
+ },
66
+ },
67
+ ],
68
+ related: ["storyboard", "time"],
69
+ },
70
+ {
71
+ name: "time",
72
+ summary: "区分故事板毫秒、多轨微秒、3D 帧与源动画秒。",
73
+ content: [
74
+ "各领域单位不同,先 describe 具体字段;不要把一个领域的数值直接复制到另一个领域。此指南只解释单位,不触发写入或生成。",
75
+ "故事板 <duration-ms> 与 generation.durationMs 使用毫秒:1 秒 = 1000 ms。多个正数标签累加;没有有效标签才回退到 generation.durationMs。提交视频时对总毫秒数向上取整为秒,随后遵循当前故事板模型策略。时间码文字不是执行调度,详见 storyboard。",
76
+ "多轨 TimelineDraft 的时间字段使用整数微秒:1 秒 = 1000000 µs。输出 width/height 使用像素。草稿 duration 由片段推导;frameRate 当前只读,没有设置帧率 command。expectedRevision 是草稿版本号,不是时间。",
77
+ "3D 关键帧与动作片段 startFrame/durationFrames 使用帧,timeline.fps 定义帧时长;move_keyframes.deltaFrames 是整数帧偏移。动作片段 trimStart/trimEnd 使用源动画秒数,可带小数,不是帧。",
78
+ "通用 generate-video --duration 使用整数秒,由该视频入口处理;它不会在本地把故事板 duration-ms 标签自动转换成该参数。其模型/时长能力不能替代故事板当前模型约束。",
79
+ "ID 从所属查询取得:Timeline 的轨道/片段/来源 ID 由 xyq.timeline.query 返回;3D 对象/动画 ID 由 xyq.scene3d.query 返回。外层 nodeId 始终从当前 Canvas 节点查询取得。",
80
+ ].join("\n\n"),
81
+ next_commands: [
82
+ "pippit-tool-cli canvas command guide storyboard",
83
+ "pippit-tool-cli canvas command guide timeline",
84
+ "pippit-tool-cli canvas command guide scene3d",
85
+ "pippit-tool-cli canvas command describe xyq.scene3d.apply --operation move_keyframes",
86
+ ],
87
+ examples: [
88
+ {
89
+ description: "仅作换算:在 30 fps 的 3D 时间轴上,持续 2 秒对应 60 帧;不是字段更新请求。",
90
+ conversion: { seconds: 2, milliseconds: 2000, microseconds: 2000000, fps: 30, frames: 60 },
91
+ },
92
+ ],
93
+ related: ["storyboard", "timeline", "scene3d"],
94
+ },
95
+ {
96
+ name: "timeline",
97
+ summary: "查询多轨节点、草稿版本和素材来源,再预演现有编辑命令。",
98
+ content: [
99
+ "多轨的外层 Canvas 节点类型是 biz/timeline-composition,创建工厂 nodeKind 是 timeline-composition。编辑内容位于节点引用的独立 TimelineDraft;create_biz_node 的 initialData 被该工厂忽略,工厂创建空草稿。先 list 确认命令可用,再按 node-kind 查看工厂参数。",
100
+ "已有节点先运行 xyq.timeline.query。nodeId 来自 Canvas 节点查询,expectedRevision 必须使用此次查询的 draft.revision,轨道/片段/来源 ID 使用草稿中的真实 ID。版本不匹配会中止写入;重新查询后基于新状态准备操作。",
101
+ "xyq.timeline.apply.commands 接受现有 Timeline SDK 命令的 {type,payload} 数组。用 describe --operation 按 type 查看单项参数,schema 显式导出完整集合。dryRun:true 执行同一业务校验与原子预演,不保存 Canvas 状态。",
102
+ "时间字段使用整数微秒,1 秒 = 1000000 µs;输出尺寸使用像素。总时长由片段推导,frameRate 当前只读。编辑器会按既有规则闭合主视频轨空隙并删除空副轨,不应假定编辑后草稿只改一处字段。",
103
+ "新增素材需复用已有来源,或关联真实 Canvas 素材节点;不能发明 source ID 或任意 URL 来替代素材解析。渲染、导出、上传与生成需要各自现有运行环境,不包含在草稿编辑命令内。",
104
+ ].join("\n\n"),
105
+ next_commands: [
106
+ "pippit-tool-cli canvas command describe create_biz_node --node-kind timeline-composition",
107
+ 'pippit-tool-cli canvas command run xyq.timeline.query --canvas-id CANVAS_ID --input \'{"nodeId":"TIMELINE_NODE_ID"}\'',
108
+ "pippit-tool-cli canvas command describe xyq.timeline.apply --operation set_output_size",
109
+ "pippit-tool-cli canvas command schema xyq.timeline.apply",
110
+ ],
111
+ examples: [
112
+ {
113
+ description: "将查询得到的 draft.revision 替换示例 0,并填写真实 nodeId;先预演输出尺寸。",
114
+ command:
115
+ "pippit-tool-cli canvas command run xyq.timeline.apply --canvas-id CANVAS_ID --file ./timeline-input.json",
116
+ input: {
117
+ nodeId: "TIMELINE_NODE_ID",
118
+ expectedRevision: 0,
119
+ commands: [{ type: "set_output_size", payload: { width: 1920, height: 1080 } }],
120
+ dryRun: true,
121
+ },
122
+ },
123
+ ],
124
+ related: ["time", "scene3d"],
125
+ },
126
+ {
127
+ name: "scene3d",
128
+ summary: "3D 导演文档、对象 ID、空间与动画单位及编辑边界。",
129
+ content: [
130
+ "3D 导演台由外层 Canvas 节点和独立导演文档组成。create_biz_node 使用 nodeKind: scene3d;先 list 确认当前安装版本支持的命令,再 describe --node-kind 查看工厂参数。",
131
+ "xyq.scene3d.query 的 nodeId 是外层 Canvas 节点 ID;返回的对象、摄像机、关键帧等 ID 属于导演文档,不能与 Canvas 节点 ID 混用。先查询文档与可动画属性通道,再引用已有 ID。创建新对象时选文档内未占用的 ID。",
132
+ "xyq.scene3d.apply.operations 是 {command,args} 数组,command 使用现有 Scene3D SDK 命令名。describe --operation 查看一种操作及其 args,schema 可导出完整输入。dryRun:true 预演编辑,不改变原文档。",
133
+ "transform.position 使用米,rotation 为 XYZ Euler 角度,scale 为无单位 XYZ 倍率;camera FOV 使用角度。几何体 theta/phi/arc 等参数使用弧度,以对应 kind 的 schema 为准。",
134
+ "关键帧与 startFrame/durationFrames 使用帧,帧时长取决于 timeline.fps;动作片段 trimStart/trimEnd 使用源动画秒数,可为小数。不要把微秒、毫秒或旋转弧度填入帧或角度字段。",
135
+ "这套命令编辑导演文档;截图、渲染导出、上传与生成需要相应运行环境和现有入口。资源/动作引用应来自已查询到的真实资源,不用临时 URL 或虚构 ID 代替。",
136
+ ].join("\n\n"),
137
+ next_commands: [
138
+ "pippit-tool-cli canvas command describe create_biz_node --node-kind scene3d",
139
+ 'pippit-tool-cli canvas command run xyq.scene3d.query --canvas-id CANVAS_ID --input \'{"nodeId":"DIRECTOR_NODE_ID"}\'',
140
+ "pippit-tool-cli canvas command describe xyq.scene3d.apply --operation create_node",
141
+ "pippit-tool-cli canvas command schema xyq.scene3d.apply",
142
+ ],
143
+ examples: [
144
+ {
145
+ description: "填入真实外层 nodeId,并确保 camera-new 在导演文档内未占用,再预演创建摄像机。",
146
+ command:
147
+ "pippit-tool-cli canvas command run xyq.scene3d.apply --canvas-id CANVAS_ID --file ./scene3d-input.json",
148
+ input: {
149
+ nodeId: "DIRECTOR_NODE_ID",
150
+ operations: [{ command: "create_node", args: { kind: "camera", id: "camera-new", name: "Close-up" } }],
151
+ dryRun: true,
152
+ },
153
+ },
154
+ ],
155
+ related: ["time", "timeline"],
156
+ },
157
+ ];
158
+
159
+ function listGuides() {
160
+ return GUIDES.map(({ name, summary }) => ({ name, summary }));
161
+ }
162
+
163
+ function getGuide(name) {
164
+ const guide = GUIDES.find((item) => item.name === name);
165
+ if (!guide) {
166
+ throw new Error(`未知的画布指南:${String(name)}。运行 pippit-tool-cli canvas command guide 查看主题。`);
167
+ }
168
+ return JSON.parse(JSON.stringify(guide));
169
+ }
170
+
171
+ module.exports = { listGuides, getGuide };
@@ -8,6 +8,7 @@ const path = require("path");
8
8
  const { spawn } = require("child_process");
9
9
  const { createHash, randomBytes } = require("crypto");
10
10
  const { pathToFileURL } = require("url");
11
+ const { assertOutputBudget, describeEntry, listCatalog, SCHEMA_VERSION } = require("./canvas-command-discovery");
11
12
 
12
13
  const DEFAULT_SDK_MODULE = path.join(
13
14
  __dirname,
@@ -36,9 +37,15 @@ const CANVAS_COMMAND_PERMISSIONS = [
36
37
  ];
37
38
 
38
39
  const COMMAND_HELP = `用法:
39
- pippit-tool-cli canvas command list
40
- pippit-tool-cli canvas command describe <command>
40
+ pippit-tool-cli canvas command list [--category <category>]
41
+ pippit-tool-cli canvas command describe <command> [--operation <name>] [--node-kind <kind>] [--path <schema.path>]
42
+ pippit-tool-cli canvas command guide [topic]
43
+ pippit-tool-cli canvas command schema [command]
41
44
  pippit-tool-cli canvas command run <command> --canvas-id <id> [--input <JSON> | --file <path|->]
45
+
46
+ list 返回简短索引;describe 按层展示字段和可选操作,使用 --path 展开 schema_path。
47
+ guide 按主题说明时间、引用和工作流;schema 显式导出完整 schema(可能很大)。
48
+ 这些发现入口无需登录;run 需要目标画布的访问权限。
42
49
  `;
43
50
 
44
51
  function isCanvasCommand(args) {
@@ -53,17 +60,30 @@ function parseCanvasCommandArgs(args) {
53
60
  }
54
61
 
55
62
  const action = values[0];
56
- if (!new Set(["list", "describe", "run"]).has(action)) {
63
+ if (!new Set(["list", "describe", "schema", "guide", "run"]).has(action)) {
57
64
  throw new Error(`未知的 canvas command 子命令:${action}`);
58
65
  }
59
66
  let canvasId = "";
60
67
  let commandName = "";
61
68
  let filePath = "";
62
69
  let input = "";
70
+ let hasRunOptions = false;
71
+ const discovery = {};
72
+ const discoveryFlags = { "--category": "category", "--operation": "operation", "--node-kind": "nodeKind", "--path": "schemaPath" };
63
73
  for (let index = 1; index < values.length; index += 1) {
64
74
  const value = values[index];
65
75
  if (value === "--help" || value === "-h") return { action: "help" };
76
+ const flag = value.split("=", 1)[0];
77
+ if (discoveryFlags[flag]) {
78
+ const next = value.includes("=") ? value.slice(flag.length + 1) : values[++index];
79
+ if (!next || next.startsWith("--")) throw new Error(`参数 ${flag} 缺少取值`);
80
+ const key = discoveryFlags[flag];
81
+ if (discovery[key]) throw new Error(`重复参数:${flag}`);
82
+ discovery[key] = next;
83
+ continue;
84
+ }
66
85
  if (value === "--canvas-id" || value === "--file" || value === "--input") {
86
+ hasRunOptions = true;
67
87
  const next = values[index + 1];
68
88
  if (next === undefined || next.startsWith("--")) {
69
89
  throw new Error(`参数 ${value} 缺少取值`);
@@ -75,14 +95,17 @@ function parseCanvasCommandArgs(args) {
75
95
  continue;
76
96
  }
77
97
  if (value.startsWith("--canvas-id=")) {
98
+ hasRunOptions = true;
78
99
  canvasId = value.slice("--canvas-id=".length).trim();
79
100
  continue;
80
101
  }
81
102
  if (value.startsWith("--file=")) {
103
+ hasRunOptions = true;
82
104
  filePath = value.slice("--file=".length);
83
105
  continue;
84
106
  }
85
107
  if (value.startsWith("--input=")) {
108
+ hasRunOptions = true;
86
109
  input = value.slice("--input=".length);
87
110
  continue;
88
111
  }
@@ -91,14 +114,22 @@ function parseCanvasCommandArgs(args) {
91
114
  commandName = value;
92
115
  }
93
116
 
117
+ const allowed = action === "list" ? ["category"] : action === "describe" ? ["operation", "nodeKind", "schemaPath"] : [];
118
+ for (const key of Object.keys(discovery)) {
119
+ if (!allowed.includes(key)) throw new Error(`canvas command ${action} 不接受参数 ${key}`);
120
+ }
94
121
  if (action === "list") {
95
- if (commandName || canvasId || filePath || input) throw new Error("canvas command list 不接受额外参数");
96
- return { action };
122
+ if (commandName || hasRunOptions) throw new Error("canvas command list 不接受额外参数");
123
+ return { action, ...discovery };
124
+ }
125
+ if (action === "schema" || action === "guide") {
126
+ if (hasRunOptions) throw new Error(`canvas command ${action} 不接受运行参数`);
127
+ return { action, commandName };
97
128
  }
98
129
  if (!commandName) throw new Error(`canvas command ${action} 缺少 command 名称`);
99
130
  if (action === "describe") {
100
- if (canvasId || filePath || input) throw new Error("canvas command describe 不接受运行参数");
101
- return { action, commandName };
131
+ if (hasRunOptions) throw new Error("canvas command describe 不接受运行参数");
132
+ return { action, commandName, ...discovery };
102
133
  }
103
134
  if (!canvasId) throw new Error("canvas command run 缺少必填参数 --canvas-id");
104
135
  if (input && filePath) throw new Error("--input 和 --file 不能同时使用");
@@ -571,9 +602,16 @@ function definitionToJSON(name, definition) {
571
602
  properties[argument] = schemaToJSON(schema);
572
603
  if (!schema[OPTIONAL_SCHEMA]) required.push(argument);
573
604
  }
605
+ const inputSchema = definition.inputSchema;
574
606
  return {
575
607
  description: definition.description,
576
- input_schema: {
608
+ input_schema: inputSchema ? {
609
+ ...inputSchema,
610
+ ...(inputSchema.properties ? { properties: Object.fromEntries(Object.entries(inputSchema.properties).map(([key, schema]) => [key, {
611
+ ...(properties[key]?.description ? { description: properties[key].description } : {}),
612
+ ...schema,
613
+ }])) } : {}),
614
+ } : {
577
615
  properties,
578
616
  ...(required.length ? { required } : {}),
579
617
  type: "object",
@@ -584,6 +622,7 @@ function definitionToJSON(name, definition) {
584
622
 
585
623
  function createDefinitions(sdk, runtime, allocateNodeId) {
586
624
  const definitions = sdk.createXyqCanvasOpencodeToolDefinitions({
625
+ allocateAssetId: allocateNodeId,
587
626
  allocateNodeId,
588
627
  runtime,
589
628
  schema: createSchemaFactory(),
@@ -663,13 +702,19 @@ function writeJSON(stream, value) {
663
702
  stream.write(`${JSON.stringify(value, null, 2)}\n`);
664
703
  }
665
704
 
666
- function decorateCatalogEntry(sdk, entry) {
705
+ function decorateCatalogEntry(entry) {
667
706
  const output = { ...entry };
668
707
  if (entry.name === "apply_mutations") {
669
- output.mutation_definitions = sdk.XYQ_CANVAS_OPENCODE_MUTATION_DEFINITIONS;
670
- }
671
- if (entry.name === "apply_mutations" || entry.name === "invoke_command") {
672
- output.registered_commands = sdk.XYQ_CANVAS_REGISTERED_COMMAND_DEFINITIONS;
708
+ const properties = entry.input_schema?.properties;
709
+ if (properties?.atomic) {
710
+ output.input_schema = {
711
+ ...entry.input_schema,
712
+ properties: {
713
+ ...properties,
714
+ atomic: { ...properties.atomic, default: true, description: "The CLI always selects atomic execution for apply_mutations." },
715
+ },
716
+ };
717
+ }
673
718
  }
674
719
  return output;
675
720
  }
@@ -679,7 +724,7 @@ function createPublicCatalog(sdk, definitions) {
679
724
  const routes = new Map();
680
725
  const append = (entry, route) => {
681
726
  if (routes.has(entry.name)) return;
682
- entries.push(decorateCatalogEntry(sdk, entry));
727
+ entries.push(decorateCatalogEntry(entry));
683
728
  routes.set(entry.name, route);
684
729
  };
685
730
  for (const [name, definition] of Object.entries(definitions)) {
@@ -688,14 +733,14 @@ function createPublicCatalog(sdk, definitions) {
688
733
  for (const definition of sdk.XYQ_CANVAS_OPENCODE_MUTATION_DEFINITIONS) {
689
734
  append({
690
735
  description: definition.description,
691
- input_schema: { description: definition.input, type: "object" },
736
+ input_schema: definition.inputSchema || { description: definition.input, type: "object" },
692
737
  name: definition.kind,
693
738
  }, { kind: "mutation", name: definition.kind });
694
739
  }
695
740
  for (const definition of sdk.XYQ_CANVAS_REGISTERED_COMMAND_DEFINITIONS) {
696
741
  append({
697
742
  description: definition.description,
698
- input_schema: { description: definition.input, type: "object" },
743
+ input_schema: definition.inputSchema || { description: definition.input, type: "object" },
699
744
  name: definition.name,
700
745
  }, { kind: "registered", name: definition.name });
701
746
  }
@@ -738,6 +783,11 @@ async function runCanvasCommand(args, options = {}) {
738
783
  stdout.write(COMMAND_HELP);
739
784
  return 0;
740
785
  }
786
+ if (parsed.action === "guide") {
787
+ const { getGuide, listGuides } = require("./canvas-command-guides");
788
+ writeJSON(stdout, parsed.commandName ? getGuide(parsed.commandName) : { guides: listGuides(), next: "pippit-tool-cli canvas command guide <topic>" });
789
+ return 0;
790
+ }
741
791
 
742
792
  const sdk = await loadSdk(options);
743
793
  assertSdkFunctions(sdk, ["createXyqCanvasOpencodeToolDefinitions"]);
@@ -754,13 +804,21 @@ async function runCanvasCommand(args, options = {}) {
754
804
  );
755
805
  const { entries: catalog, routes } = createPublicCatalog(sdk, catalogDefinitions);
756
806
  if (parsed.action === "list") {
757
- writeJSON(stdout, { commands: catalog });
807
+ writeJSON(stdout, assertOutputBudget(listCatalog(catalog, parsed.category), "list"));
808
+ return 0;
809
+ }
810
+ if (parsed.action === "schema" && !parsed.commandName) {
811
+ writeJSON(stdout, { schema_version: SCHEMA_VERSION, commands: catalog });
758
812
  return 0;
759
813
  }
760
814
  const catalogEntry = catalog.find((entry) => entry.name === parsed.commandName);
761
815
  if (!catalogEntry) throw new Error(`未知的画布 command:${parsed.commandName}`);
762
816
  if (parsed.action === "describe") {
763
- writeJSON(stdout, catalogEntry);
817
+ writeJSON(stdout, assertOutputBudget(describeEntry(catalogEntry, parsed, catalog), "describe"));
818
+ return 0;
819
+ }
820
+ if (parsed.action === "schema") {
821
+ writeJSON(stdout, { schema_version: SCHEMA_VERSION, ...catalogEntry });
764
822
  return 0;
765
823
  }
766
824
 
@@ -794,6 +852,12 @@ async function runCanvasCommand(args, options = {}) {
794
852
  let saveError;
795
853
  let serializedResult;
796
854
  const allocatedAssetIds = [];
855
+ const allocateAssetId = async () => {
856
+ const [assetId] = await assetRuntime.client.ids.allocate(1);
857
+ if (!assetId) throw new Error("资产服务未返回新资产 ID");
858
+ allocatedAssetIds.push(assetId);
859
+ return assetId;
860
+ };
797
861
  let standalone;
798
862
  const persistence = authStatus.credential_scope
799
863
  ? createFilePersistence({
@@ -804,8 +868,14 @@ async function runCanvasCommand(args, options = {}) {
804
868
  : createMemoryPersistence();
805
869
  try {
806
870
  standalone = sdk.createXyqCanvasCommandRuntime({
871
+ allocateAssetId,
807
872
  canvasId: parsed.canvasId,
873
+ command: { name: parsed.commandName, input },
808
874
  persistence,
875
+ queryAsset: async (assetId) => {
876
+ const asset = await assetRuntime.client.assets.getAsset({ mediaType: "text", pippitAssetId: assetId });
877
+ return { asset: asset?.text?.content ?? null, version: asset?.version };
878
+ },
809
879
  sync: { flush: { maxAttempts: 1, maxBatchSize: 1 } },
810
880
  transportFactory: createCanvasTransportFactory({ assetRuntime, loader }),
811
881
  });
@@ -817,12 +887,19 @@ async function runCanvasCommand(args, options = {}) {
817
887
  const definitions = createDefinitions(
818
888
  sdk,
819
889
  { checkpoints: checkpointStore, permissions: CANVAS_COMMAND_PERMISSIONS, store: standalone.store },
820
- async () => {
821
- const [assetId] = await assetRuntime.client.ids.allocate(1);
822
- allocatedAssetIds.push(assetId);
823
- return assetId;
824
- }
890
+ allocateAssetId
825
891
  );
892
+ if (typeof standalone.prepareCommand === "function") {
893
+ if (route.kind === "registered") {
894
+ await standalone.prepareCommand(route.name, input);
895
+ } else if (route.name === "apply_mutations" && !input.dryRun && Array.isArray(input.mutations)) {
896
+ for (const mutation of input.mutations) {
897
+ if (mutation?.kind === "invoke_command") {
898
+ await standalone.prepareCommand(mutation.name, mutation.args?.[0]);
899
+ }
900
+ }
901
+ }
902
+ }
826
903
  try {
827
904
  serializedResult = await executePublicCommand(route, definitions, input);
828
905
  } catch (error) {
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { install } = require("./install");
4
+
5
+ if (require.main === module) {
6
+ if (process.argv.length === 3 && process.argv[2] === "--help") {
7
+ console.log("Usage: node scripts/install-cli.js\nInstall this package's CLI binary without changing global Skills.");
8
+ } else if (process.argv.length !== 2) {
9
+ console.error("Unsupported arguments. Usage: node scripts/install-cli.js");
10
+ process.exitCode = 1;
11
+ } else {
12
+ try {
13
+ install({ cliOnly: true });
14
+ } catch (err) {
15
+ console.error(`Failed to install pippit-tool-cli: ${err.message || err}`);
16
+ process.exitCode = 1;
17
+ }
18
+ }
19
+ }
@@ -1,12 +1,27 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const fs = require("fs");
4
- const path = require("path");
5
- const { isWindows, run, runSilent } = require("./platform");
6
- const { DEFAULT_PKG, installGlobalPackageSkills } = require("./skills");
7
- const { reportBundledSkillTelemetry } = require("./telemetry");
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { isWindows, run, runSilent } = require('./platform');
6
+ const { DEFAULT_PKG, installGlobalPackageSkills } = require('./skills');
7
+ const { reportBundledSkillTelemetry } = require('./telemetry');
8
8
 
9
- const VERSION = require("../package.json").version.replace(/-.*$/, "");
9
+ const VERSION = require('../package.json').version.replace(/-.*$/, '');
10
+
11
+ const INSTALL_HELP = `Install or update the global Pippit CLI and its bundled skills.
12
+
13
+ Usage:
14
+ pippit-tool-cli install
15
+ pippit-tool-cli install --help
16
+ npx @pippit-dev/cli install
17
+
18
+ Requires npm, npx, network access, and permission to write the global npm package
19
+ and skill directories. Installs the version of this package by default.
20
+ PIPPIT_CLI_INSTALL_PACKAGE can override the package/version to install.
21
+
22
+ Options:
23
+ -h, --help Show this help without installing, updating, or sending telemetry
24
+ `;
10
25
 
11
26
  function defaultInstallPackage() {
12
27
  return `${DEFAULT_PKG}@${VERSION}`;
@@ -18,9 +33,9 @@ function installPackage() {
18
33
 
19
34
  function getGloballyInstalledVersion() {
20
35
  try {
21
- const out = runSilent("npm", ["list", "-g", DEFAULT_PKG], { timeout: 15000 });
36
+ const out = runSilent('npm', ['list', '-g', DEFAULT_PKG], { timeout: 15000 });
22
37
  const match = out.toString().match(/@(\d+\.\d+\.\d+[^\s]*)/);
23
- return match ? match[1] : "unknown";
38
+ return match ? match[1] : 'unknown';
24
39
  } catch (_) {
25
40
  return null;
26
41
  }
@@ -28,24 +43,33 @@ function getGloballyInstalledVersion() {
28
43
 
29
44
  function whichPippitToolCli() {
30
45
  try {
31
- const prefix = runSilent("npm", ["prefix", "-g"], { timeout: 15000 }).toString().trim();
46
+ const prefix = runSilent('npm', ['prefix', '-g'], { timeout: 15000 }).toString().trim();
32
47
  const bin = isWindows
33
- ? path.join(prefix, "pippit-tool-cli.cmd")
34
- : path.join(prefix, "bin", "pippit-tool-cli");
48
+ ? path.join(prefix, 'pippit-tool-cli.cmd')
49
+ : path.join(prefix, 'bin', 'pippit-tool-cli');
35
50
  if (fs.existsSync(bin)) return bin;
36
51
  } catch (_) {
37
52
  // Fall back to PATH lookup.
38
53
  }
39
54
 
40
55
  try {
41
- const cmd = isWindows ? "where" : "which";
42
- return runSilent(cmd, ["pippit-tool-cli"]).toString().split("\n")[0].trim();
56
+ const cmd = isWindows ? 'where' : 'which';
57
+ return runSilent(cmd, ['pippit-tool-cli']).toString().split('\n')[0].trim();
43
58
  } catch (_) {
44
59
  return null;
45
60
  }
46
61
  }
47
62
 
48
- function main() {
63
+ function main(args = process.argv.slice(2)) {
64
+ if (args.includes('--help') || args.includes('-h')) {
65
+ console.log(INSTALL_HELP);
66
+ return;
67
+ }
68
+ if (args.length > 0) {
69
+ console.error(`Unknown install argument: ${args[0]}. Run pippit-tool-cli install --help.`);
70
+ process.exitCode = 1;
71
+ return;
72
+ }
49
73
  const pkg = installPackage();
50
74
  const installed = getGloballyInstalledVersion();
51
75
  if (installed) {
@@ -53,33 +77,33 @@ function main() {
53
77
  } else {
54
78
  console.log(`Installing ${pkg} globally...`);
55
79
  }
56
- run("npm", ["install", "-g", pkg], {
80
+ run('npm', ['install', '-g', pkg], {
57
81
  timeout: 120000,
58
- env: { ...process.env, PIPPIT_CLI_SKIP_SKILLS: "1" },
82
+ env: { ...process.env, PIPPIT_CLI_SKIP_SKILLS: '1' },
59
83
  });
60
84
 
61
- console.log("Installing pippit-tool-cli skills...");
85
+ console.log('Installing pippit-tool-cli skills...');
62
86
  try {
63
87
  installGlobalPackageSkills(DEFAULT_PKG);
64
88
  } catch (err) {
65
89
  if (!installed) {
66
90
  throw err;
67
91
  }
68
- console.log("Existing global package does not contain skills; reinstalling...");
69
- run("npm", ["install", "-g", pkg], { timeout: 120000 });
92
+ console.log('Existing global package does not contain skills; reinstalling...');
93
+ run('npm', ['install', '-g', pkg], { timeout: 120000 });
70
94
  installGlobalPackageSkills(DEFAULT_PKG);
71
95
  }
72
96
 
73
97
  const bin = whichPippitToolCli();
74
98
  if (!bin) {
75
- console.error("pippit-tool-cli was installed, but no global command was found in npm prefix.");
99
+ console.error('pippit-tool-cli was installed, but no global command was found in npm prefix.');
76
100
  console.error("Check that npm's global bin directory is in PATH.");
77
101
  process.exit(1);
78
102
  }
79
103
 
80
104
  console.log(`pippit-tool-cli is ready: ${bin}`);
81
- reportBundledSkillTelemetry("install", "npx_install");
82
- console.log("Try: pippit-tool-cli short-drama +submit-run --message \"写一个短剧开头\"");
105
+ reportBundledSkillTelemetry('install', 'npx_install');
106
+ console.log('Try: pippit-tool-cli short-drama +submit-run --message "写一个短剧开头"');
83
107
  }
84
108
 
85
109
  if (require.main === module) {
@@ -115,7 +115,7 @@ function extractArchive(archivePath, destDir) {
115
115
  run("tar", ["-xzf", archivePath, "-C", destDir]);
116
116
  }
117
117
 
118
- function install() {
118
+ function install({ cliOnly = false } = {}) {
119
119
  if (!platform || !arch) {
120
120
  throw new Error(`Unsupported platform: ${process.platform}-${process.arch}`);
121
121
  }
@@ -133,11 +133,13 @@ function install() {
133
133
  fs.copyFileSync(extracted, dest);
134
134
  fs.chmodSync(dest, 0o755);
135
135
 
136
- if (process.env.PIPPIT_CLI_SKIP_SKILLS !== "1") {
137
- installSkillsFromRoot(ROOT);
138
- reportBundledSkillTelemetry("install", "npm_install");
139
- } else {
140
- cleanupLegacyGlobalSkills();
136
+ if (!cliOnly) {
137
+ if (process.env.PIPPIT_CLI_SKIP_SKILLS !== "1") {
138
+ installSkillsFromRoot(ROOT);
139
+ reportBundledSkillTelemetry("install", "npm_install");
140
+ } else {
141
+ cleanupLegacyGlobalSkills();
142
+ }
141
143
  }
142
144
  console.log(`${NAME} v${VERSION} installed successfully`);
143
145
  } finally {