@remixmate/cli 0.12.3 → 0.12.6

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 @@
1
+ var y=Object.defineProperty;var c=(i,o)=>y(i,"name",{value:o,configurable:!0});import{copyFileSync as s,mkdirSync as m,readdirSync as f,statSync as p}from"node:fs";import e from"node:path";function S(i,o){m(o,{recursive:!0});for(const r of f(i,{withFileTypes:!0})){const n=e.join(i,r.name),t=e.join(o,r.name);(r.isSymbolicLink()?p(n).isDirectory():r.isDirectory())?S(n,t):s(n,t)}}c(S,"copyDirSync");export{S as copyDirSync};
package/dist/install.js CHANGED
@@ -1,4 +1,4 @@
1
- var K=Object.defineProperty;var s=(e,r)=>K(e,"name",{value:r,configurable:!0});import{cpSync as G,existsSync as D,mkdirSync as H,readdirSync as V,readFileSync as O,rmSync as b,writeFileSync as B}from"node:fs";import v from"node:os";import c from"node:path";import{fileURLToPath as F}from"node:url";import{SKILLS_DIR as k}from"./registry.js";import{EXIT as d}from"./errors.js";import{padDisplay as j}from"./text.js";const P="install",E="uninstall",J=c.dirname(F(import.meta.url)),M=c.resolve(J,".."),S=".remixmate-install.json",W="@remixmate/cli",w=[{id:"claude",label:"Claude Code",detectDirNames:[".claude"],homeEnv:"CLAUDE_CONFIG_DIR",userDirName:".claude",projectDirName:".claude"},{id:"codex",label:"Codex",detectDirNames:[".codex"],homeEnv:"CODEX_HOME",userDirName:".codex"},{id:"workbuddy",label:"WorkBuddy",detectDirNames:[".workbuddy"],userDirName:".workbuddy",projectDirName:".workbuddy"},{id:"codebuddy",label:"CodeBuddy",detectDirNames:[".codebuddy"],userDirName:".codebuddy",projectDirName:".codebuddy"},{id:"trae",label:"Trae / TraeWork",detectDirNames:[".trae-cn",".trae"],projectDirName:".trae"}],m=w.map(e=>e.id);function u(e){return w.find(r=>r.id===e)}s(u,"spec");function X(e){const r=u(e);if(!r.userDirName)return null;const t=r.homeEnv?process.env[r.homeEnv]:void 0;return c.join(t||c.join(v.homedir(),r.userDirName),"skills")}s(X,"userSkillsDir");function I(e){const r=u(e),t=r.homeEnv?process.env[r.homeEnv]:void 0;return t?[t]:r.detectDirNames.map(n=>c.join(v.homedir(),n))}s(I,"detectDirs");function x(e){return I(e).some(r=>D(r))}s(x,"isInstalled");function N(e){const r=X(e);return r?{id:e,label:u(e).label,skillsDir:r}:null}s(N,"hostOf");function q(e){const r=u(e);return r.projectDirName?{id:e,label:`${r.label}(项目级)`,skillsDir:c.join(process.cwd(),r.projectDirName,"skills")}:null}s(q,"projectHostOf");function R(){return m.filter(x).map(N).filter(e=>e!==null)}s(R,"detectHosts");function z(){return m.filter(e=>x(e)&&!u(e).userDirName)}s(z,"projectOnlyHosts");function C(){try{const e=O(c.join(M,"package.json"),"utf-8");return JSON.parse(e).version??"unknown"}catch{return"unknown"}}s(C,"packageVersion");function Q(){return D(k)?V(k,{withFileTypes:!0}).filter(e=>e.isDirectory()&&D(c.join(k,e.name,"SKILL.md"))).map(e=>e.name).sort():[]}s(Q,"bundledSkills");function g(e){const r=c.join(e,S);if(!D(r))return null;try{return JSON.parse(O(r,"utf-8"))}catch{return null}}s(g,"readReceipt");const $=s(e=>e!==null,"nonNull");function L(e,r){const t={hosts:[],force:!1,help:!1},n=[];let f=!1,p="",o=!1;for(let a=0;a<e.length;a++){const i=e[a],l=i.indexOf("="),T=l>0?i.slice(0,l):i,_=l>0?i.slice(l+1):null,y=s(()=>_??e[++a]??"","next");switch(T){case"--host":{const h=y();if(h==="all")f=!0;else if(m.includes(h))n.push(h);else return t.error=`未知参数: --host ${h}`,t;break}case"--dir":p=y();break;case"--project":o=!0;break;case"--force":t.force=!0;break;case"-h":case"--help":return t.help=!0,t;default:return t.error=`未知参数: ${i}`,t}}if(p)return t.hosts=[{id:"custom",label:"自定义目录",skillsDir:c.resolve(p)}],t;if(o){const a=f?m.filter(l=>u(l).projectDirName):n.length>0?[...new Set(n)]:m.filter(l=>x(l)&&u(l).projectDirName),i=a.filter(l=>!u(l).projectDirName);return i.length>0?(t.error=`${i.map(l=>u(l).label).join("、")} 没有项目级 skill 目录,不能和 --project 一起用`,t):(t.hosts=a.map(q).filter($),t)}if(f)return t.hosts=m.map(N).filter($),t;if(n.length>0){const a=[...new Set(n)],i=a.filter(l=>!u(l).userDirName);if(i.length>0)return t.error=`${i.map(l=>u(l).label).join("、")} 只有项目级 skill 目录 —— 进到项目根目录后加 --project 重跑。`,t;t.hosts=a.map(N).filter($)}else t.hosts=r===E?m.map(N).filter($):R();return t}s(L,"parseArgs");function A(){process.stdout.write(`Usage: remixmate install [options]
1
+ var K=Object.defineProperty;var i=(e,r)=>K(e,"name",{value:r,configurable:!0});import{existsSync as D,mkdirSync as G,readdirSync as H,readFileSync as O,rmSync as b,writeFileSync as V}from"node:fs";import E from"node:os";import c from"node:path";import{fileURLToPath as B}from"node:url";import{SKILLS_DIR as k}from"./registry.js";import{copyDirSync as F}from"./fs-copy.js";import{EXIT as d}from"./errors.js";import{padDisplay as j}from"./text.js";const P="install",v="uninstall",J=c.dirname(B(import.meta.url)),M=c.resolve(J,".."),S=".remixmate-install.json",W="@remixmate/cli",w=[{id:"claude",label:"Claude Code",detectDirNames:[".claude"],homeEnv:"CLAUDE_CONFIG_DIR",userDirName:".claude",projectDirName:".claude"},{id:"codex",label:"Codex",detectDirNames:[".codex"],homeEnv:"CODEX_HOME",userDirName:".codex"},{id:"workbuddy",label:"WorkBuddy",detectDirNames:[".workbuddy"],userDirName:".workbuddy",projectDirName:".workbuddy"},{id:"codebuddy",label:"CodeBuddy",detectDirNames:[".codebuddy"],userDirName:".codebuddy",projectDirName:".codebuddy"},{id:"trae",label:"Trae / TraeWork",detectDirNames:[".trae-cn",".trae"],projectDirName:".trae"}],m=w.map(e=>e.id);function u(e){return w.find(r=>r.id===e)}i(u,"spec");function X(e){const r=u(e);if(!r.userDirName)return null;const t=r.homeEnv?process.env[r.homeEnv]:void 0;return c.join(t||c.join(E.homedir(),r.userDirName),"skills")}i(X,"userSkillsDir");function I(e){const r=u(e),t=r.homeEnv?process.env[r.homeEnv]:void 0;return t?[t]:r.detectDirNames.map(n=>c.join(E.homedir(),n))}i(I,"detectDirs");function x(e){return I(e).some(r=>D(r))}i(x,"isInstalled");function N(e){const r=X(e);return r?{id:e,label:u(e).label,skillsDir:r}:null}i(N,"hostOf");function q(e){const r=u(e);return r.projectDirName?{id:e,label:`${r.label}(项目级)`,skillsDir:c.join(process.cwd(),r.projectDirName,"skills")}:null}i(q,"projectHostOf");function R(){return m.filter(x).map(N).filter(e=>e!==null)}i(R,"detectHosts");function z(){return m.filter(e=>x(e)&&!u(e).userDirName)}i(z,"projectOnlyHosts");function C(){try{const e=O(c.join(M,"package.json"),"utf-8");return JSON.parse(e).version??"unknown"}catch{return"unknown"}}i(C,"packageVersion");function Q(){return D(k)?H(k,{withFileTypes:!0}).filter(e=>e.isDirectory()&&D(c.join(k,e.name,"SKILL.md"))).map(e=>e.name).sort():[]}i(Q,"bundledSkills");function g(e){const r=c.join(e,S);if(!D(r))return null;try{return JSON.parse(O(r,"utf-8"))}catch{return null}}i(g,"readReceipt");const $=i(e=>e!==null,"nonNull");function L(e,r){const t={hosts:[],force:!1,help:!1},n=[];let f=!1,p="",o=!1;for(let a=0;a<e.length;a++){const s=e[a],l=s.indexOf("="),T=l>0?s.slice(0,l):s,_=l>0?s.slice(l+1):null,y=i(()=>_??e[++a]??"","next");switch(T){case"--host":{const h=y();if(h==="all")f=!0;else if(m.includes(h))n.push(h);else return t.error=`未知参数: --host ${h}`,t;break}case"--dir":p=y();break;case"--project":o=!0;break;case"--force":t.force=!0;break;case"-h":case"--help":return t.help=!0,t;default:return t.error=`未知参数: ${s}`,t}}if(p)return t.hosts=[{id:"custom",label:"自定义目录",skillsDir:c.resolve(p)}],t;if(o){const a=f?m.filter(l=>u(l).projectDirName):n.length>0?[...new Set(n)]:m.filter(l=>x(l)&&u(l).projectDirName),s=a.filter(l=>!u(l).projectDirName);return s.length>0?(t.error=`${s.map(l=>u(l).label).join("、")} 没有项目级 skill 目录,不能和 --project 一起用`,t):(t.hosts=a.map(q).filter($),t)}if(f)return t.hosts=m.map(N).filter($),t;if(n.length>0){const a=[...new Set(n)],s=a.filter(l=>!u(l).userDirName);if(s.length>0)return t.error=`${s.map(l=>u(l).label).join("、")} 只有项目级 skill 目录 —— 进到项目根目录后加 --project 重跑。`,t;t.hosts=a.map(N).filter($)}else t.hosts=r===v?m.map(N).filter($):R();return t}i(L,"parseArgs");function A(){process.stdout.write(`Usage: remixmate install [options]
2
2
 
3
3
  把本包自带的 skill 装进已检测到的 agent 宿主,让它们能自动发现这些能力。
4
4
  重复运行即升级。支持的宿主:
@@ -10,7 +10,7 @@ Options:
10
10
  --project 装到当前目录 ./<配置目录>/skills(Codex 不支持)
11
11
  --dir <path> 自定义 skills 父目录(覆盖上面两者)
12
12
  --force 覆盖同名的、非本 CLI 安装的 skill 目录
13
- `)}s(A,"installUsage");function U(){process.stdout.write(`Usage: remixmate uninstall [options]
13
+ `)}i(A,"installUsage");function U(){process.stdout.write(`Usage: remixmate uninstall [options]
14
14
 
15
15
  按装机回执删除本 CLI 装过的 skill —— 只删回执里列出的目录,不碰其它 skill。
16
16
 
@@ -19,9 +19,9 @@ Options:
19
19
  只卸载指定宿主(默认:所有宿主都查)
20
20
  --project 卸载当前目录 ./.claude/skills 下的安装
21
21
  --dir <path> 自定义 skills 父目录
22
- `)}s(U,"uninstallUsage");function Y(e,r,t,n){const f=g(e.skillsDir),p=new Set(f?.skills??[]),o=r.filter(i=>D(c.join(e.skillsDir,i))&&!p.has(i));if(o.length>0&&!n)return`${e.skillsDir} 下已存在同名目录,且不是本 CLI 装的:${o.join("、")}
23
- 确认可以覆盖后加 --force 重跑。`;H(e.skillsDir,{recursive:!0});for(const i of p)r.includes(i)||b(c.join(e.skillsDir,i),{recursive:!0,force:!0});for(const i of r){const l=c.join(e.skillsDir,i);b(l,{recursive:!0,force:!0}),G(c.join(k,i),l,{recursive:!0})}const a={package:W,version:t,installedAt:new Date().toISOString(),source:k,skills:r};return B(c.join(e.skillsDir,S),JSON.stringify(a,null,2)+`
24
- `,"utf-8"),null}s(Y,"installInto");function ce(e){const r=L(e,P);if(r.help)return A(),d.OK;if(r.error)return process.stderr.write(`❌ ${r.error}
22
+ `)}i(U,"uninstallUsage");function Y(e,r,t,n){const f=g(e.skillsDir),p=new Set(f?.skills??[]),o=r.filter(s=>D(c.join(e.skillsDir,s))&&!p.has(s));if(o.length>0&&!n)return`${e.skillsDir} 下已存在同名目录,且不是本 CLI 装的:${o.join("、")}
23
+ 确认可以覆盖后加 --force 重跑。`;G(e.skillsDir,{recursive:!0});for(const s of p)r.includes(s)||b(c.join(e.skillsDir,s),{recursive:!0,force:!0});for(const s of r){const l=c.join(e.skillsDir,s);b(l,{recursive:!0,force:!0}),F(c.join(k,s),l)}const a={package:W,version:t,installedAt:new Date().toISOString(),source:k,skills:r};return V(c.join(e.skillsDir,S),JSON.stringify(a,null,2)+`
24
+ `,"utf-8"),null}i(Y,"installInto");function ae(e){const r=L(e,P);if(r.help)return A(),d.OK;if(r.error)return process.stderr.write(`❌ ${r.error}
25
25
  `),A(),d.USAGE;const t=Q();if(t.length===0)return process.stderr.write(`❌ 包内没有可安装的 skill(${k})
26
26
  `),d.ERROR;const n=z();if(r.hosts.length===0)return n.length>0?(process.stderr.write(`❌ 只检测到 ${n.map(o=>u(o).label).join("、")},它只有项目级 skill 目录。
27
27
  进到项目根目录后运行:remixmate install --project
@@ -34,7 +34,7 @@ Options:
34
34
  ℹ️ 还检测到 ${n.map(o=>u(o).label).join("、")},它只有项目级 skill 目录 —— 在项目根目录里运行 remixmate install --project 即可。
35
35
  `),process.stdout.write(`
36
36
  重启 agent 后即可生效。升级后重跑 remixmate install 覆盖即可。
37
- `),d.OK)}s(ce,"runInstall");function ae(e){const r=L(e,E);if(r.help)return U(),d.OK;if(r.error)return process.stderr.write(`❌ ${r.error}
37
+ `),d.OK)}i(ae,"runInstall");function ue(e){const r=L(e,v);if(r.help)return U(),d.OK;if(r.error)return process.stderr.write(`❌ ${r.error}
38
38
  `),U(),d.USAGE;let t=0;for(const n of r.hosts){const f=g(n.skillsDir);if(f){for(const p of f.skills)b(c.join(n.skillsDir,p),{recursive:!0,force:!0});b(c.join(n.skillsDir,S),{force:!0}),t+=1,process.stdout.write(`✅ ${j(n.label,16)}已移除 ${f.skills.length} 个 skill(${n.skillsDir})
39
39
  `)}}return t===0&&process.stdout.write(`没有找到本 CLI 的装机记录,无需卸载。
40
- `),d.OK}s(ae,"runUninstall");function Z(){const e=C();return R().map(r=>{const t=g(r.skillsDir);return{label:r.label,skillsDir:r.skillsDir,installedVersion:t?.version??null,currentVersion:e,drifted:t!=null&&t.version!==e}})}s(Z,"installStates");function ue(e=Z()){const r=e.filter(n=>n.installedVersion==null).map(n=>n.label),t=e.filter(n=>n.drifted).map(n=>n.label);return r.length>0?`💡 检测到 ${r.join("、")},但还没装 skill → 运行 remixmate install,重启 agent 后它才能自动发现 RemixMate 的能力`:t.length>0?`💡 ${t.join("、")} 里的 skill 是旧版本 → 运行 remixmate install 覆盖`:null}s(ue,"skillInstallHint");export{P as INSTALL_VERB,E as UNINSTALL_VERB,R as detectHosts,Z as installStates,A as installUsage,C as packageVersion,z as projectOnlyHosts,ce as runInstall,ae as runUninstall,ue as skillInstallHint,U as uninstallUsage};
40
+ `),d.OK}i(ue,"runUninstall");function Z(){const e=C();return R().map(r=>{const t=g(r.skillsDir);return{label:r.label,skillsDir:r.skillsDir,installedVersion:t?.version??null,currentVersion:e,drifted:t!=null&&t.version!==e}})}i(Z,"installStates");function fe(e=Z()){const r=e.filter(n=>n.installedVersion==null).map(n=>n.label),t=e.filter(n=>n.drifted).map(n=>n.label);return r.length>0?`💡 检测到 ${r.join("、")},但还没装 skill → 运行 remixmate install,重启 agent 后它才能自动发现 RemixMate 的能力`:t.length>0?`💡 ${t.join("、")} 里的 skill 是旧版本 → 运行 remixmate install 覆盖`:null}i(fe,"skillInstallHint");export{P as INSTALL_VERB,v as UNINSTALL_VERB,R as detectHosts,Z as installStates,A as installUsage,C as packageVersion,z as projectOnlyHosts,ae as runInstall,ue as runUninstall,fe as skillInstallHint,U as uninstallUsage};
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.12.3",
4
- "generatedAt": "2026-10-12T14:16:45.020Z",
3
+ "version": "0.12.6",
4
+ "generatedAt": "2026-10-18T13:29:08.016Z",
5
5
  "skills": [
6
6
  {
7
7
  "id": "doc-parse",
@@ -1,10 +1,10 @@
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(`即将发布为私有模板:
1
+ var _=Object.defineProperty;var s=(r,e)=>_(r,"name",{value:e,configurable:!0});import{mkdtempSync as F,readFileSync as L,rmSync as P,writeFileSync as z}from"node:fs";import H from"node:os";import d from"node:path";import{EXIT as h,SkillError as f}from"../errors.js";import{copyDirSync as q}from"../fs-copy.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 Ee(r){return r.needsConfirmation===!0}s(Ee,"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 D(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(D,"confirmCommandFor");async function je(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:"",O=te(n,t,i),l=ae(t,o,y,v,O);if(!r.dryRun&&!r.yes){e.info(`即将发布为私有模板:
3
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} 还没有远程预览过,已中止发布。
4
+ ⏸ 确认点:尚未发布。请把以上摘要展示给用户,用户确认后执行:`),e.info(` ${D(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:D(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
5
  先运行 remixmate template preview ${t},让用户确认预览后再发布。`:`❌ 模板 ${t} 在上次预览(${l.preview.at??"时间未知"})之后源码有改动,用户看到的不是将要发布的版本,已中止发布。
6
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(`
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=F(d.join(H.tmpdir(),"remixmate-publish-"));const $=d.join(g,"pkg");q(i,$),P(d.join($,U),{force:!0}),z(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=L(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
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};
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(je,"runPublish");export{ae as buildPublishSummary,le as evaluateCoreSurface,Ee as isPublishConfirmation,je as runPublish,pe as tarArgs};
@@ -1,4 +1,4 @@
1
- var V=Object.defineProperty;var d=(e,r)=>V(e,"name",{value:r,configurable:!0});import{cpSync as B,existsSync as g,mkdirSync as F,mkdtempSync as H,readFileSync as L,renameSync as x,rmSync as p,writeFileSync as N}from"node:fs";import P from"node:os";import i from"node:path";import{EXIT as T,SkillError as l}from"../errors.js";import{emitProgress as $}from"../progress.js";import{RULES_DIR as u,RULES_VERSION_FILE as _,STARTER_VERSION as U}from"./constants.js";import{applyDocs as C,describeDoc as O,readDocHashes as J,writeDocHashes as W}from"./docs.js";import{obtainStarterTarball as q}from"./init.js";import{Output as D,compareVersions as z,readRulesVersion as A,requireProjectRoot as K}from"./project.js";import{runProcess as Q}from"./proc.js";function X(e,r){const o={...e.devDependencies,...e.dependencies},c={...r.devDependencies,...r.dependencies};return Object.entries(c).filter(([t,s])=>o[t]!==s).map(([t,s])=>({name:t,project:o[t]??null,starter:s})).sort((t,s)=>t.name.localeCompare(s.name))}d(X,"diffDependencies");function E(e){try{return JSON.parse(L(e,"utf8"))}catch{return{}}}d(E,"readPkg");class G extends D{static{d(this,"QuietOutput")}constructor(){super(!0)}info(){}}async function ae(e={}){const r=new D(!!e.json||!!e.auto),o=K(e.cwd),c=A(o),t=U,s=d(a=>(e.auto||r.result({...a}),a),"finish");if(c&&z(c,t)>=0&&!e.force)return e.auto||r.info(`✅ 本地校验规则已是最新(starter ${c},当前 cli 对应 ${t})。`),s({ok:!0,from:c,to:t,upgraded:!1,dryRun:!!e.dryRun,docs:[],dependencyHints:[]});const y=H(i.join(P.tmpdir(),"remixmate-upgrade-"));try{$({phase:"template-upgrade:fetch",progress:.2});const{file:a,label:j}=await q(y,e.auto?new G:r),f=i.join(y,"starter");F(f,{recursive:!0});const h=await Q("tar",["-xzf",a,"-C",f,"--strip-components=1"],{stdio:"pipe"});if(h.code!==0)throw new l(`❌ 解压 starter 失败(tar 退出码 ${h.code}): ${h.stderr.trim().slice(0,300)}`);const w=i.join(f,u);if(!g(i.join(w,"check","index.mjs")))throw new l(`❌ ${j} 里没有 ${u}/check/index.mjs,不是有效的 starter 包`);const k=X(E(i.join(o,"package.json")),E(i.join(f,"package.json"))),b=J(o);if(e.auto?r.info(`↻ 已自动更新本地校验规则与工作流(.remixmate/):starter ${c??"(未知版本)"} → ${t},templates/ 未改动。`):(r.info(`升级本地校验规则:starter ${c??"(未知版本)"} → ${t}(${j})`),r.info(` 替换 ${u}/(check 脚本、schema、安全策略、WORKFLOW.md)`),r.info(" 不改动 templates/、src/、examples/、package.json")),!e.dryRun){$({phase:"template-upgrade:apply",progress:.6});const n=i.join(o,u),m=i.join(o,`${u}.upgrading`),R=i.join(o,`${u}.previous`);p(m,{recursive:!0,force:!0}),p(R,{recursive:!0,force:!0}),B(w,m,{recursive:!0});const v=i.join(m,i.relative(u,_));g(v)||N(v,JSON.stringify({starterVersion:t},null,2)+`
2
- `,"utf8"),g(n)&&x(n,R),x(m,n),p(R,{recursive:!0,force:!0})}const{outcomes:S,hashes:I}=C(o,f,b,{force:e.withDocs,dryRun:e.dryRun});e.dryRun||W(o,I);for(const n of S)n.action==="kept-modified"?r.warn(` ⚠ ${O(n)}`):e.auto&&n.action==="unchanged"||r.info(` ${O(n)}`);for(const n of k)r.warn(` ⚠ 新 starter 钉的 ${n.name}@${n.starter},你的工程是 ${n.project??"(未安装)"}。需要时手动执行 npm i ${n.name}@${n.starter}(升级 core 可能影响模板效果,先 check + preview 再发布)。`);return $({phase:"template-upgrade:done",progress:1}),e.auto||r.info(e.dryRun?`
1
+ var V=Object.defineProperty;var m=(e,r)=>V(e,"name",{value:r,configurable:!0});import{existsSync as g,mkdirSync as B,mkdtempSync as F,readFileSync as H,renameSync as v,rmSync as p,writeFileSync as L}from"node:fs";import N from"node:os";import i from"node:path";import{EXIT as P,SkillError as l}from"../errors.js";import{copyDirSync as T}from"../fs-copy.js";import{emitProgress as $}from"../progress.js";import{RULES_DIR as u,RULES_VERSION_FILE as _,STARTER_VERSION as U}from"./constants.js";import{applyDocs as C,describeDoc as D,readDocHashes as J,writeDocHashes as W}from"./docs.js";import{obtainStarterTarball as q}from"./init.js";import{Output as O,compareVersions as z,readRulesVersion as A,requireProjectRoot as K}from"./project.js";import{runProcess as Q}from"./proc.js";function X(e,r){const o={...e.devDependencies,...e.dependencies},c={...r.devDependencies,...r.dependencies};return Object.entries(c).filter(([t,a])=>o[t]!==a).map(([t,a])=>({name:t,project:o[t]??null,starter:a})).sort((t,a)=>t.name.localeCompare(a.name))}m(X,"diffDependencies");function E(e){try{return JSON.parse(H(e,"utf8"))}catch{return{}}}m(E,"readPkg");class G extends O{static{m(this,"QuietOutput")}constructor(){super(!0)}info(){}}async function ue(e={}){const r=new O(!!e.json||!!e.auto),o=K(e.cwd),c=A(o),t=U,a=m(s=>(e.auto||r.result({...s}),s),"finish");if(c&&z(c,t)>=0&&!e.force)return e.auto||r.info(`✅ 本地校验规则已是最新(starter ${c},当前 cli 对应 ${t})。`),a({ok:!0,from:c,to:t,upgraded:!1,dryRun:!!e.dryRun,docs:[],dependencyHints:[]});const y=F(i.join(N.tmpdir(),"remixmate-upgrade-"));try{$({phase:"template-upgrade:fetch",progress:.2});const{file:s,label:j}=await q(y,e.auto?new G:r),d=i.join(y,"starter");B(d,{recursive:!0});const h=await Q("tar",["-xzf",s,"-C",d,"--strip-components=1"],{stdio:"pipe"});if(h.code!==0)throw new l(`❌ 解压 starter 失败(tar 退出码 ${h.code}): ${h.stderr.trim().slice(0,300)}`);const w=i.join(d,u);if(!g(i.join(w,"check","index.mjs")))throw new l(`❌ ${j} 里没有 ${u}/check/index.mjs,不是有效的 starter 包`);const k=X(E(i.join(o,"package.json")),E(i.join(d,"package.json"))),b=J(o);if(e.auto?r.info(`↻ 已自动更新本地校验规则与工作流(.remixmate/):starter ${c??"(未知版本)"} → ${t},templates/ 未改动。`):(r.info(`升级本地校验规则:starter ${c??"(未知版本)"} → ${t}(${j})`),r.info(` 替换 ${u}/(check 脚本、schema、安全策略、WORKFLOW.md)`),r.info(" 不改动 templates/、src/、examples/、package.json")),!e.dryRun){$({phase:"template-upgrade:apply",progress:.6});const n=i.join(o,u),f=i.join(o,`${u}.upgrading`),R=i.join(o,`${u}.previous`);p(f,{recursive:!0,force:!0}),p(R,{recursive:!0,force:!0}),T(w,f);const x=i.join(f,i.relative(u,_));g(x)||L(x,JSON.stringify({starterVersion:t},null,2)+`
2
+ `,"utf8"),g(n)&&v(n,R),v(f,n),p(R,{recursive:!0,force:!0})}const{outcomes:S,hashes:I}=C(o,d,b,{force:e.withDocs,dryRun:e.dryRun});e.dryRun||W(o,I);for(const n of S)n.action==="kept-modified"?r.warn(` ⚠ ${D(n)}`):e.auto&&n.action==="unchanged"||r.info(` ${D(n)}`);for(const n of k)r.warn(` ⚠ 新 starter 钉的 ${n.name}@${n.starter},你的工程是 ${n.project??"(未安装)"}。需要时手动执行 npm i ${n.name}@${n.starter}(升级 core 可能影响模板效果,先 check + preview 再发布)。`);return $({phase:"template-upgrade:done",progress:1}),e.auto||r.info(e.dryRun?`
3
3
  [dry-run] 未写入任何文件。`:`
4
- ✅ 已更新。接着运行 remixmate template check 看新规则下是否还有问题。`),s({ok:!0,from:c,to:t,upgraded:!e.dryRun,dryRun:!!e.dryRun,docs:S,dependencyHints:k})}catch(a){throw a instanceof l?a:new l(`❌ 升级失败: ${a.message}`,T.ERROR)}finally{p(y,{recursive:!0,force:!0})}}d(ae,"runUpgrade");export{X as diffDependencies,ae as runUpgrade};
4
+ ✅ 已更新。接着运行 remixmate template check 看新规则下是否还有问题。`),a({ok:!0,from:c,to:t,upgraded:!e.dryRun,dryRun:!!e.dryRun,docs:S,dependencyHints:k})}catch(s){throw s instanceof l?s:new l(`❌ 升级失败: ${s.message}`,P.ERROR)}finally{p(y,{recursive:!0,force:!0})}}m(ue,"runUpgrade");export{X as diffDependencies,ue as runUpgrade};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remixmate/cli",
3
- "version": "0.12.3",
3
+ "version": "0.12.6",
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",
@@ -22,6 +22,7 @@
22
22
 
23
23
 
24
24
 
25
+
25
26
 
26
27
 
27
28
  from __future__ import annotations
@@ -161,6 +162,46 @@ def start_render(
161
162
  return str(task_id)
162
163
 
163
164
 
165
+ def cancel_path_for(status_path: str) -> str:
166
+
167
+ if status_path.endswith("Status"):
168
+ return status_path[: -len("Status")] + "Cancel"
169
+ return "/renderCancel"
170
+
171
+
172
+ def cancel_render(
173
+ task_id: str,
174
+ *,
175
+ private_token: str,
176
+ base_url: Optional[str] = None,
177
+ path: str = "/renderCancel",
178
+ reason: str = "user_interrupt",
179
+ timeout: float = 1.5,
180
+ ) -> bool:
181
+
182
+
183
+
184
+
185
+
186
+
187
+ try:
188
+ result = _request(
189
+ "POST",
190
+ path,
191
+ private_token=private_token,
192
+ payload={"taskId": task_id, "reason": reason},
193
+ base_url=base_url,
194
+ timeout=timeout,
195
+ )
196
+ _check_code(result)
197
+ return True
198
+ except BaseException as exc:
199
+ if isinstance(exc, (KeyboardInterrupt, SystemExit)):
200
+ raise
201
+ print(f" ⚠️ cancel {path} failed (taskId={task_id}): {exc}", file=sys.stderr, flush=True)
202
+ return False
203
+
204
+
164
205
  def poll_render(
165
206
  task_id: str,
166
207
  *,
@@ -29,6 +29,7 @@ import builtins
29
29
  import json
30
30
  import os
31
31
  import re
32
+ import signal
32
33
  import subprocess
33
34
  import sys
34
35
  import tempfile
@@ -2276,14 +2277,31 @@ def render_with_remote_api(
2276
2277
  _persist_plan_checkpoint(render_plan, job_id, private_token)
2277
2278
 
2278
2279
  last_progress = -1.0
2280
+ last_status: Optional[str] = None
2281
+ last_stage: Optional[str] = None
2282
+ last_emit_at = 0.0
2279
2283
 
2280
2284
  render_start_time = float(render_plan.get("remoteStartedAt") or time.time())
2281
2285
 
2282
2286
  def _on_progress(status_data: dict):
2283
- nonlocal last_progress
2287
+ nonlocal last_progress, last_status, last_stage, last_emit_at
2284
2288
  progress = float(status_data.get("progress") or 0.0)
2289
+ status = status_data.get("status")
2285
2290
 
2286
- if progress - last_progress >= 0.02 or progress >= 1.0:
2291
+ stage = status_data.get("stage")
2292
+ stage_elapsed_ms = status_data.get("stageElapsedMs")
2293
+ now = time.monotonic()
2294
+
2295
+
2296
+
2297
+
2298
+ if (
2299
+ progress - last_progress >= 0.02
2300
+ or progress >= 1.0
2301
+ or status != last_status
2302
+ or stage != last_stage
2303
+ or now - last_emit_at >= 15.0
2304
+ ):
2287
2305
  pct = int(progress * 100)
2288
2306
  elapsed = time.time() - render_start_time
2289
2307
  eta_remaining = 0.0
@@ -2297,18 +2315,45 @@ def render_with_remote_api(
2297
2315
  LogPrint(f" 🎞️ Remote render progress: {pct}%", file=sys.stderr)
2298
2316
  sys.stderr.flush()
2299
2317
 
2300
- print(
2301
- json.dumps({
2302
- "__progress__": True,
2303
- "phase": "render-remote",
2304
- "progress": round(progress, 4),
2305
- "etaSeconds": int(eta_remaining) if eta_remaining > 0 else None,
2306
- "elapsedSeconds": int(elapsed),
2307
- "taskId": task_id,
2308
- }, ensure_ascii=False),
2309
- flush=True,
2310
- )
2318
+ event = {
2319
+ "__progress__": True,
2320
+ "phase": "render-remote",
2321
+ "progress": round(progress, 4),
2322
+ "etaSeconds": int(eta_remaining) if eta_remaining > 0 else None,
2323
+ "elapsedSeconds": int(elapsed),
2324
+ "taskId": task_id,
2325
+ "status": status,
2326
+ }
2327
+ if stage is not None:
2328
+ event["stage"] = stage
2329
+ if stage_elapsed_ms is not None:
2330
+ event["stageElapsedMs"] = stage_elapsed_ms
2331
+ print(json.dumps(event, ensure_ascii=False), flush=True)
2311
2332
  last_progress = progress
2333
+ last_status = status
2334
+ last_stage = stage
2335
+ last_emit_at = now
2336
+
2337
+
2338
+
2339
+
2340
+
2341
+
2342
+
2343
+
2344
+ cancel_path = remote_renderer_client.cancel_path_for(status_path)
2345
+ cancelling = False
2346
+
2347
+ def _on_interrupt(signum, _frame):
2348
+ nonlocal cancelling
2349
+ if cancelling:
2350
+ return
2351
+ cancelling = True
2352
+ LogPrint(f"🛑 interrupted, cancelling remote task: taskId={task_id}", file=sys.stderr)
2353
+ remote_renderer_client.cancel_render(task_id, private_token=private_token, path=cancel_path)
2354
+ sys.exit(130)
2355
+
2356
+ prev_sigint = signal.signal(signal.SIGINT, _on_interrupt)
2312
2357
 
2313
2358
  try:
2314
2359
  result = remote_renderer_client.poll_render(
@@ -2352,8 +2397,11 @@ def render_with_remote_api(
2352
2397
  "timestamp": now_iso(),
2353
2398
  })
2354
2399
  return False
2400
+ finally:
2401
+
2402
+ signal.signal(signal.SIGINT, prev_sigint)
2355
2403
 
2356
- file_url = (result or {}).get("fileUrl") or (result or {}).get("videoUrl")
2404
+ file_url =(result or {}).get("fileUrl") or (result or {}).get("videoUrl")
2357
2405
  if not file_url:
2358
2406
  LogPrint(f"❌ remote render finished but did not return a fileUrl: {result}", file=sys.stderr)
2359
2407
  render_plan["status"] = "failed"
@@ -365,6 +365,11 @@ def _allowed_statuses() -> set[str]:
365
365
 
366
366
 
367
367
 
368
+
369
+
370
+
371
+
372
+
368
373
  allowed = {"stable"}
369
374
  if _truthy_env(_BETA_GATE_ENV):
370
375
  allowed.add("beta")