zerocut-cli 0.1.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.
Files changed (63) hide show
  1. package/.eslintrc.js +11 -0
  2. package/.prettierignore +3 -0
  3. package/.prettierrc.json +6 -0
  4. package/README.md +205 -0
  5. package/dist/bin/zerocut.d.ts +2 -0
  6. package/dist/bin/zerocut.js +5 -0
  7. package/dist/cli.d.ts +3 -0
  8. package/dist/cli.js +24 -0
  9. package/dist/commands/config.d.ts +4 -0
  10. package/dist/commands/config.js +129 -0
  11. package/dist/commands/ffmpeg.d.ts +4 -0
  12. package/dist/commands/ffmpeg.js +32 -0
  13. package/dist/commands/foo.d.ts +4 -0
  14. package/dist/commands/foo.js +14 -0
  15. package/dist/commands/help.d.ts +4 -0
  16. package/dist/commands/help.js +14 -0
  17. package/dist/commands/image.d.ts +4 -0
  18. package/dist/commands/image.js +149 -0
  19. package/dist/commands/music.d.ts +4 -0
  20. package/dist/commands/music.js +74 -0
  21. package/dist/commands/pandoc.d.ts +4 -0
  22. package/dist/commands/pandoc.js +32 -0
  23. package/dist/commands/skill.d.ts +4 -0
  24. package/dist/commands/skill.js +24 -0
  25. package/dist/commands/tts.d.ts +4 -0
  26. package/dist/commands/tts.js +74 -0
  27. package/dist/commands/video.d.ts +4 -0
  28. package/dist/commands/video.js +166 -0
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.js +6 -0
  31. package/dist/services/cerevox.d.ts +34 -0
  32. package/dist/services/cerevox.js +256 -0
  33. package/dist/services/commandLoader.d.ts +3 -0
  34. package/dist/services/commandLoader.js +80 -0
  35. package/dist/services/config.d.ts +15 -0
  36. package/dist/services/config.js +235 -0
  37. package/dist/skill/SKILL.md +133 -0
  38. package/dist/types/command.d.ts +6 -0
  39. package/dist/types/command.js +2 -0
  40. package/dist/utils/progress.d.ts +1 -0
  41. package/dist/utils/progress.js +13 -0
  42. package/eslint.config.js +30 -0
  43. package/package.json +52 -0
  44. package/scripts/copy-skill-md.cjs +8 -0
  45. package/src/bin/zerocut.ts +3 -0
  46. package/src/cli.ts +25 -0
  47. package/src/commands/config.ts +130 -0
  48. package/src/commands/ffmpeg.ts +37 -0
  49. package/src/commands/help.ts +13 -0
  50. package/src/commands/image.ts +194 -0
  51. package/src/commands/music.ts +78 -0
  52. package/src/commands/pandoc.ts +37 -0
  53. package/src/commands/skill.ts +20 -0
  54. package/src/commands/tts.ts +80 -0
  55. package/src/commands/video.ts +202 -0
  56. package/src/index.ts +1 -0
  57. package/src/services/cerevox.ts +296 -0
  58. package/src/services/commandLoader.ts +42 -0
  59. package/src/services/config.ts +230 -0
  60. package/src/skill/SKILL.md +209 -0
  61. package/src/types/command.ts +7 -0
  62. package/src/utils/progress.ts +10 -0
  63. package/tsconfig.json +16 -0
@@ -0,0 +1,133 @@
1
+ ---
2
+ name: "zerocut-cli-tools"
3
+ description: "Use ZeroCut CLI media and document tools. Invoke when user needs generate media, run ffmpeg/pandoc, sync resources, or save outputs."
4
+ ---
5
+
6
+ # ZeroCut CLI Tools
7
+
8
+ ## Purpose
9
+
10
+ This skill provides a single reference for using ZeroCut CLI commands:
11
+ - image generation
12
+ - video generation
13
+ - music generation
14
+ - text-to-speech
15
+ - ffmpeg sandbox execution
16
+ - pandoc sandbox execution
17
+
18
+ ## When To Invoke
19
+
20
+ Invoke this skill when the user asks to:
21
+ - generate image, video, music, or speech audio
22
+ - run ffmpeg or ffprobe command in sandbox
23
+ - run pandoc conversion in sandbox
24
+ - sync local/remote resources into sandbox
25
+ - save generated results to local output files
26
+
27
+ ## Command Reference
28
+
29
+ ### image
30
+
31
+ Default action: `create`
32
+
33
+ ```bash
34
+ zerocut image --prompt "a cat on a bike" --output out.png
35
+ zerocut image create --prompt "a cat on a bike" --model seedream-5l --aspectRatio 1:1 --resolution 1K --refs ref1.png,ref2.jpg --output out.png
36
+ ```
37
+
38
+ Options:
39
+ - `--prompt <prompt>` required
40
+ - `--model <model>`
41
+ - `--aspectRatio <ratio>`
42
+ - `--resolution <resolution>`
43
+ - `--refs <refs>` comma-separated local paths or URLs
44
+ - `--output <file>` save generated file
45
+
46
+ ### video
47
+
48
+ Default action: `create`
49
+
50
+ ```bash
51
+ zerocut video --prompt "city night drive" --video vidu --duration 8 --output out.mp4
52
+ zerocut video create --prompt "city night drive" --video vidu --aspectRatio 1:1 --refs ref1.png,ref2.png --output out.mp4
53
+ ```
54
+
55
+ Options:
56
+ - `--prompt <prompt>` required
57
+ - `--video <model>`
58
+ - `--duration <seconds>`
59
+ - `--seed <seed>`
60
+ - `--firstFrame <image>`
61
+ - `--lastFrame <image>`
62
+ - `--refs <assets>`
63
+ - `--resolution <resolution>`
64
+ - `--aspectRatio <ratio>`
65
+ - `--withAudio`
66
+ - `--optimizeCameraMotion`
67
+ - `--output <file>`
68
+
69
+ ### music
70
+
71
+ Default action: `create`
72
+
73
+ ```bash
74
+ zerocut music --prompt "lofi beat" --output music.mp3
75
+ zerocut music create --prompt "lofi beat" --output music.mp3
76
+ ```
77
+
78
+ Options:
79
+ - `--prompt <prompt>` required
80
+ - `--output <file>`
81
+
82
+ ### tts
83
+
84
+ Default action: `create`
85
+
86
+ ```bash
87
+ zerocut tts --text "你好,欢迎使用 ZeroCut" --voiceId voice_xxx --output speech.mp3
88
+ zerocut tts create --prompt "calm tone" --text "Hello world" --voiceId voice_xxx --output speech.mp3
89
+ ```
90
+
91
+ Options:
92
+ - `--prompt <prompt>`
93
+ - `--text <text>` required
94
+ - `--voiceId <voiceId>`
95
+ - `--output <file>`
96
+
97
+ ### ffmpeg
98
+
99
+ ```bash
100
+ zerocut ffmpeg --args -i input.mp4 -vn output.mp3 --resources input.mp4
101
+ zerocut ffmpeg --args -i input.mp4 -vf scale=1280:720 output.mp4 --resources input.mp4
102
+ ```
103
+
104
+ Options:
105
+ - `--args <args...>` required, arguments appended after `ffmpeg`
106
+ - `--resources <resources...>` optional, files/URLs to sync into sandbox materials
107
+
108
+ Behavior:
109
+ - command is validated to only allow `ffmpeg` or `ffprobe`
110
+ - for `ffmpeg`, `-y` is auto-injected when absent
111
+ - output file is auto-downloaded from sandbox to local current directory
112
+
113
+ ### pandoc
114
+
115
+ ```bash
116
+ zerocut pandoc --args input.md -o output.pdf --resources input.md
117
+ zerocut pandoc --args input.md --output=output.docx --resources input.md template.docx
118
+ ```
119
+
120
+ Options:
121
+ - `--args <args...>` required, arguments appended after `pandoc`
122
+ - `--resources <resources...>` optional, files/URLs to sync into sandbox materials
123
+
124
+ Behavior:
125
+ - command is validated to only allow `pandoc`
126
+ - output file must be specified in args with `-o`, `--output`, or `--output=...`
127
+ - output file is auto-downloaded from sandbox to local current directory
128
+
129
+ ## Output And Sync Rules
130
+
131
+ - Media URLs from generation are synced to TOS when available.
132
+ - `--output` saves files to an absolute path resolved from current working directory.
133
+ - Missing parent directories for `--output` are created automatically.
@@ -0,0 +1,6 @@
1
+ import type { Command } from "commander";
2
+ export interface CommandModule {
3
+ name: string;
4
+ description?: string;
5
+ register: (program: Command) => void;
6
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1 @@
1
+ export declare function createProgressSpinner(label?: string): () => void;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createProgressSpinner = createProgressSpinner;
4
+ function createProgressSpinner(label = "inferencing") {
5
+ const frames = ["|", "/", "-", "\\"];
6
+ let si = 0;
7
+ const t0 = Date.now();
8
+ return () => {
9
+ const f = frames[si++ % frames.length];
10
+ const sec = Math.floor((Date.now() - t0) / 1000);
11
+ process.stdout.write(`\r${f} ${label}... ${sec}s`);
12
+ };
13
+ }
@@ -0,0 +1,30 @@
1
+ const tsParser = require("@typescript-eslint/parser");
2
+ const tsPlugin = require("@typescript-eslint/eslint-plugin");
3
+ const prettierPlugin = require("eslint-plugin-prettier");
4
+
5
+ module.exports = [
6
+ {
7
+ ignores: ["dist", "node_modules"],
8
+ },
9
+ {
10
+ files: ["**/*.ts"],
11
+ languageOptions: {
12
+ parser: tsParser,
13
+ parserOptions: {
14
+ ecmaVersion: 2020,
15
+ sourceType: "commonjs",
16
+ project: "./tsconfig.json",
17
+ },
18
+ },
19
+ plugins: {
20
+ "@typescript-eslint": tsPlugin,
21
+ prettier: prettierPlugin,
22
+ },
23
+ rules: {
24
+ ...tsPlugin.configs.recommended.rules,
25
+ "@typescript-eslint/no-unused-vars": "warn",
26
+ "@typescript-eslint/no-explicit-any": "warn",
27
+ "prettier/prettier": "error",
28
+ },
29
+ },
30
+ ];
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "zerocut-cli",
3
+ "version": "0.1.0",
4
+ "description": "ZeroCut CLI: AI assistant CLI for creating and editing images/audio/video",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "bin": {
8
+ "zerocut": "dist/bin/zerocut.js",
9
+ "zerocut-cli": "dist/bin/zerocut.js"
10
+ },
11
+ "keywords": [
12
+ "cli",
13
+ "zerocut"
14
+ ],
15
+ "author": "liubei-ai",
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+ssh://git@github.com/liubei-ai/zerocut-cli.git"
20
+ },
21
+ "homepage": "https://github.com/liubei-ai/zerocut-cli#readme",
22
+ "bugs": {
23
+ "url": "https://github.com/liubei-ai/zerocut-cli/issues"
24
+ },
25
+ "engines": {
26
+ "node": ">=18"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^25.2.0",
30
+ "@typescript-eslint/eslint-plugin": "^8.54.0",
31
+ "@typescript-eslint/parser": "^8.54.0",
32
+ "eslint": "^9.39.2",
33
+ "eslint-config-prettier": "^10.1.8",
34
+ "eslint-plugin-prettier": "^5.5.5",
35
+ "prettier": "^3.8.1",
36
+ "ts-node": "^10.9.2",
37
+ "typescript": "^5.9.3",
38
+ "typescript-eslint": "^8.54.0"
39
+ },
40
+ "dependencies": {
41
+ "cerevox": "^4.22.0",
42
+ "commander": "^14.0.3",
43
+ "inquirer": "^13.2.2"
44
+ },
45
+ "scripts": {
46
+ "build": "tsc && node scripts/copy-skill-md.cjs",
47
+ "typecheck": "tsc --noEmit",
48
+ "lint": "eslint . --ext .ts",
49
+ "format": "prettier --write .",
50
+ "dev": "ts-node src/bin/zerocut.ts"
51
+ }
52
+ }
@@ -0,0 +1,8 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+
4
+ const src = path.resolve(__dirname, "../src/skill/SKILL.md");
5
+ const dest = path.resolve(__dirname, "../dist/skill/SKILL.md");
6
+
7
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
8
+ fs.copyFileSync(src, dest);
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { run } from "../cli";
3
+ run(process.argv);
package/src/cli.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { Command } from "commander";
2
+ import { loadBuiltInCommands, loadExternalCommandsAsync } from "./services/commandLoader";
3
+ import { applyConfigInterceptor } from "./services/config";
4
+
5
+ export function createProgram(): Command {
6
+ const program = new Command();
7
+ program.name("zerocut").description("Zerocut CLI");
8
+
9
+ loadBuiltInCommands(program);
10
+ applyConfigInterceptor(program);
11
+ return program;
12
+ }
13
+
14
+ export async function run(argv: string[]): Promise<void> {
15
+ const args = argv.slice(2);
16
+ const program = createProgram();
17
+ await loadExternalCommandsAsync(program);
18
+
19
+ if (args.length === 0) {
20
+ await program.parseAsync([argv[0] ?? "node", argv[1] ?? "zerocut", "help"]);
21
+ return;
22
+ }
23
+
24
+ await program.parseAsync(argv);
25
+ }
@@ -0,0 +1,130 @@
1
+ import type { Command } from "commander";
2
+ import readline from "node:readline";
3
+ import { readConfigSync, setConfigValueSync } from "../services/config";
4
+
5
+ export const name = "config";
6
+ export const description = "Configuration management";
7
+
8
+ async function ask(question: string, defaults?: string): Promise<string> {
9
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
10
+ const q = defaults ? `${question} [${defaults}]: ` : `${question}: `;
11
+ const answer = await new Promise<string>((resolve) => rl.question(q, (ans) => resolve(ans)));
12
+ rl.close();
13
+ const trimmed = answer.trim();
14
+ return trimmed.length > 0 ? trimmed : (defaults ?? "");
15
+ }
16
+
17
+ export function register(program: Command): void {
18
+ const parent = program
19
+ .command("config")
20
+ .description("Configuration management: set key, projectDir, and region")
21
+ .option("--ott <token>", "One-Time Token (OTT) for fetching API key")
22
+ .option("--region <region>", "Region for OTT exchange: cn|us")
23
+ .action(async function (this: Command, opts: { ott?: string; region?: string }) {
24
+ const ott = typeof opts.ott === "string" ? opts.ott.trim() : "";
25
+ const region = typeof opts.region === "string" ? opts.region.trim().toLowerCase() : "";
26
+ if (!ott) return; // no quick params; fall through to subcommands normally
27
+ if (region !== "cn" && region !== "us") {
28
+ process.stderr.write("Invalid or missing --region. Allowed: cn|us\n");
29
+ process.exitCode = 1;
30
+ return;
31
+ }
32
+ try {
33
+ const base = region === "cn" ? "https://api2.zerocut.cn" : "https://api2.zerocut.art";
34
+ const resp = await fetch(`${base}/api/open/ott/exchange`, {
35
+ method: "POST",
36
+ headers: { "content-type": "application/json" },
37
+ body: JSON.stringify({ ott }),
38
+ });
39
+ if (!resp.ok) {
40
+ process.stderr.write(`OTT exchange failed: HTTP ${resp.status}\n`);
41
+ process.exitCode = 1;
42
+ return;
43
+ }
44
+ const json = (await resp.json()) as { data?: { apiKey?: string } };
45
+ const apiKey = json?.data?.apiKey;
46
+ if (typeof apiKey !== "string" || apiKey.length === 0) {
47
+ process.stderr.write("OTT exchange failed: missing data.apiKey in response\n");
48
+ process.exitCode = 1;
49
+ return;
50
+ }
51
+ setConfigValueSync("apiKey", apiKey);
52
+ setConfigValueSync("region", region);
53
+ process.stdout.write("apiKey set via OTT\n");
54
+ } catch (err) {
55
+ process.stderr.write(`OTT exchange failed: ${(err as Error).message}\n`);
56
+ process.exitCode = 1;
57
+ }
58
+ });
59
+
60
+ parent
61
+ .command("key [key]")
62
+ .description(
63
+ "Set API key. If omitted, exchange via One-Time Token (OTT) after choosing region."
64
+ )
65
+ .action(async (key?: string) => {
66
+ const direct = typeof key === "string" ? key.trim() : "";
67
+ if (direct.length > 0) {
68
+ setConfigValueSync("apiKey", direct);
69
+ process.stdout.write("apiKey set\n");
70
+ return;
71
+ }
72
+ const region = (await ask("Choose region (cn/us)", "us")).trim().toLowerCase();
73
+ if (region !== "cn" && region !== "us") {
74
+ process.stderr.write("Invalid region. Allowed: cn|us\n");
75
+ process.exitCode = 1;
76
+ return;
77
+ }
78
+ const ott = (await ask("Enter One-Time Token (OTT)")).trim();
79
+ if (!ott) {
80
+ process.stderr.write("OTT is required when no apiKey is provided\n");
81
+ process.exitCode = 1;
82
+ return;
83
+ }
84
+ try {
85
+ const base = region === "cn" ? "https://api2.zerocut.cn" : "https://api2.zerocut.art";
86
+ const resp = await fetch(`${base}/api/open/ott/exchange`, {
87
+ method: "POST",
88
+ headers: {
89
+ "content-type": "application/json",
90
+ },
91
+ body: JSON.stringify({ ott }),
92
+ });
93
+ if (!resp.ok) {
94
+ process.stderr.write(`OTT exchange failed: HTTP ${resp.status}\n`);
95
+ process.exitCode = 1;
96
+ return;
97
+ }
98
+ const json = (await resp.json()) as {
99
+ data?: { apiKey?: string };
100
+ [k: string]: unknown;
101
+ };
102
+ const apiKey = json?.data?.apiKey;
103
+ if (typeof apiKey !== "string" || apiKey.length === 0) {
104
+ process.stderr.write("OTT exchange failed: missing data.apiKey in response\n");
105
+ process.exitCode = 1;
106
+ return;
107
+ }
108
+ setConfigValueSync("apiKey", apiKey);
109
+ setConfigValueSync("region", region);
110
+ process.stdout.write("apiKey set via OTT\n");
111
+ } catch (err) {
112
+ process.stderr.write(`OTT exchange failed: ${(err as Error).message}\n`);
113
+ process.exitCode = 1;
114
+ }
115
+ });
116
+
117
+ parent
118
+ .command("list")
119
+ .description("List current configuration values")
120
+ .action(() => {
121
+ const cfg = readConfigSync() as Record<string, unknown>;
122
+ const masked = { ...cfg } as Record<string, unknown>;
123
+ const k = masked.apiKey;
124
+ if (typeof k === "string" && k.length > 0) {
125
+ const visible = k.slice(-4);
126
+ masked.apiKey = `${"*".repeat(Math.max(0, k.length - 4))}${visible}`;
127
+ }
128
+ process.stdout.write(`${JSON.stringify(masked, null, 2)}\n`);
129
+ });
130
+ }
@@ -0,0 +1,37 @@
1
+ import type { Command } from "commander";
2
+ import { getSessionFromCommand, runFFMpegCommand } from "../services/cerevox";
3
+
4
+ export const name = "ffmpeg";
5
+ export const description = "Run ffmpeg in sandbox";
6
+
7
+ export function register(program: Command): void {
8
+ program
9
+ .command("ffmpeg")
10
+ .description("Run ffmpeg command in sandbox")
11
+ .allowUnknownOption(true)
12
+ .option("--args <args...>", "Arguments passed to ffmpeg")
13
+ .option("--resources <resources...>", "Resource files/urls to sync into sandbox")
14
+ .action(async function (
15
+ this: Command,
16
+ opts: {
17
+ args?: string[];
18
+ resources?: string[];
19
+ }
20
+ ) {
21
+ const session = getSessionFromCommand(this as unknown as Record<symbol, unknown>);
22
+ if (!session) {
23
+ process.stderr.write("No active session\n");
24
+ return;
25
+ }
26
+ const args = Array.isArray(opts.args) ? opts.args : [];
27
+ if (args.length === 0) {
28
+ process.stderr.write("Missing required option: --args\n");
29
+ process.exitCode = 1;
30
+ return;
31
+ }
32
+ const command = `ffmpeg ${args.join(" ")}`;
33
+ const resources = Array.isArray(opts.resources) ? opts.resources : [];
34
+ const res = await runFFMpegCommand(session, command, resources);
35
+ console.log(res);
36
+ });
37
+ }
@@ -0,0 +1,13 @@
1
+ import type { Command } from "commander";
2
+
3
+ export const name = "help";
4
+ export const description = "Show help";
5
+
6
+ export function register(program: Command): void {
7
+ program
8
+ .command("help")
9
+ .description("Show help")
10
+ .action(() => {
11
+ program.outputHelp();
12
+ });
13
+ }
@@ -0,0 +1,194 @@
1
+ import type { Command } from "commander";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { getMaterialUri, getSessionFromCommand, syncToTOS } from "../services/cerevox";
5
+ import type { Session } from "cerevox";
6
+ import { createProgressSpinner } from "../utils/progress";
7
+
8
+ export const name = "image";
9
+ export const description = "Image command: create image";
10
+
11
+ export function register(program: Command): void {
12
+ const parent = program.command("image").description("Create a new image; requires --prompt");
13
+
14
+ const allowedModels = [
15
+ "seedream",
16
+ "seedream-pro",
17
+ "seedream-5l",
18
+ "banana",
19
+ "banana2",
20
+ "banana-pro",
21
+ "wan",
22
+ ] as const;
23
+ type AllowedModel = (typeof allowedModels)[number];
24
+ const allowedAspectRatios = [
25
+ "1:1",
26
+ "3:4",
27
+ "4:3",
28
+ "16:9",
29
+ "9:16",
30
+ "2:3",
31
+ "3:2",
32
+ "21:9",
33
+ "1:4",
34
+ "4:1",
35
+ "1:8",
36
+ "8:1",
37
+ ] as const;
38
+ type AllowedAspectRatio = (typeof allowedAspectRatios)[number];
39
+ const allowedResolutions = ["1K", "2K", "4K"] as const;
40
+ type AllowedResolution = (typeof allowedResolutions)[number];
41
+
42
+ async function performImageGeneration(
43
+ session: Session,
44
+ {
45
+ prompt,
46
+ model,
47
+ aspectRatio,
48
+ resolution,
49
+ refsList,
50
+ output,
51
+ }: {
52
+ prompt: string;
53
+ model?: AllowedModel;
54
+ aspectRatio?: AllowedAspectRatio;
55
+ resolution?: AllowedResolution;
56
+ refsList: string[];
57
+ output?: string;
58
+ }
59
+ ): Promise<void> {
60
+ const referenceImages = await Promise.all(
61
+ refsList.map(async (ref) => ({ url: await getMaterialUri(session, ref) }))
62
+ );
63
+ const onProgress = createProgressSpinner("inferencing");
64
+ const payload: Record<string, unknown> = {
65
+ model: model || "seedream-5l",
66
+ prompt,
67
+ aspect_ratio: aspectRatio,
68
+ resolution,
69
+ reference_images: referenceImages,
70
+ onProgress,
71
+ };
72
+ const res = await (session.ai.generateImage as (arg: Record<string, unknown>) => Promise<any>)(
73
+ payload
74
+ );
75
+ if (res?.urls && Array.isArray(res.urls) && res.urls.length > 0) {
76
+ try {
77
+ const tosUrl = await syncToTOS(res.urls[0]);
78
+ if (tosUrl) {
79
+ res.url = tosUrl;
80
+ res.urls[0] = tosUrl;
81
+ }
82
+ } catch {}
83
+ }
84
+ process.stdout.write("\n");
85
+ if (output) {
86
+ const dir = process.cwd();
87
+ const url = (res.url as string) ?? res.urls[0];
88
+ const response = await fetch(url);
89
+ const buffer = Buffer.from(await response.arrayBuffer());
90
+ const filePath = path.resolve(dir, output);
91
+ if (!fs.existsSync(path.dirname(filePath))) {
92
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
93
+ }
94
+ fs.writeFileSync(filePath, buffer);
95
+ res.output = filePath;
96
+ }
97
+ console.log(res);
98
+ }
99
+
100
+ async function imageCreateAction(
101
+ this: Command,
102
+ opts: {
103
+ prompt?: string;
104
+ model?: string;
105
+ aspectRatio?: string;
106
+ resolution?: string;
107
+ refs?: string;
108
+ output?: string;
109
+ }
110
+ ) {
111
+ const session = getSessionFromCommand(this as unknown as Record<symbol, unknown>);
112
+ if (!session) {
113
+ process.stderr.write("No active session\n");
114
+ return;
115
+ }
116
+ const prompt = typeof opts.prompt === "string" ? opts.prompt : undefined;
117
+ if (!prompt || prompt.trim().length === 0) {
118
+ process.stderr.write("Missing required option: --prompt\n");
119
+ process.exitCode = 1;
120
+ return;
121
+ }
122
+ const model = typeof opts.model === "string" ? opts.model.trim() : undefined;
123
+ if (model && !(allowedModels as readonly string[]).includes(model)) {
124
+ process.stderr.write(
125
+ `Invalid value for --model: ${model}. Allowed: ${allowedModels.join("|")}\n`
126
+ );
127
+ process.exitCode = 1;
128
+ return;
129
+ }
130
+ const modelArg: AllowedModel | undefined = (model ?? undefined) as AllowedModel | undefined;
131
+ const aspectRatio =
132
+ typeof opts.aspectRatio === "string"
133
+ ? (opts.aspectRatio.trim() as AllowedAspectRatio)
134
+ : undefined;
135
+ if (aspectRatio && !(allowedAspectRatios as readonly string[]).includes(aspectRatio)) {
136
+ process.stderr.write(
137
+ `Invalid value for --aspectRatio: ${aspectRatio}. Allowed: ${allowedAspectRatios.join("|")}\n`
138
+ );
139
+ process.exitCode = 1;
140
+ return;
141
+ }
142
+ const resolution =
143
+ typeof opts.resolution === "string"
144
+ ? (opts.resolution.trim() as AllowedResolution)
145
+ : undefined;
146
+ if (resolution && !(allowedResolutions as readonly string[]).includes(resolution)) {
147
+ process.stderr.write(
148
+ `Invalid value for --resolution: ${resolution}. Allowed: ${allowedResolutions.join("|")}\n`
149
+ );
150
+ process.exitCode = 1;
151
+ return;
152
+ }
153
+ const refsList =
154
+ typeof opts.refs === "string" && opts.refs.length > 0
155
+ ? opts.refs
156
+ .split(",")
157
+ .map((s) => s.trim())
158
+ .filter((s) => s.length > 0)
159
+ : [];
160
+ const output = typeof opts.output === "string" ? opts.output : undefined;
161
+ await performImageGeneration(session, {
162
+ prompt,
163
+ model: modelArg,
164
+ aspectRatio,
165
+ resolution,
166
+ refsList,
167
+ output,
168
+ });
169
+ }
170
+
171
+ // default action on `zerocut image`
172
+ parent
173
+ .option("--prompt <prompt>", "Text prompt for image generation (required)")
174
+ .option("--model <model>", `Generator model: ${allowedModels.join("|")}`)
175
+ .option("--aspectRatio <ratio>", `Aspect ratio: ${allowedAspectRatios.join("|")}`)
176
+ .option("--resolution <resolution>", `Resolution: ${allowedResolutions.join("|")}`)
177
+ .option("--refs <refs>", "Comma-separated reference image paths/urls")
178
+ .option("--output <file>", "Output file path")
179
+ .action(imageCreateAction);
180
+
181
+ // keep `image create` for compatibility
182
+ parent
183
+ .command("create")
184
+ .description("Create a new image; requires --prompt")
185
+ .option("--prompt <prompt>", "Text prompt for image generation (required)")
186
+ .option("--model <model>", `Generator model: ${allowedModels.join("|")}`)
187
+ .option("--aspectRatio <ratio>", `Aspect ratio: ${allowedAspectRatios.join("|")}`)
188
+ .option("--resolution <resolution>", `Resolution: ${allowedResolutions.join("|")}`)
189
+ .option("--refs <refs>", "Comma-separated reference image paths/urls")
190
+ .option("--output <file>", "Output file path")
191
+ .action(imageCreateAction);
192
+
193
+ // removed `image edit`
194
+ }