@remixmate/cli 0.9.73 → 0.10.0

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.
package/LICENSE CHANGED
@@ -1,16 +1,16 @@
1
- Remixmate Commercial License
1
+ RemixMate Commercial License
2
2
 
3
- Copyright (c) 2026 Remixmate. All rights reserved.
3
+ Copyright (c) 2026 RemixMate. All rights reserved.
4
4
 
5
5
  This software, including its source code, object code, skill definitions,
6
- scripts and documentation (the "Software"), is proprietary to Remixmate and is
6
+ scripts and documentation (the "Software"), is proprietary to RemixMate and is
7
7
  protected by copyright law. The Software is licensed, not sold.
8
8
 
9
- 1. Grant of use. Subject to these terms and to the Remixmate Terms of Service
10
- (https://www.remixmate.ai), Remixmate grants you a limited, non-exclusive,
9
+ 1. Grant of use. Subject to these terms and to the RemixMate Terms of Service
10
+ (https://www.remixmate.ai), RemixMate grants you a limited, non-exclusive,
11
11
  non-transferable, non-sublicensable, revocable license to install and run
12
- the Software, unmodified, solely to access Remixmate services with a valid
13
- Remixmate account.
12
+ the Software, unmodified, solely to access RemixMate services with a valid
13
+ RemixMate account.
14
14
 
15
15
  2. Restrictions. Except as expressly permitted above or required by applicable
16
16
  law, you may not:
@@ -22,15 +22,15 @@ protected by copyright law. The Software is licensed, not sold.
22
22
  attempt to derive the source code, algorithms or internal logic of the
23
23
  Software;
24
24
  (d) use the Software, or any part of it, to build a competing product or
25
- service, or to access Remixmate services other than through the
25
+ service, or to access RemixMate services other than through the
26
26
  interfaces the Software provides;
27
27
  (e) remove or alter any copyright, license or proprietary notice.
28
28
 
29
- 3. Ownership. Remixmate retains all right, title and interest in and to the
29
+ 3. Ownership. RemixMate retains all right, title and interest in and to the
30
30
  Software. No rights are granted other than those expressly stated here.
31
31
 
32
32
  4. Termination. This license terminates automatically if you breach any of its
33
- terms or your Remixmate account is terminated. On termination you must stop
33
+ terms or your RemixMate account is terminated. On termination you must stop
34
34
  using and delete all copies of the Software.
35
35
 
36
36
  5. Disclaimer. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
@@ -42,4 +42,4 @@ protected by copyright law. The Software is licensed, not sold.
42
42
  IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
43
43
  CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
44
44
 
45
- For licensing inquiries, contact Remixmate via https://www.remixmate.ai.
45
+ For licensing inquiries, contact RemixMate via https://www.remixmate.ai.
package/README.md CHANGED
@@ -316,4 +316,4 @@ remixmate template-registry --list-templates
316
316
 
317
317
  ## License
318
318
 
319
- Commercial license, all rights reserved. You may install and run it unmodified with a valid Remixmate account; copying, modification, redistribution and reverse engineering are not permitted. See [LICENSE](./LICENSE).
319
+ Commercial license, all rights reserved. You may install and run it unmodified with a valid RemixMate account; copying, modification, redistribution and reverse engineering are not permitted. See [LICENSE](./LICENSE).
package/README.zh-CN.md CHANGED
@@ -256,4 +256,4 @@ remixmate template-registry --list-templates
256
256
 
257
257
  ## 许可证
258
258
 
259
- 商业许可,保留所有权利。仅允许在持有有效 Remixmate 账号的前提下安装并原样使用,禁止复制、修改、再分发和逆向工程。详见 [LICENSE](./LICENSE)。
259
+ 商业许可,保留所有权利。仅允许在持有有效 RemixMate 账号的前提下安装并原样使用,禁止复制、修改、再分发和逆向工程。详见 [LICENSE](./LICENSE)。
package/dist/cli.js CHANGED
@@ -15,8 +15,9 @@ var u=Object.defineProperty;var o=(t,s)=>u(t,"name",{value:s,configurable:!0});i
15
15
  `),process.stdout.write(` remixmate host probe Diagnose host detection and take grouping
16
16
  `),process.stdout.write(` remixmate template init <dir> Create a video-template project (no login)
17
17
  `),process.stdout.write(` remixmate template check [name] Validate templates locally (no login)
18
+ `),process.stdout.write(` remixmate template still <name> Render keyframe PNGs locally (no login)
18
19
  `),process.stdout.write(` remixmate template preview <name> Render a draft preview mp4 remotely
19
- `),process.stdout.write(` remixmate template publish <name> Publish as your private template
20
+ `),process.stdout.write(` remixmate template publish <name> Publish as your private template (needs --yes)
20
21
  `),process.stdout.write(` remixmate template list List your private templates
21
22
  `),process.stdout.write(` remixmate template upgrade Update the project's local check rules (no login)
22
23
  `),process.stdout.write(` remixmate <name> [--flag value ...] Invoke a skill
package/dist/errors.js CHANGED
@@ -1 +1 @@
1
- var t=Object.defineProperty;var e=(E,r)=>t(E,"name",{value:r,configurable:!0});const s={OK:0,ERROR:1,USAGE:2,NOT_AUTHENTICATED:4,BACKEND_UNREACHABLE:5,SPAWN_FAILED:127};class R extends Error{static{e(this,"SkillError")}exitCode;constructor(r,o=s.ERROR){super(r),this.exitCode=o,this.name="SkillError"}}export{s as EXIT,R as SkillError};
1
+ var t=Object.defineProperty;var e=(r,E)=>t(r,"name",{value:E,configurable:!0});const A={OK:0,ERROR:1,USAGE:2,NEEDS_CONFIRMATION:3,NOT_AUTHENTICATED:4,BACKEND_UNREACHABLE:5,SPAWN_FAILED:127};class N extends Error{static{e(this,"SkillError")}exitCode;constructor(E,o=A.ERROR){super(E),this.exitCode=o,this.name="SkillError"}}export{A as EXIT,N as SkillError};
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.9.73",
4
- "generatedAt": "2026-10-08T10:57:35.046Z",
3
+ "version": "0.10.0",
4
+ "generatedAt": "2026-10-08T23:39:22.710Z",
5
5
  "skills": [
6
6
  {
7
7
  "id": "doc-parse",
@@ -1073,7 +1073,7 @@
1073
1073
  "tier": "tool",
1074
1074
  "category": "meta",
1075
1075
  "title": "Video Template Authoring",
1076
- "summary": "Start authoring a custom video template locally: scaffolds a starter project (Remotion Studio preview, local validation, remote draft preview, publishing as the user's private template) into the given directory. After this, follow the project's AGENTS.md and use `remixmate template check|preview|publish`. Local-only; requires Node >= 18 and npm.",
1076
+ "summary": "Start authoring a custom video template locally: scaffolds a starter project (Remotion Studio preview, local validation, remote draft preview, publishing as the user's private template) into the given directory. After this, follow the project's AGENTS.md and use `remixmate template check|still|preview|publish`, stopping for user confirmation after the spec, the keyframe stills, the preview and before publishing (`publish` needs `--yes`). Local-only; requires Node >= 18 and npm.",
1077
1077
  "triggers": [
1078
1078
  "做视频模板 / 做视频模版",
1079
1079
  "自定义视频模板 / 自定义视频模版",
@@ -1,26 +1,33 @@
1
- var w=Object.defineProperty;var a=(o,r)=>w(o,"name",{value:r,configurable:!0});import{cleanDiagnosticText as l}from"../diagnostics.js";import{EXIT as n,SkillError as u}from"../errors.js";import{flagNumber as d,flagString as m,parseTemplateArgs as E,usageError as g}from"./args.js";import{runCheck as b}from"./check.js";import{runUpgrade as k}from"./upgrade.js";import{STARTER_PACKAGE as f,STARTER_VERSION as h}from"./constants.js";import{runInit as j}from"./init.js";import{runList as T}from"./list.js";import{runPreview as R}from"./preview.js";import{runPublish as S}from"./publish.js";const D="template",x=["init","check","preview","publish","list","upgrade"],c={init:{flags:{force:"bool","skip-install":"bool"},maxPositionals:1,usage:`remixmate template init <dir> [--force] [--skip-install] [--json]
2
- 从 ${f}@${h} 创建模板工程并安装依赖(无需登录)。
1
+ var k=Object.defineProperty;var m=(n,s)=>k(n,"name",{value:s,configurable:!0});import{cleanDiagnosticText as u}from"../diagnostics.js";import{EXIT as i,SkillError as d}from"../errors.js";import{flagNumber as l,flagString as o,parseTemplateArgs as E,usageError as f}from"./args.js";import{runCheck as b}from"./check.js";import{runUpgrade as S}from"./upgrade.js";import{STARTER_PACKAGE as g,STARTER_VERSION as h}from"./constants.js";import{runInit as y}from"./init.js";import{runList as R}from"./list.js";import{runPreview as T}from"./preview.js";import{isPublishConfirmation as j,runPublish as P}from"./publish.js";import{MAX_STILLS as A,runStill as N}from"./still.js";const X="template",w=["init","check","still","preview","publish","list","upgrade"],c={init:{flags:{force:"bool","skip-install":"bool"},maxPositionals:1,usage:`remixmate template init <dir> [--force] [--skip-install] [--json]
2
+ 从 ${g}@${h} 创建模板工程并安装依赖(无需登录)。
3
3
  目标目录非空时拒绝,除非 --force。--skip-install 只解压不执行 npm install。
4
4
  `},check:{flags:{"no-write":"bool"},maxPositionals:1,usage:`remixmate template check [name] [--json] [--no-write]
5
5
  在模板工程内运行本地校验(类型、schema、安全策略、props 契约、core 导出)。
6
6
  不传 name 校验 templates/ 下全部模板;默认刷新 coreVersion/coreExports,--no-write 只检查。
7
+ `},still:{flags:{frames:"string","composition-id":"string",scale:"string","out-dir":"string"},maxPositionals:1,usage:`remixmate template still <name> [--frames start,mid,end] [--composition-id ID] [--scale N] [--out-dir DIR] [--json]
8
+ 用工程内的 Remotion 在本地渲染关键帧 PNG(无需登录、不占平台资源),供用户确认画面。
9
+ --frames 取值:start / mid / end / 帧号 / 秒数(如 2.5s),逗号分隔,最多 ${A} 帧;默认 start,mid,end。
10
+ 默认输出到 output/stills/<name>/;--scale 0.5 可缩小图片。
7
11
  `},preview:{flags:{"props-json":"string","duration-sec":"string","composition-id":"string","timeout-sec":"string"},maxPositionals:1,usage:`remixmate template preview <name> [--props-json JSON] [--duration-sec N] [--composition-id ID] [--timeout-sec N] [--json]
8
12
  先 check,再把模板提交到远程渲染器出预览 mp4(需登录)。
9
13
  样例数据取自模板的 dsl-example.json,可用 --props-json 覆盖(传数组 = 每个元素一页)。
10
14
  预览上限:长边 1280、短边 720、总时长 60 秒。
11
- `},publish:{flags:{name:"string","dry-run":"bool"},maxPositionals:1,usage:`remixmate template publish <name> [--name <displayName>] [--dry-run] [--json]
15
+ `},publish:{flags:{name:"string","dry-run":"bool",yes:"bool","skip-preview-check":"bool"},maxPositionals:1,usage:`remixmate template publish <name> [--name <displayName>] [--yes] [--skip-preview-check] [--dry-run] [--json]
12
16
  先 check 与 core 预检,再发布为你的私有模板(需登录)。
17
+ 不带 --yes 只打印待发布摘要并以退出码 3 结束(确认点,不做任何改动);用户确认后加 --yes 真正发布。
18
+ 源码必须与最近一次 template preview 一致,否则拒绝发布;--skip-preview-check 跳过该校验。
13
19
  首次发布生成 templateKey 并写入 templates/<name>/.remixmate.json,之后重复发布即更新。
14
20
  `},list:{flags:{},maxPositionals:0,usage:`remixmate template list [--json]
15
21
  列出你发布的私有模板(需登录)。
16
22
  `},upgrade:{flags:{"dry-run":"bool","with-docs":"bool",force:"bool"},maxPositionals:0,usage:`remixmate template upgrade [--dry-run] [--with-docs] [--force] [--json]
17
- 把工程的本地校验规则(.remixmate/)更新到 ${f}@${h}(无需登录)。
23
+ 把工程的本地校验规则(.remixmate/)更新到 ${g}@${h}(无需登录)。
18
24
  不改动 templates/、src/、examples/、package.json;--with-docs 同时刷新 AGENTS.md / CLAUDE.md(旧文件存为 *.orig)。
19
- `}};function A(){return`Usage: remixmate template <command> [options]
25
+ `}};function O(){return`Usage: remixmate template <command> [options]
20
26
 
21
27
  Commands:
22
28
  init <dir> 创建模板工程(无需登录)
23
29
  check [name] 本地校验(无需登录)
30
+ still <name> 本地渲染关键帧 PNG(无需登录)
24
31
  preview <name> 远程渲染预览 mp4
25
32
  publish <name> 发布为私有模板
26
33
  list 列出我的私有模板
@@ -29,6 +36,6 @@ Commands:
29
36
  Common options: --json --token <PrivToken> --api-base-url <url>
30
37
  环境变量:AB_RENDER_URL(渲染服务地址)、REMIXMATE_STARTER_TARBALL(init 使用本地 starter 包)
31
38
  运行 remixmate template <command> --help 查看子命令说明。
32
- `}a(A,"templateUsage");function y(o){const[r,...e]=o;if(!r||r==="--help"||r==="-h"||r==="help")return{sub:"help",args:{positionals:[],flags:{}}};if(!x.includes(r))throw g(`未知子命令: template ${r}(可选:${x.join(" | ")})`);const t=c[r];return{sub:r,args:E(e,t.flags,{maxPositionals:t.maxPositionals})}}a(y,"parseTemplateCommand");async function P(o,r){const{flags:e,positionals:t}=r,s=e.json===!0,i=m(e,"token"),p=m(e,"api-base-url");switch(o){case"init":if(!t[0])throw g("缺少目标目录:remixmate template init <dir>");return await j({dir:t[0],force:e.force===!0,json:s,skipInstall:e["skip-install"]===!0}),n.OK;case"check":return b({name:t[0],json:s,noWrite:e["no-write"]===!0});case"preview":return await R({name:t[0],propsJson:m(e,"props-json"),durationSec:d(e,"duration-sec"),compositionId:m(e,"composition-id"),timeoutSec:d(e,"timeout-sec"),json:s,token:i,apiBaseUrl:p}),n.OK;case"publish":return await S({name:t[0],displayName:m(e,"name"),dryRun:e["dry-run"]===!0,json:s,token:i,apiBaseUrl:p}),n.OK;case"list":return await T({json:s,token:i,apiBaseUrl:p}),n.OK;case"upgrade":return await k({dryRun:e["dry-run"]===!0,withDocs:e["with-docs"]===!0,force:e.force===!0,json:s}),n.OK}}a(P,"dispatch");async function M(o){const r=o.includes("--json");let e;try{const t=y(o);return e=t.sub,e==="help"?(process.stdout.write(A()),n.OK):t.args.flags.help?(process.stdout.write("Usage: "+c[e].usage),n.OK):await P(e,t.args)}catch(t){const s=t instanceof u?t.exitCode:n.ERROR,i=t instanceof u?t.message:`❌ unexpected error: ${t.stack??String(t)}`;return process.stderr.write(l(i,8192)+`
33
- `),s===n.USAGE&&e&&e!=="help"&&process.stderr.write("Usage: "+c[e].usage),r&&process.stdout.write(JSON.stringify({ok:!1,exitCode:s,error:l(i.replace(/^❌\s*/,""),2048)})+`
34
- `),s}}a(M,"runTemplateCommand");export{c as SUB_SPECS,x as TEMPLATE_SUBCOMMANDS,D as TEMPLATE_VERB,y as parseTemplateCommand,M as runTemplateCommand,A as templateUsage};
39
+ `}m(O,"templateUsage");function C(n){const[s,...e]=n;if(!s||s==="--help"||s==="-h"||s==="help")return{sub:"help",args:{positionals:[],flags:{}}};if(!w.includes(s))throw f(`未知子命令: template ${s}(可选:${w.join(" | ")})`);const t=c[s];return{sub:s,args:E(e,t.flags,{maxPositionals:t.maxPositionals})}}m(C,"parseTemplateCommand");async function I(n,s){const{flags:e,positionals:t}=s,r=e.json===!0,a=o(e,"token"),p=o(e,"api-base-url");switch(n){case"init":if(!t[0])throw f("缺少目标目录:remixmate template init <dir>");return await y({dir:t[0],force:e.force===!0,json:r,skipInstall:e["skip-install"]===!0}),i.OK;case"check":return b({name:t[0],json:r,noWrite:e["no-write"]===!0});case"still":return await N({name:t[0],frames:o(e,"frames"),compositionId:o(e,"composition-id"),scale:l(e,"scale"),outDir:o(e,"out-dir"),json:r}),i.OK;case"preview":return await T({name:t[0],propsJson:o(e,"props-json"),durationSec:l(e,"duration-sec"),compositionId:o(e,"composition-id"),timeoutSec:l(e,"timeout-sec"),json:r,token:a,apiBaseUrl:p}),i.OK;case"publish":{const x=await P({name:t[0],displayName:o(e,"name"),dryRun:e["dry-run"]===!0,yes:e.yes===!0,skipPreviewCheck:e["skip-preview-check"]===!0,json:r,token:a,apiBaseUrl:p});return j(x)?i.NEEDS_CONFIRMATION:i.OK}case"list":return await R({json:r,token:a,apiBaseUrl:p}),i.OK;case"upgrade":return await S({dryRun:e["dry-run"]===!0,withDocs:e["with-docs"]===!0,force:e.force===!0,json:r}),i.OK}}m(I,"dispatch");async function F(n){const s=n.includes("--json");let e;try{const t=C(n);return e=t.sub,e==="help"?(process.stdout.write(O()),i.OK):t.args.flags.help?(process.stdout.write("Usage: "+c[e].usage),i.OK):await I(e,t.args)}catch(t){const r=t instanceof d?t.exitCode:i.ERROR,a=t instanceof d?t.message:`❌ unexpected error: ${t.stack??String(t)}`;return process.stderr.write(u(a,8192)+`
40
+ `),r===i.USAGE&&e&&e!=="help"&&process.stderr.write("Usage: "+c[e].usage),s&&process.stdout.write(JSON.stringify({ok:!1,exitCode:r,error:u(a.replace(/^❌\s*/,""),2048)})+`
41
+ `),r}}m(F,"runTemplateCommand");export{c as SUB_SPECS,w as TEMPLATE_SUBCOMMANDS,X as TEMPLATE_VERB,C as parseTemplateCommand,F as runTemplateCommand,O as templateUsage};
@@ -1 +1 @@
1
- const t="@remixmate/template-starter",e="0.2.0",E=".remixmate/version.json",o=".remixmate",_=["AGENTS.md","CLAUDE.md"],r="REMIXMATE_STARTER_TARBALL",R=".remixmate/check/index.mjs",s=".remixmate.json",x="https://api-render.remixmate.ai",n=1280,A=720,c=60,p=["__pycache__","node_modules","dist",".DS_Store","*.test.*","*.spec.*"];export{R as CHECK_ENTRY,x as DEFAULT_RENDER_BASE_URL,_ as DOC_FILES,s as IDENTITY_FILE,n as PREVIEW_MAX_LONG_EDGE,c as PREVIEW_MAX_SECONDS,A as PREVIEW_MAX_SHORT_EDGE,o as RULES_DIR,E as RULES_VERSION_FILE,t as STARTER_PACKAGE,r as STARTER_TARBALL_ENV,e as STARTER_VERSION,p as TAR_EXCLUDES};
1
+ const t="@remixmate/template-starter",e="0.4.0",E=".remixmate/version.json",o=".remixmate",_=["AGENTS.md","CLAUDE.md"],r="REMIXMATE_STARTER_TARBALL",s=".remixmate/check/index.mjs",R=".remixmate.json",x=".remixmate-state",n="output/stills",p="https://api-render.remixmate.ai",T=1280,c=720,A=60,S=["__pycache__","node_modules","dist",".DS_Store","*.test.*","*.spec.*"];export{s as CHECK_ENTRY,p as DEFAULT_RENDER_BASE_URL,_ as DOC_FILES,R as IDENTITY_FILE,T as PREVIEW_MAX_LONG_EDGE,A as PREVIEW_MAX_SECONDS,c as PREVIEW_MAX_SHORT_EDGE,o as RULES_DIR,E as RULES_VERSION_FILE,t as STARTER_PACKAGE,r as STARTER_TARBALL_ENV,e as STARTER_VERSION,x as STATE_DIR,n as STILLS_DIR,S as TAR_EXCLUDES};
@@ -1,5 +1,5 @@
1
- var k=Object.defineProperty;var m=(e,i)=>k(e,"name",{value:i,configurable:!0});import{existsSync as w,mkdirSync as u,mkdtempSync as x,readdirSync as E,rmSync as T,statSync as g}from"node:fs";import $ from"node:os";import o from"node:path";import{EXIT as f,SkillError as a}from"../errors.js";import{emitProgress as c}from"../progress.js";import{STARTER_PACKAGE as N,STARTER_TARBALL_ENV as A,STARTER_VERSION as R}from"./constants.js";import{Output as j}from"./project.js";import{runNpm as y,runProcess as b}from"./proc.js";import{skillInstallHint as v}from"../install.js";function C(e){if(!w(e))return!1;if(!g(e).isDirectory())throw new a(`❌ ${e} 已存在且不是目录`,f.USAGE);return E(e).length>0}m(C,"isNonEmptyDir");async function G(e,i){const l=(process.env[A]??"").trim();if(l){const t=o.resolve(l);if(!w(t))throw new a(`❌ ${A} 指向的文件不存在: ${t}`,f.USAGE);return i.info(`使用本地 starter 包: ${t}`),{file:t,label:`local:${o.basename(t)}`}}const n=`${N}@${R}`;i.info(`下载 ${n} …`);const r=await y(["pack",n,"--json","--silent"],{cwd:e,stdio:"pipe"});if(r.code!==0){const t=r.stderr.trim().split(`
1
+ var x=Object.defineProperty;var m=(e,i)=>x(e,"name",{value:i,configurable:!0});import{existsSync as w,mkdirSync as k,mkdtempSync as T,readdirSync as E,rmSync as u,statSync as g}from"node:fs";import $ from"node:os";import o from"node:path";import{EXIT as f,SkillError as a}from"../errors.js";import{emitProgress as c}from"../progress.js";import{STARTER_PACKAGE as N,STARTER_TARBALL_ENV as A,STARTER_VERSION as R}from"./constants.js";import{Output as j}from"./project.js";import{runNpm as y,runProcess as G}from"./proc.js";import{skillInstallHint as b}from"../install.js";function v(e){if(!w(e))return!1;if(!g(e).isDirectory())throw new a(`❌ ${e} 已存在且不是目录`,f.USAGE);return E(e).length>0}m(v,"isNonEmptyDir");async function C(e,i){const l=(process.env[A]??"").trim();if(l){const t=o.resolve(l);if(!w(t))throw new a(`❌ ${A} 指向的文件不存在: ${t}`,f.USAGE);return i.info(`使用本地 starter 包: ${t}`),{file:t,label:`local:${o.basename(t)}`}}const n=`${N}@${R}`;i.info(`下载 ${n} …`);const r=await y(["pack",n,"--json","--silent"],{cwd:e,stdio:"pipe"});if(r.code!==0){const t=r.stderr.trim().split(`
2
2
  `).slice(-5).join(`
3
3
  `);throw new a(`❌ npm pack ${n} 失败(退出码 ${r.code})。检查网络与 npm registry 配置。
4
- ${t}`,f.BACKEND_UNREACHABLE)}let s;try{const t=JSON.parse(r.stdout);t[0]?.filename&&(s=o.join(e,t[0].filename))}catch{}if(s??=E(e).filter(t=>t.endsWith(".tgz")).map(t=>o.join(e,t))[0],!s||!w(s))throw new a("❌ npm pack 未产出 tarball");return{file:s,label:n}}m(G,"obtainStarterTarball");async function K(e){const i=new j(!!e.json);if(!e.dir)throw new a("❌ 缺少目标目录:remixmate template init <dir>",f.USAGE);const l=e.dir==="~"||/^~[\\/]/.test(e.dir)?o.join($.homedir(),e.dir.slice(1)):e.dir,n=o.resolve(l);if(C(n)&&!e.force)throw new a(`❌ 目标目录非空: ${n}
5
- 换一个空目录,或确认可以覆盖后加 --force。`,f.USAGE);const r=x(o.join($.tmpdir(),"remixmate-starter-"));try{c({phase:"template-init:fetch",progress:.1});const{file:s,label:t}=await G(r,i);c({phase:"template-init:extract",progress:.4}),u(n,{recursive:!0});const p=await b("tar",["-xzf",s,"-C",n,"--strip-components=1"],{stdio:"pipe"});if(p.code!==0)throw new a(`❌ 解压 starter 失败(tar 退出码 ${p.code}): ${p.stderr.trim().slice(0,300)}`);let d=!1;if(!e.skipInstall){c({phase:"template-init:install",progress:.6}),i.info("安装依赖(npm install)…");const S=await y(["install","--no-audit","--no-fund"],{cwd:n,stdio:"stderr"});if(S.code!==0)throw new a(`❌ npm install 失败(退出码 ${S.code})。工程文件已解压到 ${n},修复后在该目录重跑 npm install。`);d=!0}c({phase:"template-init:done",progress:1}),U(i,n,d);const h={ok:!0,dir:n,starter:t,installed:d};return i.result({...h}),h}finally{T(r,{recursive:!0,force:!0})}}m(K,"runInit");function U(e,i,l){const n=/^[\w@%+=:,./\\~-]+$/.test(i)?i:`"${i}"`;e.info(""),e.info(`✅ 模板工程已创建: ${i}`),e.info(""),e.info("下一步:"),e.info(` cd ${n}`),l||e.info(" npm install"),e.info(" npm run dev # 打开 Remotion Studio 本地预览"),e.info(" remixmate template check <name> # 本地校验(无需登录)"),e.info(" remixmate template preview <name> # 远程渲染预览 mp4(需登录)"),e.info(" remixmate template publish <name> # 发布为你的私有模板(需登录)"),e.info(""),e.info("在本地 agent 里使用:"),e.info(" Codex / WorkBuddy 在工程目录打开,agent 会读取 AGENTS.md"),e.info(" Claude Code 在工程目录打开,agent 会读取 CLAUDE.md(与 AGENTS.md 内容相同)"),e.info(" TraeWork 只认项目级 skill:在工程目录执行一次 remixmate install --project"),e.info(" 其它宿主若还没装 skill:remixmate install"),e.info(" 当前会话不在工程目录里时:先读 "+o.join(i,"AGENTS.md")+",或在该目录重开会话");let r=null;try{r=v()}catch{}r&&(e.info(""),e.info(r))}m(U,"printNextSteps");export{G as obtainStarterTarball,K as runInit};
4
+ ${t}`,f.BACKEND_UNREACHABLE)}let s;try{const t=JSON.parse(r.stdout);t[0]?.filename&&(s=o.join(e,t[0].filename))}catch{}if(s??=E(e).filter(t=>t.endsWith(".tgz")).map(t=>o.join(e,t))[0],!s||!w(s))throw new a("❌ npm pack 未产出 tarball");return{file:s,label:n}}m(C,"obtainStarterTarball");async function K(e){const i=new j(!!e.json);if(!e.dir)throw new a("❌ 缺少目标目录:remixmate template init <dir>",f.USAGE);const l=e.dir==="~"||/^~[\\/]/.test(e.dir)?o.join($.homedir(),e.dir.slice(1)):e.dir,n=o.resolve(l);if(v(n)&&!e.force)throw new a(`❌ 目标目录非空: ${n}
5
+ 换一个空目录,或确认可以覆盖后加 --force。`,f.USAGE);const r=T(o.join($.tmpdir(),"remixmate-starter-"));try{c({phase:"template-init:fetch",progress:.1});const{file:s,label:t}=await C(r,i);c({phase:"template-init:extract",progress:.4}),k(n,{recursive:!0});const p=await G("tar",["-xzf",s,"-C",n,"--strip-components=1"],{stdio:"pipe"});if(p.code!==0)throw new a(`❌ 解压 starter 失败(tar 退出码 ${p.code}): ${p.stderr.trim().slice(0,300)}`);let d=!1;if(!e.skipInstall){c({phase:"template-init:install",progress:.6}),i.info("安装依赖(npm install)…");const S=await y(["install","--no-audit","--no-fund"],{cwd:n,stdio:"stderr"});if(S.code!==0)throw new a(`❌ npm install 失败(退出码 ${S.code})。工程文件已解压到 ${n},修复后在该目录重跑 npm install。`);d=!0}c({phase:"template-init:done",progress:1}),U(i,n,d);const h={ok:!0,dir:n,starter:t,installed:d};return i.result({...h}),h}finally{u(r,{recursive:!0,force:!0})}}m(K,"runInit");function U(e,i,l){const n=/^[\w@%+=:,./\\~-]+$/.test(i)?i:`"${i}"`;e.info(""),e.info(`✅ 模板工程已创建: ${i}`),e.info(""),e.info("下一步:"),e.info(` cd ${n}`),l||e.info(" npm install"),e.info(" npm run dev # 打开 Remotion Studio 本地预览"),e.info(" remixmate template check <name> # 本地校验(无需登录)"),e.info(" remixmate template still <name> # 本地渲染关键帧 PNG,确认画面(无需登录)"),e.info(" remixmate template preview <name> # 远程渲染预览 mp4(需登录)"),e.info(" remixmate template publish <name> # 打印待发布摘要;确认后加 --yes 发布(需登录)"),e.info(""),e.info("在本地 agent 里使用:"),e.info(" Codex / WorkBuddy 在工程目录打开,agent 会读取 AGENTS.md"),e.info(" Claude Code 在工程目录打开,agent 会读取 CLAUDE.md(与 AGENTS.md 内容相同)"),e.info(" TraeWork 只认项目级 skill:在工程目录执行一次 remixmate install --project"),e.info(" 其它宿主若还没装 skill:remixmate install"),e.info(" 当前会话不在工程目录里时:先读 "+o.join(i,"AGENTS.md")+",或在该目录重开会话");let r=null;try{r=b()}catch{}r&&(e.info(""),e.info(r))}m(U,"printNextSteps");export{C as obtainStarterTarball,K as runInit};
@@ -1,7 +1,8 @@
1
- var O=Object.defineProperty;var f=(e,t)=>O(e,"name",{value:t,configurable:!0});import{existsSync as U,readdirSync as N,readFileSync as L,statSync as B}from"node:fs";import $ from"node:path";import{EXIT as x,SkillError as w}from"../errors.js";import{resolveHttpContext as C}from"../http.js";import{emitProgress as A}from"../progress.js";import{IDENTITY_FILE as G,PREVIEW_MAX_LONG_EDGE as R,PREVIEW_MAX_SECONDS as k,PREVIEW_MAX_SHORT_EDGE as j}from"./constants.js";import{runCheck as X}from"./check.js";import{Output as W,readJsonFile as M,requireProjectRoot as V,requireTemplateDir as J,validateTemplateName as Y}from"./project.js";import{RenderBusinessError as q,renderPost as z,resolveRenderBaseUrl as H}from"./render-client.js";const g=30,K=5,Q=180,Z=new Set(["node_modules","dist","__pycache__",".git"]),tt=new Set([".ts",".tsx",".js",".jsx",".mts",".cts",".json",".css",".scss",".md"]),et=512*1024,ot=20*1024*1024,D={"16:9":[16,9],"9:16":[9,16],"1:1":[1,1],"3:4":[3,4],"4:3":[4,3]},T=`草稿预览超出上限:长边 ≤ ${R}px、短边 ≤ ${j}px、时长 ≤ ${k} 秒。请缩短 --duration-sec,或减少 --props-json 的页数;正式出片用 remixmate render-video,不受此限。`;function nt(e,t=()=>{}){const r={};let n=0;const i=f(p=>{for(const o of N(p,{withFileTypes:!0})){const a=$.join(p,o.name);if(o.isDirectory()){Z.has(o.name)||i(a);continue}if(!tt.has($.extname(o.name))||/\.(test|spec)\./.test(o.name)||p===e&&o.name===G)continue;const s=B(a).size;if(s>et){t(` ⚠ 跳过超大文件(${(s/1024).toFixed(0)} KB):${$.relative(e,a)}`);continue}n+=s,r[$.relative(e,a).split($.sep).join("/")]=L(a,"utf8")}},"walk");return i(e),{files:r,bytes:n}}f(nt,"collectFiles");function st(e,t){const r={};for(const[n,i]of Object.entries(t??{})){if(!i||typeof i.from!="string")continue;const p=i.from;if(p==="textLayers"){const a=(e.textLayers??[]).find(s=>s.role===i.role);a?.content!==void 0&&(r[n]=a.content);continue}const o=p.split(".").reduce((a,s)=>a==null?a:a[s],e);o!==void 0&&(r[n]=o)}return r}f(st,"applyExtractors");function rt(e,t,r,n,i=()=>{}){const p=$.join(e,"dsl-example.json"),o=U(p)?M(p):null,a=Array.isArray(o?.assets)?o.assets:[];if(n!==void 0){let u;try{u=JSON.parse(n)}catch(h){throw new w(`❌ --props-json 解析失败: ${h.message}`,x.USAGE)}return{props:u,assets:a,durationSec:null}}const s=(o?.scenes??[])[0];if(!s)return{props:{},assets:a,durationSec:null};const d=(t.compositions??[]).find(u=>u.compositionId===r)?.slot,l=d?t.slotMapping?.[d]?.propExtractors:void 0;d&&d!=="cover"&&!l&&i(` ⚠ template.json 的 slotMapping.${d} 未声明 propExtractors —— 预览将只带 templateData。`);const m=st(s,l);return s.customPayload&&(m.templateData=s.customPayload),{props:m,assets:a,durationSec:typeof s.duration=="number"?s.duration:null,scene:s}}f(rt,"buildSampleProps");function it(e,t){const r=e?.audio?.narration;if(!r)return[];const n=f(l=>l.filter(m=>m.text).map((m,u,h)=>({text:m.text,startFrame:Math.max(0,Math.round(m.startFrame)),endFrame:u===h.length-1?t:Math.round(h[u+1].startFrame)})).filter(m=>m.endFrame>m.startFrame),"finish"),i=Array.isArray(r.items)?r.items:null;if(i&&i.length>0&&i.some(l=>typeof l?.atSec=="number")){let l=0;return n(i.map(m=>{const u=typeof m?.atSec=="number"?m.atSec*g:l;return l=u,{text:m?.text??"",startFrame:u}}))}const p=typeof r.text=="string"?r.text.trim():"";if(!p)return[];const o=p.split(/(?<=[。!?!?])/).map(l=>l.trim()).filter(Boolean);if(o.length===0)return[];const a=o.map(l=>Math.max(1,l.length)),s=a.reduce((l,m)=>l+m,0);let d=0;return n(o.map((l,m)=>{const u=d/s*t;return d+=a[m],{text:l,startFrame:u}}))}f(it,"buildSubtitleSegments");function at(e){const[t,r]=D[e]??D["16:9"],n=t>=r,i=n?t:r,p=n?r:t,o=Math.min(R/i,j/p),a=f(l=>Math.floor(l/2)*2,"even"),s=a(t*o),d=a(r*o);return[s,d]}f(at,"previewDimensions");function ct(e,t,r=()=>{}){const n=M($.join(e,"template.json")),i=Array.isArray(n.compositions)?n.compositions:[],p=t.compositionId??i[0]?.compositionId;if(!p)throw new w("❌ template.json 未声明 compositions",x.USAGE);if(t.compositionId&&!i.some(I=>I.compositionId===p))throw new w(`❌ composition "${p}" 不在该模板中。可选: ${i.map(I=>I.compositionId).join(", ")}`,x.USAGE);const{files:o,bytes:a}=nt(e,r);if(Object.keys(o).length===0)throw new w("❌ 模板目录没有可提交的源文件");a>ot&&r(` ⚠ 模板总体积 ${(a/1024/1024).toFixed(1)} MB,接近 ab-render 的请求上限`);const s=rt(e,n,p,t.propsJson,r),d=(n.supportedAspectRatios??[])[0]??"16:9",l=Number(t.durationSec??s.durationSec??n.supportedDurations?.min??K),[m,u]=at(d),h=Array.isArray(s.props)?s.props:null,y=Math.max(1,Math.round(l*g)),_=h?y*h.length:y,E=_/g;if(E>k)throw new w(`❌ ${T}(本次 ${E.toFixed(1)} 秒)`,x.USAGE);const c=h?[]:it(s.scene,y),F={timeline:(h??[s.props]).map((I,b)=>{const v=b*y,P=v+y;return{sceneId:h?`preview-${String(b+1).padStart(2,"0")}`:"preview",compositionId:p,startFrame:v,endFrame:P,durationFrames:y,startTime:Number((v/g).toFixed(2)),endTime:Number((P/g).toFixed(2)),props:I,layers:[],subtitleSegments:c,transition:{type:"none",durationFrames:0}}}),renderConfig:{width:m,height:u,fps:g,totalFrames:_,totalDuration:Number(E.toFixed(2)),codec:"h264",crf:18,outputFormat:"mp4"},resolvedAssets:s.assets,globalTypography:n.defaultTypography,colorScheme:n.defaultColorScheme,motionPreset:n.defaultMotionPreset};return{files:o,inputProps:F,compositionId:p,totalSeconds:E}}f(ct,"buildPreviewPlan");function pt(e){return JSON.stringify(e.payload??{}).includes("draft_preview_limit_exceeded")}f(pt,"isLimitError");function lt(e,t){const n=t.payload?.data?.issues??t.payload?.issues;if(!Array.isArray(n)||n.length===0)return!1;e.warn(`
2
- ✗ 被 ab-render 的安全扫描拒绝(${n.length} 处):`);for(const i of n)e.warn(typeof i=="string"?` · ${i}`:` · ${i.file}
3
- ${i.reason}${i.detail?`: ${i.detail}`:""}`);return e.warn(`
4
- 先在本地运行 remixmate template check,它与平台使用同一份安全策略。`),!0}f(lt,"printScanIssues");async function $t(e){const t=new W(!!e.json),r=Y(e.name),n=V(e.cwd),i=J(n,r);A({phase:"template-preview:check",progress:.05});const p=await X({name:r,cwd:n,stdio:e.json?"stderr":"inherit"});if(p!==0)throw new w(`❌ 本地校验未通过(check 退出码 ${p}),已中止预览。`,p===2?x.USAGE:x.ERROR);const o=ct(i,e,c=>t.warn(c)),a=await C("template-preview",{privateToken:e.token,apiBaseUrl:e.apiBaseUrl}),s=H(),{width:d,height:l}=o.inputProps.renderConfig;t.info(`模板 ${r}`),t.info(`composition ${o.compositionId}`),t.info(`画幅 ${d}×${l} ${o.totalSeconds.toFixed(1)}s @${g}fps`),t.info(`文件 ${Object.keys(o.files).length} 个`),t.info(`ab-render ${s}`);const m=f((c,S)=>z(s,c,S,{token:a.privateToken,skillName:"template-preview"}),"post");let u;try{A({phase:"template-preview:submit",progress:.1});const c=await m("/renderDraft",{files:o.files,inputProps:o.inputProps,upload:!1});if(!c.taskId)throw new w("❌ /renderDraft 未返回 taskId");u=c.taskId}catch(c){if(c instanceof q){if(pt(c))throw new w(`❌ ${T}`,x.ERROR);if(lt(t,c))throw new w("❌ 预览被安全扫描拒绝",x.ERROR)}throw c}t.info(`taskId ${u}`);const h=e.timeoutSec??Q,y=e.pollIntervalMs??1500,_=Date.now()+h*1e3;let E="";for(;;){if(Date.now()>_)throw new w(`❌ 轮询超时(${h}s),任务 ${u} 仍在 ab-render 排队/渲染。
5
- 加长等待:--timeout-sec 900`);await new Promise(S=>setTimeout(S,y));const c=await m("/renderDraftStatus",{taskId:u});if(c.status!==E&&(E=c.status??"",t.info(c.status==="pending"?"排队中…(队列前面还有任务)":`状态: ${c.status}`)),A({phase:`template-preview:${c.status??"unknown"}`,progress:.1+.9*(c.progress??0),taskId:u}),c.status==="succeeded"){const S=c.previewUrl?.startsWith("http")?c.previewUrl:`${s}${c.previewUrl??""}`;t.info(`
1
+ var O=Object.defineProperty;var h=(o,t)=>O(o,"name",{value:t,configurable:!0});import{existsSync as U,readdirSync as N,readFileSync as C,statSync as L}from"node:fs";import $ from"node:path";import{EXIT as E,SkillError as S}from"../errors.js";import{resolveHttpContext as B}from"../http.js";import{emitProgress as T}from"../progress.js";import{IDENTITY_FILE as G,PREVIEW_MAX_LONG_EDGE as R,PREVIEW_MAX_SECONDS as k,PREVIEW_MAX_SHORT_EDGE as j}from"./constants.js";import{runCheck as X}from"./check.js";import{Output as W,readJsonFile as D,requireProjectRoot as V,requireTemplateDir as H,validateTemplateName as J}from"./project.js";import{RenderBusinessError as Y,renderPost as q,resolveRenderBaseUrl as z}from"./render-client.js";import{hashTemplateSource as K,writePreviewRecord as Q}from"./state.js";const g=30,Z=5,tt=180,et=new Set(["node_modules","dist","__pycache__",".git"]),ot=new Set([".ts",".tsx",".js",".jsx",".mts",".cts",".json",".css",".scss",".md"]),nt=512*1024,rt=20*1024*1024,M={"16:9":[16,9],"9:16":[9,16],"1:1":[1,1],"3:4":[3,4],"4:3":[4,3]},A=`草稿预览超出上限:长边 ≤ ${R}px、短边 ≤ ${j}px、时长 ≤ ${k} 秒。请缩短 --duration-sec,或减少 --props-json 的页数;正式出片用 remixmate render-video,不受此限。`;function st(o,t=()=>{}){const r={};let n=0;const s=h(a=>{for(const c of N(a,{withFileTypes:!0})){const e=$.join(a,c.name);if(c.isDirectory()){et.has(c.name)||s(e);continue}if(!ot.has($.extname(c.name))||/\.(test|spec)\./.test(c.name)||a===o&&c.name===G)continue;const i=L(e).size;if(i>nt){t(` ⚠ 跳过超大文件(${(i/1024).toFixed(0)} KB):${$.relative(o,e)}`);continue}n+=i,r[$.relative(o,e).split($.sep).join("/")]=C(e,"utf8")}},"walk");return s(o),{files:r,bytes:n}}h(st,"collectFiles");function it(o,t){const r={};for(const[n,s]of Object.entries(t??{})){if(!s||typeof s.from!="string")continue;const a=s.from;if(a==="textLayers"){const e=(o.textLayers??[]).find(i=>i.role===s.role);e?.content!==void 0&&(r[n]=e.content);continue}const c=a.split(".").reduce((e,i)=>e==null?e:e[i],o);c!==void 0&&(r[n]=c)}return r}h(it,"applyExtractors");function at(o,t,r,n,s=()=>{}){const a=$.join(o,"dsl-example.json"),c=U(a)?D(a):null,e=Array.isArray(c?.assets)?c.assets:[];if(n!==void 0){let d;try{d=JSON.parse(n)}catch(u){throw new S(`❌ --props-json 解析失败: ${u.message}`,E.USAGE)}return{props:d,assets:e,durationSec:null}}const i=(c?.scenes??[])[0];if(!i)return{props:{},assets:e,durationSec:null};const f=(t.compositions??[]).find(d=>d.compositionId===r)?.slot,l=f?t.slotMapping?.[f]?.propExtractors:void 0;f&&f!=="cover"&&!l&&s(` ⚠ template.json 的 slotMapping.${f} 未声明 propExtractors —— 预览将只带 templateData。`);const m=it(i,l);return i.customPayload&&(m.templateData=i.customPayload),{props:m,assets:e,durationSec:typeof i.duration=="number"?i.duration:null,scene:i}}h(at,"buildSampleProps");function ct(o,t){const r=o?.audio?.narration;if(!r)return[];const n=h(l=>l.filter(m=>m.text).map((m,d,u)=>({text:m.text,startFrame:Math.max(0,Math.round(m.startFrame)),endFrame:d===u.length-1?t:Math.round(u[d+1].startFrame)})).filter(m=>m.endFrame>m.startFrame),"finish"),s=Array.isArray(r.items)?r.items:null;if(s&&s.length>0&&s.some(l=>typeof l?.atSec=="number")){let l=0;return n(s.map(m=>{const d=typeof m?.atSec=="number"?m.atSec*g:l;return l=d,{text:m?.text??"",startFrame:d}}))}const a=typeof r.text=="string"?r.text.trim():"";if(!a)return[];const c=a.split(/(?<=[。!?!?])/).map(l=>l.trim()).filter(Boolean);if(c.length===0)return[];const e=c.map(l=>Math.max(1,l.length)),i=e.reduce((l,m)=>l+m,0);let f=0;return n(c.map((l,m)=>{const d=f/i*t;return f+=e[m],{text:l,startFrame:d}}))}h(ct,"buildSubtitleSegments");function pt(o){const[t,r]=M[o]??M["16:9"],n=t>=r,s=n?t:r,a=n?r:t,c=Math.min(R/s,j/a),e=h(l=>Math.floor(l/2)*2,"even"),i=e(t*c),f=e(r*c);return[i,f]}h(pt,"previewDimensions");function lt(o,t,r=()=>{}){const n=D($.join(o,"template.json")),s=Array.isArray(n.compositions)?n.compositions:[],a=t.compositionId??s[0]?.compositionId;if(!a)throw new S("❌ template.json 未声明 compositions",E.USAGE);if(t.compositionId&&!s.some(x=>x.compositionId===a))throw new S(`❌ composition "${a}" 不在该模板中。可选: ${s.map(x=>x.compositionId).join(", ")}`,E.USAGE);const{files:c,bytes:e}=st(o,r);if(Object.keys(c).length===0)throw new S("❌ 模板目录没有可提交的源文件");e>rt&&r(` ⚠ 模板总体积 ${(e/1024/1024).toFixed(1)} MB,接近 ab-render 的请求上限`);const i=at(o,n,a,t.propsJson,r),f=(n.supportedAspectRatios??[])[0]??"16:9",l=Number(t.durationSec??i.durationSec??n.supportedDurations?.min??Z),[m,d]=pt(f),u=Array.isArray(i.props)?i.props:null,y=Math.max(1,Math.round(l*g)),_=u?y*u.length:y,I=_/g;if(I>k)throw new S(`❌ ${A}(本次 ${I.toFixed(1)} 秒)`,E.USAGE);const v=u?[]:ct(i.scene,y),w={timeline:(u??[i.props]).map((x,P)=>{const F=P*y,b=F+y;return{sceneId:u?`preview-${String(P+1).padStart(2,"0")}`:"preview",compositionId:a,startFrame:F,endFrame:b,durationFrames:y,startTime:Number((F/g).toFixed(2)),endTime:Number((b/g).toFixed(2)),props:x,layers:[],subtitleSegments:v,transition:{type:"none",durationFrames:0}}}),renderConfig:{width:m,height:d,fps:g,totalFrames:_,totalDuration:Number(I.toFixed(2)),codec:"h264",crf:18,outputFormat:"mp4"},resolvedAssets:i.assets,globalTypography:n.defaultTypography,colorScheme:n.defaultColorScheme,motionPreset:n.defaultMotionPreset};return{files:c,inputProps:w,compositionId:a,totalSeconds:I}}h(lt,"buildPreviewPlan");function mt(o){return JSON.stringify(o.payload??{}).includes("draft_preview_limit_exceeded")}h(mt,"isLimitError");function ut(o,t){const n=t.payload?.data?.issues??t.payload?.issues;if(!Array.isArray(n)||n.length===0)return!1;o.warn(`
2
+ ✗ 被 ab-render 的安全扫描拒绝(${n.length} 处):`);for(const s of n)o.warn(typeof s=="string"?` · ${s}`:` · ${s.file}
3
+ ${s.reason}${s.detail?`: ${s.detail}`:""}`);return o.warn(`
4
+ 先在本地运行 remixmate template check,它与平台使用同一份安全策略。`),!0}h(ut,"printScanIssues");async function _t(o){const t=new W(!!o.json),r=J(o.name),n=V(o.cwd),s=H(n,r);T({phase:"template-preview:check",progress:.05});const a=await X({name:r,cwd:n,stdio:o.json?"stderr":"inherit"});if(a!==0)throw new S(`❌ 本地校验未通过(check 退出码 ${a}),已中止预览。`,a===2?E.USAGE:E.ERROR);const c=K(s),e=lt(s,o,p=>t.warn(p)),i=await B("template-preview",{privateToken:o.token,apiBaseUrl:o.apiBaseUrl}),f=z(),{width:l,height:m}=e.inputProps.renderConfig;t.info(`模板 ${r}`),t.info(`composition ${e.compositionId}`),t.info(`画幅 ${l}×${m} ${e.totalSeconds.toFixed(1)}s @${g}fps`),t.info(`文件 ${Object.keys(e.files).length} 个`),t.info(`ab-render ${f}`);const d=h((p,w)=>q(f,p,w,{token:i.privateToken,skillName:"template-preview"}),"post");let u;try{T({phase:"template-preview:submit",progress:.1});const p=await d("/renderDraft",{files:e.files,inputProps:e.inputProps,upload:!1});if(!p.taskId)throw new S("❌ /renderDraft 未返回 taskId");u=p.taskId}catch(p){if(p instanceof Y){if(mt(p))throw new S(`❌ ${A}`,E.ERROR);if(ut(t,p))throw new S("❌ 预览被安全扫描拒绝",E.ERROR)}throw p}t.info(`taskId ${u}`);const y=o.timeoutSec??tt,_=o.pollIntervalMs??1500,I=Date.now()+y*1e3;let v="";for(;;){if(Date.now()>I)throw new S(`❌ 轮询超时(${y}s),任务 ${u} 仍在 ab-render 排队/渲染。
5
+ 加长等待:--timeout-sec 900`);await new Promise(w=>setTimeout(w,_));const p=await d("/renderDraftStatus",{taskId:u});if(p.status!==v&&(v=p.status??"",t.info(p.status==="pending"?"排队中…(队列前面还有任务)":`状态: ${p.status}`)),T({phase:`template-preview:${p.status??"unknown"}`,progress:.1+.9*(p.progress??0),taskId:u}),p.status==="succeeded"){const w=p.previewUrl?.startsWith("http")?p.previewUrl:`${f}${p.previewUrl??""}`;t.info(`
6
6
  ✅ 预览就绪
7
- ${S}`),t.info(" 临时产物,最长保留 24 小时(服务重启后可能更早失效)。");const F={ok:!0,name:r,taskId:u,url:S,compositionId:o.compositionId,durationSec:o.totalSeconds};return t.result({...F}),F}if(c.status==="failed"){const S=c.error??"(无错误详情)";throw S.includes("draft_preview_limit_exceeded")?new w(`❌ ${T}`):new w(`❌ 渲染失败: ${S}`)}}}f($t,"runPreview");export{T as PREVIEW_LIMIT_MESSAGE,ct as buildPreviewPlan,rt as buildSampleProps,it as buildSubtitleSegments,nt as collectFiles,at as previewDimensions,$t as runPreview};
7
+ ${w}`),t.info(" 临时产物,最长保留 24 小时(服务重启后可能更早失效)。"),Q(n,r,{sourceHash:c,taskId:u,url:w,compositionId:e.compositionId,durationSec:e.totalSeconds,at:new Date().toISOString()}),t.info(`
8
+ ⏸ 确认点:请把预览链接交给用户查看动效与节奏,等用户确认后再发布。`),t.info(" 之后若再改动模板代码,发布前需要重新预览。");const x={ok:!0,name:r,taskId:u,url:w,compositionId:e.compositionId,durationSec:e.totalSeconds,awaitingConfirmation:!0};return t.result({...x}),x}if(p.status==="failed"){const w=p.error??"(无错误详情)";throw w.includes("draft_preview_limit_exceeded")?new S(`❌ ${A}`):new S(`❌ 渲染失败: ${w}`)}}}h(_t,"runPreview");export{A as PREVIEW_LIMIT_MESSAGE,lt as buildPreviewPlan,at as buildSampleProps,ct as buildSubtitleSegments,st as collectFiles,pt as previewDimensions,_t as runPreview};
@@ -1,5 +1,10 @@
1
- var O=Object.defineProperty;var u=(r,e)=>O(r,"name",{value:e,configurable:!0});import{cpSync as P,mkdtempSync as V,readFileSync as D,rmSync as A,writeFileSync as J}from"node:fs";import I from"node:os";import f from"node:path";import{EXIT as g,SkillError as d}from"../errors.js";import{mmPost as x,resolveHttpContext as F}from"../http.js";import{emitProgress as p}from"../progress.js";import{IDENTITY_FILE as B,TAR_EXCLUDES as C}from"./constants.js";import{runCheck as z}from"./check.js";import{Output as H,readJsonFile as L,requireProjectRoot as _,requireTemplateDir as q,resolveTemplateKey as X,validateTemplateName as G,writeIdentity as Y}from"./project.js";import{runProcess as M}from"./proc.js";import{fetchCoreSurface as Q,resolveRenderBaseUrl as W}from"./render-client.js";function Z(r,e){const t=["-czf",e];for(const i of C)t.push(`--exclude=${i}`);return t.push("-C",r,"."),t}u(Z,"tarArgs");function ee(r,e,t=()=>{}){if(!e)return"ab-render /health 未返回 coreSurface —— 该实例版本过旧,无法校验 core 契约";if(!e.available)return`ab-render 无法读取 template-core:${e.error??"未知原因"}`;if(!Array.isArray(r.coreExports))return"template.json 缺少 coreExports —— 先运行 remixmate template check 生成";const i=new Set(e.exports??[]),l=r.coreExports.filter(a=>!i.has(a)),s=typeof r.coreVersion=="string"?r.coreVersion:null,o=Array.isArray(e.versions)&&e.versions.length>0?e.versions:e.packageVersion?[e.packageVersion]:[];return l.length>0?`目标 ab-render 的 template-core 缺少导出:${l.join(", ")}。模板声明需要 core@${s??"(未声明)"},平台当前提供 ${o.length?o.map(a=>`core@${a}`).join(", "):"(未知版本)"}。请改用平台已有的导出,或等平台升级 core 后再发布。`:(s&&o.length>0&&!o.includes(s)&&t(` ⚠ 模板声明 core@${s},平台提供 ${o.join(", ")} —— 导出齐全可以渲染,但效果未必与本地完全一致`),null)}u(ee,"evaluateCoreSurface");function re(r,e){return M("tar",Z(r,e),{stdio:"pipe"}).then(t=>{if(t.code!==0)throw new d(`❌ 打包失败(tar 退出码 ${t.code}): ${t.stderr.trim().slice(0,300)}`)})}u(re,"tarball");async function te(r,e){let t;try{t=await fetch(r,{method:"PUT",headers:{"Content-Type":"application/gzip"},body:new Uint8Array(e)})}catch(i){throw new d(`❌ 上传模板包失败(网络错误): ${i.message}`,g.BACKEND_UNREACHABLE)}if(t.status>=300)throw new d(`❌ 上传模板包失败(OSS HTTP ${t.status})`,t.status>=500?g.BACKEND_UNREACHABLE:g.ERROR)}u(te,"putToOss");async function he(r){const e=new H(!!r.json),t=G(r.name),i=_(r.cwd),l=q(i,t);p({phase:"template-publish:check",progress:.05});const s=await z({name:t,cwd:i,stdio:r.json?"stderr":"inherit"});if(s!==0)throw new d(`❌ 本地校验未通过(check 退出码 ${s}),已中止发布。`,s===2?g.USAGE:g.ERROR);const o=L(f.join(l,"template.json")),{templateKey:a,existing:v}=X(l),$=r.displayName??(typeof o.name=="string"&&o.name?o.name:t),h=typeof o.coreVersion=="string"?o.coreVersion:"";let E=null;r.dryRun||(E=await F("template-publish",{privateToken:r.token,apiBaseUrl:r.apiBaseUrl}));const N=W();e.info(`模板 ${t}`),e.info(`templateKey ${a}${v?"":"(首次发布,新生成)"}`),e.info(`名称 ${$}`),e.info(`coreVersion ${h||"(未声明)"}`),p({phase:"template-publish:preflight",progress:.15});const k=await Q(N),K=ee(o,k,c=>e.warn(c));if(K)throw new d(`❌ core 预检未通过:${K}`);if(e.info(`core 预检 通过(${k?.exports.length??0} 个导出)`),r.dryRun){e.info(`
2
- [dry-run] 将依次调用 prepareSave → presignUpload → 上传 → save(isBuiltin=false)`),e.info(`[dry-run] 打包排除:${C.join(", ")}(保留 dsl-example.json)`);const c={ok:!0,name:t,templateKey:a,version:null,dryRun:!0,created:!v};return e.result({...c}),c}const b=E;let y;try{p({phase:"template-publish:prepare",progress:.3});const c=await x(b,"/remotionTemplate/prepareSave",{templateJson:o,name:$,templateKey:a,...h?{coreVersion:h}:{}}),n=c?.templateKey||a,S={...c?.templateJson||o,templateId:n};y=V(f.join(I.tmpdir(),"remixmate-publish-"));const w=f.join(y,"pkg");P(l,w,{recursive:!0}),A(f.join(w,B),{force:!0}),J(f.join(w,"template.json"),JSON.stringify(S,null,2)+`
3
- `,"utf8"),JSON.stringify(S)!==JSON.stringify(o)&&e.warn(" ⚠ 服务端规范化后的 template.json 与本地不同;本次上传使用规范化版本,本地文件未改动。"),p({phase:"template-publish:pack",progress:.45});const T=f.join(y,`${n}.tar.gz`);await re(w,T);const R=D(T);e.info(`打包 ${(R.length/1024).toFixed(0)} KB`),p({phase:"template-publish:presign",progress:.55});const m=await x(b,"/remotionTemplate/presignUpload",{templateKey:n});if(!m?.uploadUrl)throw new d("❌ presignUpload 未返回 uploadUrl");p({phase:"template-publish:upload",progress:.7}),await te(String(m.uploadUrl),R),p({phase:"template-publish:save",progress:.85}),await x(b,"/remotionTemplate/save",{templateKey:n,name:$,isBuiltin:!1,sourceOssKey:m.sourceOssKey,version:m.version,templateJson:S,...h?{coreVersion:h}:{}});const j=typeof m.version=="number"?m.version:Number(m.version)||0;Y(l,{templateKey:n,version:j}),p({phase:"template-publish:done",progress:1,templateKey:n}),e.info(`
4
- ✅ 已发布为你的私有模板`),e.info(` templateKey ${n}`),e.info(` version ${j}`),e.info(` (已写入 templates/${t}/${B},再次发布会更新同一个模板)`),e.info(`
5
- 出片:`),e.info(` remixmate render-video --template-id ${n} …`);const U={ok:!0,name:t,templateKey:n,version:j,dryRun:!1,created:!v};return e.result({...U}),U}finally{y&&A(y,{recursive:!0,force:!0})}}u(he,"runPublish");export{ee as evaluateCoreSurface,he as runPublish,Z as tarArgs};
1
+ var _=Object.defineProperty;var s=(r,e)=>_(r,"name",{value:e,configurable:!0});import{cpSync as F,mkdtempSync as L,readFileSync as z,rmSync as P,writeFileSync as H}from"node:fs";import q from"node:os";import d from"node:path";import{EXIT as h,SkillError as f}from"../errors.js";import{mmPost as R,resolveHttpContext as X}from"../http.js";import{emitProgress as c}from"../progress.js";import{IDENTITY_FILE as U,TAR_EXCLUDES as V}from"./constants.js";import{runCheck as G}from"./check.js";import{Output as M,readIdentity as W,readJsonFile as Y,requireProjectRoot as Z,requireTemplateDir as Q,resolveTemplateKey as J,validateTemplateName as ee,writeIdentity as re}from"./project.js";import{previewStatus as te}from"./state.js";import{runProcess as oe}from"./proc.js";import{fetchCoreSurface as ne,resolveRenderBaseUrl as ie}from"./render-client.js";function Re(r){return r.needsConfirmation===!0}s(Re,"isPublishConfirmation");function ae(r,e,t,n,i){const a=e.customPayloadSchema?.properties;return{name:r,displayName:n,templateKey:t?.templateKey??null,created:!t,currentVersion:t?t.version:null,coreVersion:typeof e.coreVersion=="string"?e.coreVersion:"",aspectRatios:Array.isArray(e.supportedAspectRatios)?e.supportedAspectRatios.map(String):[],durations:e.supportedDurations&&typeof e.supportedDurations=="object"?{min:e.supportedDurations.min,max:e.supportedDurations.max}:null,params:a&&typeof a=="object"?Object.keys(a):[],preview:{state:i.state,url:i.record?.url||null,at:i.record?.at||null}}}s(ae,"buildPublishSummary");const se={fresh:"已预览,且之后源码未改动",stale:"⚠ 预览之后源码有改动,预览已过期",missing:"⚠ 尚未做过远程预览"};function B(r,e){r.info(`模板 ${e.name}`),r.info(`名称 ${e.displayName}`),r.info(e.created?"templateKey 首次发布时生成(新建私有模板)":`templateKey ${e.templateKey}(⚠ 覆盖更新已发布的模板,当前 v${e.currentVersion??"?"})`),e.aspectRatios.length&&r.info(`画幅 ${e.aspectRatios.join(", ")}`),e.durations&&r.info(`时长 ${e.durations.min??"?"}–${e.durations.max??"?"} 秒`),r.info(`可调参数 ${e.params.length?e.params.join(", "):"(customPayloadSchema 未声明)"}`),r.info(`coreVersion ${e.coreVersion||"(未声明)"}`),r.info(`预览 ${se[e.preview.state]}${e.preview.url?`
2
+ ${e.preview.url}`:""}`)}s(B,"printSummary");function pe(r,e){const t=["-czf",e];for(const n of V)t.push(`--exclude=${n}`);return t.push("-C",r,"."),t}s(pe,"tarArgs");function le(r,e,t=()=>{}){if(!e)return"ab-render /health 未返回 coreSurface —— 该实例版本过旧,无法校验 core 契约";if(!e.available)return`ab-render 无法读取 template-core:${e.error??"未知原因"}`;if(!Array.isArray(r.coreExports))return"template.json 缺少 coreExports —— 先运行 remixmate template check 生成";const n=new Set(e.exports??[]),i=r.coreExports.filter(y=>!n.has(y)),a=typeof r.coreVersion=="string"?r.coreVersion:null,o=Array.isArray(e.versions)&&e.versions.length>0?e.versions:e.packageVersion?[e.packageVersion]:[];return i.length>0?`目标 ab-render 的 template-core 缺少导出:${i.join(", ")}。模板声明需要 core@${a??"(未声明)"},平台当前提供 ${o.length?o.map(y=>`core@${y}`).join(", "):"(未知版本)"}。请改用平台已有的导出,或等平台升级 core 后再发布。`:(a&&o.length>0&&!o.includes(a)&&t(` ⚠ 模板声明 core@${a},平台提供 ${o.join(", ")} —— 导出齐全可以渲染,但效果未必与本地完全一致`),null)}s(le,"evaluateCoreSurface");function me(r,e){return oe("tar",pe(r,e),{stdio:"pipe"}).then(t=>{if(t.code!==0)throw new f(`❌ 打包失败(tar 退出码 ${t.code}): ${t.stderr.trim().slice(0,300)}`)})}s(me,"tarball");async function ce(r,e){let t;try{t=await fetch(r,{method:"PUT",headers:{"Content-Type":"application/gzip"},body:new Uint8Array(e)})}catch(n){throw new f(`❌ 上传模板包失败(网络错误): ${n.message}`,h.BACKEND_UNREACHABLE)}if(t.status>=300)throw new f(`❌ 上传模板包失败(OSS HTTP ${t.status})`,t.status>=500?h.BACKEND_UNREACHABLE:h.ERROR)}s(ce,"putToOss");function O(r,e){const t=s(n=>/^[A-Za-z0-9._/-]+$/.test(n)?n:`'${n.replace(/'/g,"'\\''")}'`,"quote");return`remixmate template publish ${t(r)}${e?` --name ${t(e)}`:""} --yes`}s(O,"confirmCommandFor");async function Ee(r){const e=new M(!!r.json),t=ee(r.name),n=Z(r.cwd),i=Q(n,t);c({phase:"template-publish:check",progress:.05});const a=await G({name:t,cwd:n,stdio:r.json?"stderr":"inherit"});if(a!==0)throw new f(`❌ 本地校验未通过(check 退出码 ${a}),已中止发布。`,a===2?h.USAGE:h.ERROR);const o=Y(d.join(i,"template.json")),y=W(i),v=r.displayName??(typeof o.name=="string"&&o.name?o.name:t),w=typeof o.coreVersion=="string"?o.coreVersion:"",D=te(n,t,i),l=ae(t,o,y,v,D);if(!r.dryRun&&!r.yes){e.info(`即将发布为私有模板:
3
+ `),B(e,l),e.info(`
4
+ ⏸ 确认点:尚未发布。请把以上摘要展示给用户,用户确认后执行:`),e.info(` ${O(t,r.displayName)}`),l.preview.state!=="fresh"&&e.info(" (发布前需要先运行 remixmate template preview,或在用户明确同意时加 --skip-preview-check)"),c({phase:"template-publish:awaiting-confirmation",progress:.1,summary:l});const p={ok:!1,needsConfirmation:!0,exitCode:h.NEEDS_CONFIRMATION,summary:l,confirmCommand:O(t,r.displayName)};return e.result({...p}),p}if(!r.dryRun&&l.preview.state!=="fresh"&&!r.skipPreviewCheck)throw new f(l.preview.state==="missing"?`❌ 模板 ${t} 还没有远程预览过,已中止发布。
5
+ 先运行 remixmate template preview ${t},让用户确认预览后再发布。`:`❌ 模板 ${t} 在上次预览(${l.preview.at??"时间未知"})之后源码有改动,用户看到的不是将要发布的版本,已中止发布。
6
+ 重新运行 remixmate template preview ${t},让用户确认后再发布。`);const{templateKey:b,existing:E}=J(i);let j=null;r.dryRun||(j=await X("template-publish",{privateToken:r.token,apiBaseUrl:r.apiBaseUrl}));const I=ie();B(e,l),!r.dryRun&&l.preview.state!=="fresh"&&e.warn(" ⚠ 已按 --skip-preview-check 跳过预览校验"),c({phase:"template-publish:preflight",progress:.15});const A=await ne(I),C=le(o,A,p=>e.warn(p));if(C)throw new f(`❌ core 预检未通过:${C}`);if(e.info(`core 预检 通过(${A?.exports.length??0} 个导出)`),r.dryRun){e.info(`
7
+ [dry-run] 将依次调用 prepareSave → presignUpload → 上传 → save(isBuiltin=false)`),e.info(`[dry-run] 打包排除:${V.join(", ")}(保留 dsl-example.json)`);const p={ok:!0,name:t,templateKey:b,version:null,dryRun:!0,created:!E};return e.result({...p}),p}const x=j;let g;try{c({phase:"template-publish:prepare",progress:.3});const p=await R(x,"/remotionTemplate/prepareSave",{templateJson:o,name:v,templateKey:b,...w?{coreVersion:w}:{}}),m=p?.templateKey||b,S={...p?.templateJson||o,templateId:m};g=L(d.join(q.tmpdir(),"remixmate-publish-"));const $=d.join(g,"pkg");F(i,$,{recursive:!0}),P(d.join($,U),{force:!0}),H(d.join($,"template.json"),JSON.stringify(S,null,2)+`
8
+ `,"utf8"),JSON.stringify(S)!==JSON.stringify(o)&&e.warn(" ⚠ 服务端规范化后的 template.json 与本地不同;本次上传使用规范化版本,本地文件未改动。"),c({phase:"template-publish:pack",progress:.45});const K=d.join(g,`${m}.tar.gz`);await me($,K);const T=z(K);e.info(`打包 ${(T.length/1024).toFixed(0)} KB`),c({phase:"template-publish:presign",progress:.55});const u=await R(x,"/remotionTemplate/presignUpload",{templateKey:m});if(!u?.uploadUrl)throw new f("❌ presignUpload 未返回 uploadUrl");c({phase:"template-publish:upload",progress:.7}),await ce(String(u.uploadUrl),T),c({phase:"template-publish:save",progress:.85}),await R(x,"/remotionTemplate/save",{templateKey:m,name:v,isBuiltin:!1,sourceOssKey:u.sourceOssKey,version:u.version,templateJson:S,...w?{coreVersion:w}:{}});const k=typeof u.version=="number"?u.version:Number(u.version)||0;re(i,{templateKey:m,version:k}),c({phase:"template-publish:done",progress:1,templateKey:m}),e.info(`
9
+ ✅ 已发布为你的私有模板`),e.info(` templateKey ${m}`),e.info(` version ${k}`),e.info(` (已写入 templates/${t}/${U},再次发布会更新同一个模板)`),e.info(`
10
+ 出片:`),e.info(` remixmate render-video --template-id ${m} …`);const N={ok:!0,name:t,templateKey:m,version:k,dryRun:!1,created:!E};return e.result({...N}),N}finally{g&&P(g,{recursive:!0,force:!0})}}s(Ee,"runPublish");export{ae as buildPublishSummary,le as evaluateCoreSurface,Re as isPublishConfirmation,Ee as runPublish,pe as tarArgs};
@@ -0,0 +1,2 @@
1
+ var l=Object.defineProperty;var i=(t,r)=>l(t,"name",{value:r,configurable:!0});import{createHash as S}from"node:crypto";import{existsSync as d,mkdirSync as p,readdirSync as h,readFileSync as a,writeFileSync as m}from"node:fs";import c from"node:path";import{IDENTITY_FILE as y,STATE_DIR as w}from"./constants.js";const I=new Set(["node_modules","dist","__pycache__",".git"]),g=new Set([".DS_Store"]);function _(t){const r=[],n=i(s=>{for(const o of h(s,{withFileTypes:!0})){const u=c.join(s,o.name);if(o.isDirectory()){I.has(o.name)||n(u);continue}!o.isFile()||g.has(o.name)||s===t&&o.name===y||r.push(c.relative(t,u).split(c.sep).join("/"))}},"walk");n(t),r.sort();const e=S("sha256");for(const s of r){const o=a(c.join(t,s));e.update(s).update("\0").update(String(o.length)).update("\0").update(o)}return e.digest("hex")}i(_,"hashTemplateSource");function f(t,r){return c.join(t,w,"previews",`${r}.json`)}i(f,"previewRecordFile");function R(t,r,n){const e=f(t,r);p(c.dirname(e),{recursive:!0}),m(e,JSON.stringify(n,null,2)+`
2
+ `,"utf8")}i(R,"writePreviewRecord");function v(t,r){const n=f(t,r);if(!d(n))return null;try{const e=JSON.parse(a(n,"utf8"));return typeof e.sourceHash!="string"||!e.sourceHash?null:{sourceHash:e.sourceHash,taskId:String(e.taskId??""),url:String(e.url??""),compositionId:String(e.compositionId??""),durationSec:Number(e.durationSec??0),at:String(e.at??"")}}catch{return null}}i(v,"readPreviewRecord");function k(t,r,n){const e=v(t,r);return e?{state:e.sourceHash===_(n)?"fresh":"stale",record:e}:{state:"missing",record:null}}i(k,"previewStatus");export{_ as hashTemplateSource,k as previewStatus,v as readPreviewRecord,R as writePreviewRecord};
@@ -0,0 +1,4 @@
1
+ var _=Object.defineProperty;var l=(o,e)=>_(o,"name",{value:e,configurable:!0});import{existsSync as x,mkdirSync as O,mkdtempSync as R,rmSync as U}from"node:fs";import q from"node:os";import d from"node:path";import{EXIT as P,SkillError as j}from"../errors.js";import{emitProgress as b}from"../progress.js";import{usageError as y}from"./args.js";import{STILLS_DIR as B}from"./constants.js";import{Output as G,readJsonFile as T,requireProjectRoot as X,requireTemplateDir as z,validateTemplateName as J}from"./project.js";import{runProcess as k}from"./proc.js";const h=30,Z=5,v=6,L="node_modules/@remotion/cli/remotion-cli.js";function H(o,e){return`${o}-${e}`.replace(/[^A-Za-z0-9-]/g,"-")}l(H,"studioCompositionId");function K(o,e){const i=new Set;let t=o[e];for(;t?.$ref;){if(i.has(t.$ref))return{};i.add(t.$ref),t=o[t.$ref]}return t??{}}l(K,"resolveSlot");function Q(o,e,i){const t=Array.isArray(e?.scenes)?e.scenes:[],a=o.slotMapping??{},r=t.find(m=>m.purpose&&K(a,m.purpose).compositionId===i.compositionId)??t.find(m=>m.purpose===i.slot)??t[0],s=typeof r?.duration=="number"&&r.duration>0?r.duration:o.supportedDurations?.min??Z;return Math.max(1,Math.round(s*h))}l(Q,"sampleDurationFrames");function V(o,e){const i=e-1,t=o.split(",").map(r=>r.trim()).filter(Boolean);if(t.length===0)throw y("--frames 不能为空");const a=[];for(const r of t){let s;if(r==="start")s=0;else if(r==="mid")s=Math.floor(i/2);else if(r==="end")s=i;else if(/^\d+(\.\d+)?s$/.test(r))s=Math.round(Number(r.slice(0,-1))*h);else if(/^\d+$/.test(r))s=Number(r);else throw y(`--frames 取值不合法: '${r}'(可用 start / mid / end / 帧号 / 秒数如 2.5s)`);s=Math.min(Math.max(0,s),i),a.some(m=>m.frame===s)||a.push({label:r,frame:s})}if(a.length>v)throw y(`--frames 最多 ${v} 帧,收到 ${a.length} 帧`);return a}l(V,"parseFrameSpec");async function mo(o){const e=new G(!!o.json),i=J(o.name),t=X(o.cwd),a=z(t,i),r=d.join(t,L);if(!x(r))throw new j(`❌ 找不到 ${L}:工程依赖未安装。在工程根目录运行 npm install 后重试。`,P.USAGE);const s=T(d.join(a,"template.json")),m=Array.isArray(s.compositions)?s.compositions:[],f=o.compositionId?m.find(n=>n.compositionId===o.compositionId):m[0];if(!f)throw new j(o.compositionId?`❌ composition "${o.compositionId}" 不在该模板中。可选: ${m.map(n=>n.compositionId).join(", ")}`:"❌ template.json 未声明 compositions",P.USAGE);const D=d.join(a,"dsl-example.json"),N=x(D)?T(D):null,p=Q(s,N,f),$=V(o.frames??"start,mid,end",p),S=H(i,String(f.compositionId)),A=d.resolve(t,o.outDir??d.join(B,i));O(A,{recursive:!0}),e.info(`模板 ${i}`),e.info(`composition ${S}(${(p/h).toFixed(1)}s,${p} 帧)`),e.info(`截帧 ${$.map(n=>`${n.label}=${n.frame}`).join(", ")}`),e.info("首次运行会下载 headless Chrome,可能需要几分钟。");const E=l((n,w)=>new j(`❌ ${n}失败(remotion 退出码 ${w},错误详情见上方输出)。
2
+ 用 npm run dev 打开 Remotion Studio 定位问题,或先运行 remixmate template check。`),"fail"),g=R(d.join(q.tmpdir(),"remixmate-still-")),u=[];try{b({phase:"template-still:bundle",progress:0});const n=await k(process.execPath,[r,"bundle","src/index.ts","--out-dir",g,"--log=error"],{cwd:t,stdio:"stderr"});if(n.code!==0)throw E("打包工程",n.code);for(const[w,c]of $.entries()){b({phase:"template-still:render",progress:(w+1)/($.length+1),frame:c.frame});const I=d.join(A,`${i}-f${String(c.frame).padStart(5,"0")}.png`),F=[r,"still",g,S,I,`--frame=${c.frame}`,"--log=error"];o.scale!==void 0&&F.push(`--scale=${o.scale}`);const C=await k(process.execPath,F,{cwd:t,stdio:"stderr"});if(C.code!==0||!x(I))throw E(`第 ${c.frame} 帧渲染`,C.code);u.push({label:c.label,frame:c.frame,timeSec:Number((c.frame/h).toFixed(2)),path:I})}}finally{U(g,{recursive:!0,force:!0})}b({phase:"template-still:done",progress:1,stills:u.map(n=>n.path),awaitingConfirmation:!0}),e.info(`
3
+ ✅ 关键帧已生成:`);for(const n of u)e.info(` ${n.label.padEnd(6)} ${n.timeSec}s ${n.path}`);e.info(`
4
+ ⏸ 确认点:请把这些图片展示给用户确认画面效果,按反馈修改后可重新截帧;用户确认后再做远程预览。`);const M={ok:!0,name:i,compositionId:String(f.compositionId),studioCompositionId:S,durationFrames:p,stills:u,awaitingConfirmation:!0};return e.result({...M}),M}l(mo,"runStill");export{v as MAX_STILLS,V as parseFrameSpec,mo as runStill,Q as sampleDurationFrames,H as studioCompositionId};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remixmate/cli",
3
- "version": "0.9.73",
3
+ "version": "0.10.0",
4
4
  "description": "AI media generation skills for Claude Code / Codex — 12 skills covering image, video, voice, digital human, web screenshot, web recording, script, template registry, rendering, Jianying export, and video deconstruction.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -3,7 +3,8 @@ name: template-authoring
3
3
  description: |
4
4
  Create your own reusable video template locally and publish it as a private template on RemixMate.
5
5
  Scaffolds a starter project (`remixmate template init <dir>`) with Remotion Studio preview, local validation,
6
- remote draft preview and one-command publishing. Purely local until preview/publish; no login needed to start.
6
+ keyframe stills, remote draft preview and confirmed publishing,
7
+ with mandatory user checkpoints between steps. Purely local until preview/publish; no login needed to start.
7
8
  Use this skill whenever the user wants to (「模板」and「模版」are the same word):
8
9
  - 做视频模板 / 做视频模版 / 自定义视频模板 / 创建一个新的视频模版 / 新建模版 / 自己写一个模板 / 把这个画面做成模板
9
10
  - template authoring, create a video template, build a custom Remotion template
@@ -30,6 +31,28 @@ The project itself carries the authoritative guide: after `init`, read the proje
30
31
  public API and the Remotion rules. This skill only describes the workflow — do not invent template
31
32
  fields; take them from that guide and from the bundled examples.
32
33
 
34
+ ## Confirmation checkpoints (mandatory)
35
+
36
+ Do NOT run the whole workflow in one go. Stop at each ⏸ checkpoint below, show the user what
37
+ it asks for, and **end your turn**. Continue only after the user replies; apply their feedback
38
+ and repeat the same checkpoint until they approve. Skip a checkpoint only when the user
39
+ explicitly says so in this conversation (e.g. 「不用确认,直接发布」) — never on your own judgement.
40
+
41
+ | ⏸ | When | Show the user | Proceed when |
42
+ |---|---|---|---|
43
+ | A | before writing any template code | short spec: aspect ratio, duration, scene structure, visual style, adjustable parameters (`customPayload` fields) | user approves / edits the spec |
44
+ | B | after `template still` | the PNG keyframes (attach or embed the image paths) + one-line description of each | user approves the look |
45
+ | C | after `template preview` | the preview URL + what changed since the last round | user approves motion and pacing |
46
+ | D | after `template publish` without `--yes` (exit code 3) | the printed summary: name, new vs. **update of an existing template**, parameters, preview state | user explicitly confirms publishing |
47
+
48
+ How to ask: if the host has a structured question tool (e.g. Claude Code's `AskUserQuestion`),
49
+ use it with two options — 「确认,继续」 and 「需要修改」 (free-text feedback). Otherwise ask in
50
+ plain text and end the turn. Show stills inline where the host renders local images
51
+ (markdown `![](<absolute path>)`), and always list the absolute paths as well.
52
+
53
+ Exit code 3 from `publish` is the checkpoint, not a failure: nothing was uploaded. Never add
54
+ `--yes` (or `--skip-preview-check`) without the user's confirmation in this conversation.
55
+
33
56
  ## Workflow
34
57
 
35
58
  1. **Create the project** (no login). Use an absolute path outside the current repo, e.g.
@@ -45,6 +68,7 @@ fields; take them from that guide and from the bundled examples.
45
68
  2. **Open the project and read `<dir>/AGENTS.md`** before writing any code. If the session is not
46
69
  rooted in `<dir>`, read that file by absolute path (or suggest reopening the session there).
47
70
  Examples live in `examples/` (read-only — copy one into `templates/<name>/` and edit the copy).
71
+ **⏸ A** — propose the spec and wait for the user before coding.
48
72
  3. **Develop with live preview**: `npm run dev` opens Remotion Studio. The first run (and the first
49
73
  local render) downloads a headless Chrome — also needs network, may take a while.
50
74
  4. **Validate locally** (no login):
@@ -53,18 +77,32 @@ fields; take them from that guide and from the bundled examples.
53
77
  ```
54
78
  Fix every reported issue (`file` / `rule` / `message` / `fix`). `check` also refreshes the
55
79
  template's core contract fields — never hand-edit those.
56
- 5. **Remote draft preview** (login required): renders on the real platform renderer.
80
+ 5. **Keyframe stills** (local, no login, no platform cost):
81
+ ```bash
82
+ remixmate template still <name> [--frames start,mid,end] [--scale 0.5] [--json]
83
+ ```
84
+ `--frames` takes `start` / `mid` / `end` / a frame number / seconds like `2.5s` (max 6).
85
+ PNGs land in `output/stills/<name>/`; `--json` returns their absolute paths in `stills[].path`.
86
+ **⏸ B** — show the images, wait. Cheap to repeat: re-run after every visual fix.
87
+ 6. **Remote draft preview** (login required): renders on the real platform renderer.
57
88
  ```bash
58
89
  remixmate template preview <name>
59
90
  ```
60
- Draft limits: long edge ≤ 1280 px, short edge ≤ 720 px, ≤ 60 s.
61
- 6. **Publish** (login required):
91
+ Draft limits: long edge ≤ 1280 px, short edge ≤ 720 px, ≤ 60 s. A successful preview records
92
+ a fingerprint of the source; any later code change makes it stale for publishing.
93
+ **⏸ C** — show the URL, wait.
94
+ 7. **Publish** (login required) — two steps:
62
95
  ```bash
63
- remixmate template publish <name> [--name "<display name>"]
96
+ remixmate template publish <name> [--name "<display name>"] # summary only, exit 3
97
+ remixmate template publish <name> [--name "<display name>"] --yes # after the user confirms
64
98
  ```
99
+ **⏸ D** — relay the summary between the two commands. If it says the template is an update,
100
+ say so plainly: publishing overwrites the version the user already has. `--yes` is refused
101
+ when the source changed since the last preview (or was never previewed): preview again
102
+ (back to ⏸ C). `--skip-preview-check` only if the user explicitly accepts publishing unpreviewed code.
65
103
  The first publish writes `templates/<name>/.remixmate.json` with the template key; publishing
66
104
  again updates the same template. Keep that file.
67
- 7. **Render with it** — `render-video` needs a DSL source; `--template-id` alone is rejected.
105
+ 8. **Render with it** — `render-video` needs a DSL source; `--template-id` alone is rejected.
68
106
  Start from the template's own example (absolute path):
69
107
  ```bash
70
108
  remixmate render-video --dsl "$PWD/templates/<name>/dsl-example.json" --template-id <templateKey>
@@ -83,6 +121,7 @@ Then re-run `check`. Dependency changes (e.g. a newer template-core) are reporte
83
121
 
84
122
  ## Notes
85
123
 
124
+ - Exit code 3 = stopped at a confirmation checkpoint (publish without `--yes`); nothing changed.
86
125
  - Not logged in → exit code 4: run `remixmate login`. Backend unreachable → exit code 5.
87
126
  - Quota errors from publish (template count / package size / file count) are shown verbatim; relay them.
88
127
  - TraeWork only reads project-level skills: inside the project run `remixmate install --project` once.
@@ -4,7 +4,7 @@
4
4
  "tier": "tool",
5
5
  "category": "meta",
6
6
  "title": "Video Template Authoring",
7
- "description": "Start authoring a custom video template locally: scaffolds a starter project (Remotion Studio preview, local validation, remote draft preview, publishing as the user's private template) into the given directory. After this, follow the project's AGENTS.md and use `remixmate template check|preview|publish`. Local-only; requires Node >= 18 and npm.",
7
+ "description": "Start authoring a custom video template locally: scaffolds a starter project (Remotion Studio preview, local validation, remote draft preview, publishing as the user's private template) into the given directory. After this, follow the project's AGENTS.md and use `remixmate template check|still|preview|publish`, stopping for user confirmation after the spec, the keyframe stills, the preview and before publishing (`publish` needs `--yes`). Local-only; requires Node >= 18 and npm.",
8
8
  "auth": "none",
9
9
  "envVars": [
10
10
  "REMIXMATE_STARTER_TARBALL"
@@ -2,6 +2,6 @@
2
2
  "skillName": "template-authoring",
3
3
  "repoName": "agent-skill-media-maker",
4
4
  "skillId": "pending-template-authoring",
5
- "version": "V3",
6
- "skillDescription": "Local video-template authoring skill: scaffolds a RemixMate template starter project (`remixmate template init <dir>`), then guides the agent through the project's AGENTS.md, `npm run dev`, `remixmate template check`, `remixmate template preview`, `remixmate template publish`, and rendering with `remixmate render-video --template-id <templateKey>`.\n\nUse this skill whenever the user asks for any of:\n- 做视频模板 / 做视频模版 / 自定义视频模板 / 创建模板 / 创建一个新的视频模版 / 新建模版\n- template authoring, create a video template\n- preview or publish a template they develop locally"
5
+ "version": "V4",
6
+ "skillDescription": "Local video-template authoring skill: scaffolds a RemixMate template starter project (`remixmate template init <dir>`), then guides the agent through the project's AGENTS.md, `npm run dev`, `remixmate template check`, `remixmate template still` (keyframe PNGs), `remixmate template preview`, `remixmate template publish` (summary first, `--yes` after the user confirms), with mandatory user confirmation checkpoints, and rendering with `remixmate render-video --template-id <templateKey>`.\n\nUse this skill whenever the user asks for any of:\n- 做视频模板 / 做视频模版 / 自定义视频模板 / 创建模板 / 创建一个新的视频模版 / 新建模版\n- template authoring, create a video template\n- preview or publish a template they develop locally"
7
7
  }