From e050802b080605883a9e4185bc1d2f518f806b45 Mon Sep 17 00:00:00 2001 From: Rene Zander Date: Wed, 3 Jun 2026 14:07:22 +0000 Subject: [PATCH] feat: global --dry-run and restore command (#15, #16) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #15 — every draft-mutating command now honors --dry-run. Gated in saveDraft (the single write choke point), so all ~35 write commands are covered at once: the on-disk write and the .bak snapshot are skipped, while out() stamps the JSON result with "dryRun":true so callers can tell a preview from a commit. translate / export --batch keep their own dry-run paths. #16 — `capcut restore ` copies .bak back over the draft to undo the last write. Single-step (one backup generation kept); dies with a clear message when no .bak exists; honors --dry-run. Adds test/dry-run.test.mjs and test/restore.test.mjs (5 tests). README + CHANGELOG updated. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 5 +++++ README.md | 12 +++++++++-- src/draft.ts | 18 +++++++++++++++++ src/index.ts | 41 +++++++++++++++++++++++++++++++++++--- test/dry-run.test.mjs | 35 ++++++++++++++++++++++++++++++++ test/restore.test.mjs | 46 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 152 insertions(+), 5 deletions(-) create mode 100644 test/dry-run.test.mjs create mode 100644 test/restore.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 30cafcd..983789b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to capcut-cli are documented here. The format follows [Keep ## [Unreleased] +### Added + +- **Global `--dry-run`** ([#15](https://github.com/renezander030/capcut-cli/issues/15)) — any draft-mutating command now honors `--dry-run`: it computes and prints the normal JSON result with `"dryRun":true` added, but leaves the draft **and** its `.bak` untouched. Gated centrally in `saveDraft`, so it covers every write command at once. `translate` / `export --batch` keep their existing dry-run behavior. +- **`restore` command** ([#16](https://github.com/renezander030/capcut-cli/issues/16)) — `capcut restore ` undoes the last write by copying `.bak` back over the draft. Single-step (only one backup generation is kept); exits non-zero with a clear message when no `.bak` exists. Honors `--dry-run`. + ### Documentation - **README** — added a from-source install path and a consolidated Prerequisites note (Node ≥ 18, whisper for `caption`, `ANTHROPIC_API_KEY` for `translate`); a worked-example block for the v0.4/v0.5 commands that had none (`mix-mode`, `audio-fade`, `add-filter`, `bubble-text`, `add-cover`, `add-sfx`, `chroma`, `import-ass`); and a **Troubleshooting** table covering the CapCut-must-be-closed footgun, track-order normalization ([#21](https://github.com/renezander030/capcut-cli/issues/21)), `.bak` recovery, whisper/API-key setup, and the `--fade-out` flag. diff --git a/README.md b/README.md index 6ecdbae..51690cd 100644 --- a/README.md +++ b/README.md @@ -330,6 +330,13 @@ a1b2c3d4 0:00.50- 0:03.00 Welcome to the video capcut set-text ./project a1b2c3 "New text" -q && echo "done" ``` +**Dry run** (`--dry-run`) -- preview any mutating command. It prints the normal JSON result with `"dryRun":true` added, but leaves the draft **and** its `.bak` untouched. Preview, then commit: +```bash +capcut speed ./project a1b2c3 2.0 --dry-run +# {"ok":true,"id":"a1b2c3...","old_speed":1,"new_speed":2,"dryRun":true} ← nothing written +capcut speed ./project a1b2c3 2.0 # run for real +``` + ## Commands ### Overview (start here) @@ -391,7 +398,7 @@ Options: `--font-size `, `--color `, `--align <0|1|2>` (left/center/righ ### Edit -Every write command creates a `.bak` backup before modifying the file. +Every write command creates a `.bak` backup before modifying the file. Add `--dry-run` to any of them to preview without writing; `capcut restore` rolls the last write back. ```bash capcut set-text ./project a1b2c3 "New subtitle" @@ -403,6 +410,7 @@ capcut speed ./project a1b2c3 1.5 capcut volume ./project a1b2c3 0.8 capcut opacity ./project a1b2c3 0.5 capcut trim ./project a1b2c3 2s 5s +capcut restore ./project # undo the last write (single-step, from .bak) ``` ### Templates @@ -656,7 +664,7 @@ Close the project in CapCut before editing, reopen after. CapCut reads the JSON |---|---| | **Edits vanish / project looks unchanged** | CapCut was open. It keeps its own copy of the draft in memory and overwrites your file when it next saves. **Close the project in CapCut, run the CLI, then reopen.** This is the single most common gotcha. | | **Track / layer order looks scrambled in CapCut** | Older builds wrote tracks in command-call order, but CapCut lays out the timeline from the tracks-array order. Recent builds normalize the array to the canonical layer order (video → audio → overlays → text) on every save. Update, re-run the edit, reopen. ([#21](https://github.com/renezander030/capcut-cli/issues/21)) | -| **Need to undo an edit** | Every write leaves a `.bak` beside the draft. Roll back with `mv draft_content.json.bak draft_content.json`. | +| **Need to undo an edit** | Run `capcut restore ` — it copies the `.bak` back over the draft. Single-step (only the last write is kept). Preview any command first with `--dry-run` to avoid the round-trip. | | **`caption` fails: whisper not found** | `caption` shells out to a whisper binary. Install one (`pip install openai-whisper`, `brew install whisper-cpp`, or faster-whisper) or pass `--whisper-cmd `. | | **`translate` fails: ANTHROPIC_API_KEY** | Set the env var (`export ANTHROPIC_API_KEY=…`) or pass `--api-key`. | | **`audio-fade --out` seems ignored** | `--out` is the global output-path flag. Use `--fade-out` for the fade-out duration. | diff --git a/src/draft.ts b/src/draft.ts index abef8fc..af43a66 100644 --- a/src/draft.ts +++ b/src/draft.ts @@ -134,7 +134,25 @@ export function sortTracks(draft: Draft): void { .map(({ track }) => track); } +// --dry-run state. When set, saveDraft computes the in-memory change but skips +// both the on-disk write and the .bak snapshot, so the draft is left untouched. +// All ~35 mutating commands funnel through saveDraft, so gating here covers them. +let dryRun = false; + +export function setDryRun(value: boolean): void { + dryRun = value; +} + +export function isDryRun(): boolean { + return dryRun; +} + export function saveDraft(filePath: string, draft: Draft): void { + if (dryRun) { + // Normalize in memory (so any read-back is consistent) but write nothing. + sortTracks(draft); + return; + } const bakPath = `${filePath}.bak`; if (existsSync(filePath)) { const original = rawOriginal ?? readFileSync(filePath, "utf-8"); diff --git a/src/index.ts b/src/index.ts index 143f93d..7834939 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; +import { copyFileSync, existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { parseAss } from "./ass.js"; @@ -37,13 +37,16 @@ import { type DoctorCheck, runDoctor } from "./doctor.js"; import type { Draft, Segment, Track } from "./draft.js"; import { extractText, + findDraft, findMaterial, findMaterialGlobal, findSegment, getMaterialTypes, getTracksByType, + isDryRun, loadDraft, saveDraft, + setDryRun, updateTextContent, } from "./draft.js"; import { type Category, listEnum, type Namespace } from "./enums.js"; @@ -128,13 +131,14 @@ export const COMMANDS = [ "chroma", "enums", "doctor", + "restore", "serve", "decrypt", "export", "init", ] as const; -const GLOBAL_FLAGS = ["--jianying", "-H", "--human", "-q", "--quiet", "-v", "--version"] as const; +const GLOBAL_FLAGS = ["--jianying", "-H", "--human", "-q", "--quiet", "-v", "--version", "--dry-run"] as const; const HELP = `capcut-cli -- fast edits to CapCut projects @@ -146,6 +150,8 @@ Global flags: -H, --human Human-readable table output (default: JSON) -v, --version Print the installed CLI version -q, --quiet No output on success, exit code only (write commands) + --dry-run Preview a mutating command: print the result (with + "dryRun":true) but leave the draft and its .bak untouched --jianying Use JianYing enum namespace (default: CapCut) for transition, mask, text-anim, image-anim, add-effect, enums @@ -212,6 +218,7 @@ Edit: opacity Set opacity (0.0-1.0) export-srt Export subtitles to SRT batch Run multiple edits from stdin (JSONL) + restore Undo the last write (restore from .bak, single-step) Animate: keyframe