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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ Status of every feature shipped. ✅ = implemented, ⬜ = roadmap. Section ancho
- ✅ JSON (default — pipeable to `jq`)
- ✅ `-H` / `--human` table mode (human-readable)
- ✅ `-q` / `--quiet` mode (exit code only)
- ✅ `completions bash` — generate Bash shell completions

### Quality (v0.4)
- ✅ 60+ tests `node:test` suite ([`test/`](./test/)) running against [`test/draft_content.json`](./test/draft_content.json)
Expand Down Expand Up @@ -566,6 +567,18 @@ $ capcut set-text ./project a1b2c3 "Hey everyone"
- `1:30` -- 1 minute 30 seconds
- `0:05.5` -- 5.5 seconds

### Shell completions

Generate a Bash completion script:

Install permanently:

```bash
capcut completions bash >> ~/.bashrc
```

Completes command names and global flags (`--jianying`, `-H`/`--human`, `-q`/`--quiet`, `-v`/`--version`).

## How it works

CapCut stores projects as JSON (`draft_content.json` on Windows, `draft_info.json` on macOS). This CLI reads and modifies that JSON directly. It preserves the original file's indentation style on save.
Expand Down
86 changes: 86 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,63 @@ import { formatDuration, formatTime, parseTimeInput, srtTime } from "./time.js";
import { translateDraft } from "./translate.js";
import { detectVersion } from "./version.js";

export const COMMANDS = [
"info",
"version",
"lint",
"tracks",
"segments",
"texts",
"set-text",
"shift",
"shift-all",
"speed",
"volume",
"trim",
"opacity",
"export-srt",
"materials",
"segment",
"material",
"add-audio",
"add-video",
"add-text",
"cut",
"keyframe",
"transition",
"mask",
"bg-blur",
"text-style",
"text-anim",
"image-anim",
"add-sticker",
"mix-mode",
"audio-fade",
"add-cover",
"add-filter",
"bubble-text",
"add-effect",
"save-template",
"apply-template",
"batch",
"import-srt",
"import-ass",
"text-ranges",
"caption",
"translate",
"migrate",
"add-sfx",
"chroma",
"enums",
"doctor",
"serve",
"decrypt",
"export",
"init",
] as const;

const GLOBAL_FLAGS = ["--jianying", "-H", "--human", "-q", "--quiet", "-v", "--version"] as const;

const HELP = `capcut-cli -- fast edits to CapCut projects

Usage: capcut <command> <project> [options]
Expand Down Expand Up @@ -478,6 +535,23 @@ const ENUM_FLAG_MAP: Array<{ flag: string; category: Category }> = [
{ flag: "--filters", category: "filters" },
];

function bashCompletion(): string {
const words = [...COMMANDS, ...GLOBAL_FLAGS].join(" ");

return `# bash completion for capcut

_capcut()
{
local cur
cur="\${COMP_WORDS[COMP_CWORD]}"

COMPREPLY=( $(compgen -W "${words}" -- "$cur") )
}

complete -F _capcut capcut
`;
}

function parseFlags(args: string[]): { positional: string[]; flags: Flags } {
const positional: string[] = [];
const flags: Flags = { human: false, quiet: false, batch: false };
Expand Down Expand Up @@ -1996,6 +2070,18 @@ async function main(): Promise<void> {
process.exit(0);
}
const cmd = positional[0];

if (cmd === "completions") {
const shell = positional[1];

if (shell !== "bash") {
die("Usage: capcut completions bash");
}

process.stdout.write(bashCompletion());
process.exit(0);
}

const projectPath = positional[1];

// `enums` is a pure lookup — no project needed.
Expand Down
20 changes: 20 additions & 0 deletions test/completions.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { spawnCli } from "./helpers/spawn-cli.mjs";

describe("capcut completions bash", () => {
it("prints bash completion script", () => {
const r = spawnCli(["completions", "bash"]);

assert.equal(r.status, 0);
assert.match(r.stdout, /complete -F _capcut capcut/);
assert.match(r.stdout, /info/);
assert.match(r.stdout, /tracks/);
assert.match(r.stdout, /--jianying/);
assert.match(r.stdout, /--quiet/);
assert.match(r.stdout, /--version/);
assert.match(r.stdout, /-H/);
assert.match(r.stdout, /-q/);
assert.match(r.stdout, /-v/);
});
});
Loading