Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ All notable changes to capcut-cli are documented here. The format follows [Keep

## [Unreleased]

## [0.21.1] — 2026-08-30

### Fixed

- **`refused [editor-open]` on every write under `npx` / `npm exec`, with CapCut closed
([#99](https://github.com/renezander030/capcut-cli/issues/99))** — `editorProcesses()` substring-matched the
joined process listing, and npm rewrites its own process title to the full command line — which contains
`capcut-cli` — so the CLI detected itself as a running editor and the README's zero-install path could never
complete a write. Process names are now compared per process, by exact basename, case-insensitively (macOS
`ps -o comm=` prints the executable's full bundle path, so whole-line equality would have broken real
detection). The Windows branch had the same substring shape and now compares the exact `tasklist` image name,
which also stops an image merely *containing* `capcut.exe` from tripping the guard. Pinned by the reporter's
fixture: a listing whose only `capcut` is an `npm exec capcut-cli@0.21.0 …` title returns nothing, while the
full CapCut bundle path still detects. Diagnosis, scope table and fix direction all came with the report —
thanks [@hansuk94](https://github.com/hansuk94).

## [0.21.0] — 2026-08-28

One thread runs through most of this release: the issue-[#50](https://github.com/renezander030/capcut-cli/issues/50)
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ JSON in, JSON out: every command reads and writes the local draft store directly

## Release notes

> **New in v0.21.0:** the CapCut Mac 9.2.8 nested-Timelines report ([#50](https://github.com/renezander030/capcut-cli/issues/50)) gets its repair path — `sync-timelines --nested` copies the root timeline into the `Timelines/<id>/` documents as an explicit opt-in, each document keeping its own GUID (the workaround verified in the thread), and `fixture --check` mechanically verifies a bundle leaks no home path, email or device id before you attach it. Every write refusal now names its gate (`refused [editor-open]` / `[version-boundary]` / `[draft-changed-on-disk]`), so a pasted stderr line is unambiguous. Plus `catalogue <query>` — name → resource_id across every bundled and harvested table, `lint --pip` for the PIP + local-mask workflow ([#78](https://github.com/renezander030/capcut-cli/issues/78)), `import-srt --clone-style` (keep the draft's caption look without hunting a segment id), `relink --stage` (the repaired draft leaves portable), an observe-only `media-unregistered` note (pyCapCut#13), and Chinese docs for version support and encryption. No command was removed and no existing flag changed meaning. Full details in the [changelog](./CHANGELOG.md).
> **New in v0.21.1:** one fix, and if you run the CLI via `npx` it is the whole release: the `refused [editor-open]` guard was detecting the CLI's *own* npm process — npm rewrites its process title to the full command line, which contains `capcut-cli`, and the guard substring-matched the joined process table — so every write on the documented zero-install path (`npx capcut-cli …`) was refused even with CapCut closed ([#99](https://github.com/renezander030/capcut-cli/issues/99), reported by [@hansuk94](https://github.com/hansuk94) with the diagnosis and the fix direction included). Editor detection now compares exact process names, per process, on macOS, Linux and Windows — a really-running CapCut/JianYing still refuses, npm never does. Full details in the [changelog](./CHANGELOG.md).

> **New in v0.20.0:** subtitles now carry their styling across the draft boundary in both directions — `export-ass` writes `[V4+ Styles]`, per-range override tags and `--karaoke` word timing, and `import-ass` keeps inline bold/italic/colour/size spans instead of flattening them to plain text, pinned by a round-trip test. The raw-recording pipeline closes: `detect-silence` finds dead air (with a `--pad` so no word gets clipped mid-syllable) and `tts` voices a script through any local TTS tool (piper, `say`, espeak-ng) straight onto an audio track. `render` finishes 0.19.0's fail-fast work — the audio filter chain is probed like the video chain ([#91](https://github.com/renezander030/capcut-cli/issues/91)) — and `--encoder` unlocks hardware encoders. Plus `harvest-enums --sync`/`--add` for the whole library, and `diagnose` now captures sanitized evidence for the open store-layout questions ([#50](https://github.com/renezander030/capcut-cli/issues/50)). No command was removed and no existing flag changed meaning. Full details in the [changelog](./CHANGELOG.md).
> **New in v0.21.0:** the CapCut Mac 9.2.8 nested-Timelines report ([#50](https://github.com/renezander030/capcut-cli/issues/50)) gets its repair path — `sync-timelines --nested` copies the root timeline into the `Timelines/<id>/` documents as an explicit opt-in, each document keeping its own GUID (the workaround verified in the thread), and `fixture --check` mechanically verifies a bundle leaks no home path, email or device id before you attach it. Every write refusal now names its gate (`refused [editor-open]` / `[version-boundary]` / `[draft-changed-on-disk]`), so a pasted stderr line is unambiguous. Plus `catalogue <query>` — name → resource_id across every bundled and harvested table, `lint --pip` for the PIP + local-mask workflow ([#78](https://github.com/renezander030/capcut-cli/issues/78)), `import-srt --clone-style` (keep the draft's caption look without hunting a segment id), `relink --stage` (the repaired draft leaves portable), an observe-only `media-unregistered` note (pyCapCut#13), and Chinese docs for version support and encryption. No command was removed and no existing flag changed meaning. Full details in the [changelog](./CHANGELOG.md).

## Commands

Expand Down
4 changes: 2 additions & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ JSON 进、JSON 出:每个命令都直接读写本地草稿存储,不用 MCP

## 发布说明

> **v0.21.0 新增:** issue [#50](https://github.com/renezander030/capcut-cli/issues/50) 中 CapCut Mac 9.2.8 嵌套 Timelines 布局的报告有了修复路径——`sync-timelines --nested` 以显式选择的方式把根时间线复制进 `Timelines/<id>/` 文档(每个文档保留自己的 GUID,即讨论串中验证过的解决办法),`fixture --check` 会在你附上诊断包之前机械化地检查是否残留家目录路径、邮箱或设备 ID。每次写入拒绝现在都会标明触发的守卫(`refused [editor-open]` / `[version-boundary]` / `[draft-changed-on-disk]`),粘贴的 stderr 不再有歧义。另有 `catalogue <query>`(按名称在全部内置与采集目录中查 resource_id)、`lint --pip`(画中画+蒙版工作流校验,[#78](https://github.com/renezander030/capcut-cli/issues/78))、`import-srt --clone-style`(无需先查段 ID 即沿用草稿现有字幕样式)、`relink --stage`(修复后的草稿可随文件夹迁移)、只观察不写入的 `media-unregistered` 提示(pyCapCut#13),以及 version-support 与 jianying-encryption 的中文文档。没有删除任何命令,现有参数含义均未改变。详见[更新日志](./CHANGELOG.md)。
> **v0.21.1 新增:** 只有一个修复,但如果你通过 `npx` 使用 CLI,它就是这个版本的全部:`refused [editor-open]` 守卫会把 CLI *自己的* npm 进程识别为正在运行的编辑器——npm 会把自身进程标题改写为完整命令行(其中含有 `capcut-cli`),而守卫对拼接后的整张进程表做子串匹配——因此在文档记载的免安装路径(`npx capcut-cli …`)上,即使 CapCut 已关闭,所有写入也一律被拒绝([#99](https://github.com/renezander030/capcut-cli/issues/99),由 [@hansuk94](https://github.com/hansuk94) 报告,诊断与修复方向随报告一并给出)。编辑器检测现在在 macOS、Linux 与 Windows 上都按进程逐个精确比较进程名——真正运行中的 CapCut/JianYing 仍会拒绝写入,npm 进程不再触发。详见[更新日志](./CHANGELOG.md)。

> **v0.20.0 新增:** 字幕样式现在可以双向跨越草稿边界——`export-ass` 写出 `[V4+ Styles]`、逐区间覆盖标签与 `--karaoke` 逐词计时,`import-ass` 则保留内联的粗体/斜体/颜色/字号区间,不再压平为纯文本,并有往返测试兜底。原始录音的处理链路就此闭环:`detect-silence` 找出静音段(`--pad` 留出余量,不会把词切在半个音节上),`tts` 可通过任意本地 TTS 工具(piper、`say`、espeak-ng)把文稿直接配音到音频轨上。`render` 补完了 0.19.0 的快速失败工作——音频滤镜链与视频链一样接受预检([#91](https://github.com/renezander030/capcut-cli/issues/91))——并新增 `--encoder` 以启用硬件编码器。另有 `harvest-enums --sync`/`--add` 支持整库扫描与手工登记,`diagnose` 现在能为悬而未决的存储布局问题采集脱敏证据([#50](https://github.com/renezander030/capcut-cli/issues/50))。没有删除任何命令,现有参数含义均未改变。详见[更新日志](./CHANGELOG.md)。
> **v0.21.0 新增:** issue [#50](https://github.com/renezander030/capcut-cli/issues/50) 中 CapCut Mac 9.2.8 嵌套 Timelines 布局的报告有了修复路径——`sync-timelines --nested` 以显式选择的方式把根时间线复制进 `Timelines/<id>/` 文档(每个文档保留自己的 GUID,即讨论串中验证过的解决办法),`fixture --check` 会在你附上诊断包之前机械化地检查是否残留家目录路径、邮箱或设备 ID。每次写入拒绝现在都会标明触发的守卫(`refused [editor-open]` / `[version-boundary]` / `[draft-changed-on-disk]`),粘贴的 stderr 不再有歧义。另有 `catalogue <query>`(按名称在全部内置与采集目录中查 resource_id)、`lint --pip`(画中画+蒙版工作流校验,[#78](https://github.com/renezander030/capcut-cli/issues/78))、`import-srt --clone-style`(无需先查段 ID 即沿用草稿现有字幕样式)、`relink --stage`(修复后的草稿可随文件夹迁移)、只观察不写入的 `media-unregistered` 提示(pyCapCut#13),以及 version-support 与 jianying-encryption 的中文文档。没有删除任何命令,现有参数含义均未改变。详见[更新日志](./CHANGELOG.md)。

## 常用命令

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "capcut-cli",
"version": "0.21.0",
"version": "0.21.1",
"description": "Independent, unofficial CLI to create and edit CapCut projects — build drafts from scratch, add video/audio/text, subtitles, timing, speed, volume, templates, cut long-form to shorts. No API needed. Not affiliated with ByteDance.",
"type": "module",
"bin": {
Expand Down
27 changes: 23 additions & 4 deletions src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -549,17 +549,36 @@ export function editorProcesses(): string[] {
try {
if (platform() === "win32") {
const result = spawnSync("tasklist", ["/FO", "CSV", "/NH"], { encoding: "utf-8", timeout: 3000 });
const output = result.stdout ?? "";
return ["CapCut.exe", "JianyingPro.exe"].filter((name) => output.toLowerCase().includes(name.toLowerCase()));
return matchEditorProcesses(result.stdout ?? "", "win32");
}
const result = spawnSync("ps", ["-axo", "comm="], { encoding: "utf-8", timeout: 3000 });
const output = result.stdout ?? "";
return ["CapCut", "JianyingPro"].filter((name) => output.toLowerCase().includes(name.toLowerCase()));
return matchEditorProcesses(result.stdout ?? "", "posix");
} catch {
return [];
}
}

/**
* Match a raw process listing against the editor executables, one process per
* line — never as a substring of the joined listing: npm rewrites its own
* process title to the full command line (`npm exec capcut-cli@0.21.0 …`), so
* a substring test detects the CLI itself and refuses every write under npx
* (#99). The comparison is the exact basename, not the whole line, because
* macOS `ps -o comm=` prints the executable's full bundle path; on Windows the
* image name is the first quoted field of a `tasklist /FO CSV` row.
*/
export function matchEditorProcesses(listing: string, flavor: "win32" | "posix"): string[] {
const names = flavor === "win32" ? ["CapCut.exe", "JianyingPro.exe"] : ["CapCut", "JianyingPro"];
const seen = new Set<string>();
for (const raw of listing.split("\n")) {
const line = raw.trim();
if (!line) continue;
const executable = flavor === "win32" ? (line.match(/^"([^"]*)"/)?.[1] ?? line) : (line.split("/").pop() ?? line);
seen.add(executable.toLowerCase());
}
return names.filter((name) => seen.has(name.toLowerCase()));
}

export function isManagedDraftPath(path: string): boolean {
return path.replace(/\\/g, "/").toLowerCase().includes("/com.lveditor.draft/");
}
Expand Down
45 changes: 45 additions & 0 deletions test/editor-processes.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { matchEditorProcesses } from "../dist/store.js";

// #99: the editor-open guard must compare process names, not search the joined
// listing — npm's rewritten process title contains `capcut-cli`, so a substring
// test detects the CLI's own npm parent and refuses every write under npx.
describe("editorProcesses matching", () => {
it("ignores npm's rewritten process title under npx / npm exec (#99)", () => {
const listing = [
"npm exec capcut-cli@0.21.0 sync-timelines /Users/me/Movies/CapCut/User Data/Projects/com.lveditor.draft/proj --nested --apply",
"node",
"/usr/lib/systemd/systemd",
].join("\n");
assert.deepEqual(matchEditorProcesses(listing, "posix"), []);
});

it("still detects CapCut by its full macOS bundle path", () => {
const listing = ["/Applications/CapCut.app/Contents/MacOS/CapCut", "node"].join("\n");
assert.deepEqual(matchEditorProcesses(listing, "posix"), ["CapCut"]);
});

it("matches bare comm names case-insensitively, both editors, stable order", () => {
assert.deepEqual(matchEditorProcesses("jianyingpro\nnode\ncapcut\n", "posix"), ["CapCut", "JianyingPro"]);
});

it("does not match a name that merely contains an editor's name", () => {
const listing = ["/opt/tools/MyCapCut", "/home/me/bin/capcut-sync", "CapCut Helper (Renderer)"].join("\n");
assert.deepEqual(matchEditorProcesses(listing, "posix"), []);
});

it("compares the tasklist image name, not the whole CSV row, on win32", () => {
const clear = ['"MyCapCut.exe","1044","Console","1","12,345 K"', '"node.exe","2088","Console","1","45,678 K"'].join(
"\r\n",
);
assert.deepEqual(matchEditorProcesses(clear, "win32"), []);
const running = '"CapCut.exe","1044","Console","1","12,345 K"\r\n"node.exe","2088","Console","1","45,678 K"';
assert.deepEqual(matchEditorProcesses(running, "win32"), ["CapCut.exe"]);
});

it("returns nothing for an empty or whitespace-only listing", () => {
assert.deepEqual(matchEditorProcesses("", "posix"), []);
assert.deepEqual(matchEditorProcesses("\n \n", "win32"), []);
});
});