Merge pull request #13 from modelstudioai/feat/watermark-and-paired-flags

fix: Fix the issue of the watermark being always on and address the i…
This commit is contained in:
Gong Shiqi
2026-06-05 15:25:36 +08:00
committed by GitHub
13 changed files with 326 additions and 77 deletions
+24 -4
View File
@@ -25,6 +25,15 @@ interface FlagSchema {
arrays: Set<string>;
}
function buildAllowedFlagKeys(options: OptionDef[]): Set<string> {
const keys = new Set<string>();
for (const opt of options) {
const key = flagKey(opt);
if (key) keys.add(key);
}
return keys;
}
function buildSchema(options: OptionDef[]): FlagSchema {
const booleans = new Set<string>();
const numbers = new Set<string>();
@@ -91,6 +100,7 @@ export function scanCommandPath(argv: string[], globalOptions: OptionDef[] = [])
* - default: string
*/
export function parseFlags(argv: string[], options: OptionDef[]): GlobalFlags {
const allowedKeys = buildAllowedFlagKeys(options);
const schema = buildSchema(options);
const flags: GlobalFlags = {
quiet: false,
@@ -130,20 +140,30 @@ export function parseFlags(argv: string[], options: OptionDef[]): GlobalFlags {
const camelKey = kebabToCamel(key);
if (!allowedKeys.has(camelKey)) {
throw new BailianError(
`Unknown flag "--${key}". Run with --help to see available options.`,
ExitCode.USAGE,
);
}
// Switch-style flags (--quiet, --dry-run): no value. Value flags need a non-flag next token.
if (schema.booleans.has(camelKey)) {
(flags as Record<string, unknown>)[camelKey] = true;
i++;
continue;
}
// --prompt <text>, --watermark <bool>, …
if (value === undefined) {
i++;
value = argv[i];
const next = argv[i];
if (next === undefined || next.startsWith("-")) {
throw new BailianError(`Flag --${key} requires a value.`, ExitCode.USAGE);
}
value = next;
}
if (value === undefined)
throw new BailianError(`Flag --${key} requires a value.`, ExitCode.USAGE);
if (schema.arrays.has(camelKey)) {
const arr = (flags as Record<string, unknown>)[camelKey] as string[] | undefined;
if (arr) arr.push(value);
+19 -13
View File
@@ -15,6 +15,8 @@ import {
type DashScopeImageSyncResponse,
ExitCode,
BailianError,
resolveBooleanFlag,
resolveWatermark,
} from "bailian-cli-core";
import { downloadFile } from "../../utils/download.ts";
import { runConcurrent, downloadParallel, getConcurrency } from "../../utils/concurrent.ts";
@@ -22,6 +24,10 @@ import { promptText, failIfMissing } from "../../output/prompt.ts";
import { emitResult, emitBare } from "../../output/output.ts";
import { resolveImageSize } from "../../utils/image-size.ts";
import { join } from "path";
import {
BOOL_FLAG_PROMPT_EXTEND_CLI_TRUE,
BOOL_FLAG_WATERMARK,
} from "../../utils/flag-descriptions.ts";
export default defineCommand({
name: "image edit",
@@ -47,9 +53,14 @@ export default defineCommand({
flag: "--negative-prompt <text>",
description: "Negative prompt to exclude unwanted content",
},
{ flag: "--prompt-extend", description: "Enable prompt smart rewrite (default: true)" },
{ flag: "--no-prompt-extend", description: "Disable prompt extend" },
{ flag: "--watermark", description: "Add watermark to output images" },
{
flag: "--prompt-extend <bool>",
description: BOOL_FLAG_PROMPT_EXTEND_CLI_TRUE,
},
{
flag: "--watermark <bool>",
description: BOOL_FLAG_WATERMARK,
},
{ flag: "--out-dir <dir>", description: "Download images to directory" },
{ flag: "--out-prefix <prefix>", description: "Filename prefix (default: edited)" },
],
@@ -58,6 +69,7 @@ export default defineCommand({
'bl image edit --image https://example.com/logo.png --prompt "Change color to blue" --n 3',
'bl image edit --image ./a.png --image ./b.png --prompt "把两张图合并成一张拼图"',
'bl image edit --image https://example.com/photo.png --prompt "Remove the person" --model qwen-image-2.0-pro',
'bl image edit --image ./photo.png --prompt "把背景换成海滩" --watermark false',
],
async run(config: Config, flags: GlobalFlags) {
// Normalize --image to string array (supports both single and repeated flags)
@@ -96,15 +108,7 @@ export default defineCommand({
);
const n = (flags.n as number) ?? 1;
// Determine prompt_extend
let promptExtend: boolean | undefined;
if (flags.noPromptExtend === true) {
promptExtend = false;
} else if (flags.promptExtend === true) {
promptExtend = true;
} else {
promptExtend = true; // default on for qwen-image
}
const promptExtend = resolveBooleanFlag(flags.promptExtend, true, "prompt-extend");
// Build content: all images first, then text prompt
const contentItems: Array<{ image?: string; text?: string }> = resolvedImages.map(
@@ -112,6 +116,8 @@ export default defineCommand({
);
contentItems.push({ text: prompt! });
const watermark = resolveWatermark(flags.watermark);
const body: DashScopeImageRequest = {
model,
input: {
@@ -127,7 +133,7 @@ export default defineCommand({
n,
seed: flags.seed as number | undefined,
prompt_extend: promptExtend,
watermark: flags.watermark === true ? true : undefined,
watermark,
negative_prompt: (flags.negativePrompt as string) || undefined,
},
};
+23 -15
View File
@@ -17,6 +17,8 @@ import {
type OutputFormat,
type DashScopeTaskResponse,
generateFilename,
resolveBooleanFlag,
resolveWatermark,
} from "bailian-cli-core";
import { poll } from "../../utils/polling.ts";
import { downloadFile } from "../../utils/download.ts";
@@ -24,6 +26,10 @@ import { runConcurrent, downloadParallel, getConcurrency } from "../../utils/con
import { promptText, failIfMissing } from "../../output/prompt.ts";
import { emitResult, emitBare } from "../../output/output.ts";
import { resolveImageSize } from "../../utils/image-size.ts";
import {
BOOL_FLAG_PROMPT_EXTEND_IMAGE_GENERATE,
BOOL_FLAG_WATERMARK,
} from "../../utils/flag-descriptions.ts";
import { join } from "path";
@@ -57,11 +63,13 @@ export default defineCommand({
description: "Negative prompt to exclude unwanted content",
},
{
flag: "--prompt-extend",
description: "Automatically extend prompt for better results (default: true for qwen-image)",
flag: "--prompt-extend <bool>",
description: BOOL_FLAG_PROMPT_EXTEND_IMAGE_GENERATE,
},
{
flag: "--watermark <bool>",
description: BOOL_FLAG_WATERMARK,
},
{ flag: "--no-prompt-extend", description: "Disable prompt extend" },
{ flag: "--watermark", description: "Add watermark to generated images" },
{
flag: "--no-wait",
description: "Return task ID immediately without waiting (async models only)",
@@ -78,7 +86,9 @@ export default defineCommand({
'bl image generate --prompt "一只穿太空服的猫在火星上"',
'bl image generate --prompt "Logo design" --n 3 --out-dir ./generated/',
'bl image generate --prompt "Mountain landscape" --size 2688*1536',
'bl image generate --prompt "A castle" --seed 42 --no-prompt-extend',
'bl image generate --prompt "A castle" --seed 42 --prompt-extend false',
'bl image generate --prompt "Logo" --watermark false',
'bl image generate --prompt "An alien in the space" --watermark false',
'bl image generate --prompt "sunset" --model wan2.6-t2i --no-wait --quiet',
'bl image generate --prompt "Pro quality" --model qwen-image-2.0-pro',
'bl image generate --prompt "Product shots" --n 2 --concurrent 3 # 6 images in parallel',
@@ -111,15 +121,13 @@ export default defineCommand({
const n = (flags.n as number) ?? 1;
const concurrent = getConcurrency(flags);
// Determine prompt_extend: default true for qwen-image, undefined for others
let promptExtend: boolean | undefined;
if (flags.noPromptExtend === true) {
promptExtend = false;
} else if (flags.promptExtend === true) {
promptExtend = true;
} else if (useSync) {
promptExtend = true; // qwen-image default
}
const promptExtend = resolveBooleanFlag(
flags.promptExtend,
useSync ? true : undefined,
"prompt-extend",
);
const watermark = resolveWatermark(flags.watermark);
const body: DashScopeImageRequest = {
model,
@@ -131,7 +139,7 @@ export default defineCommand({
n,
seed: flags.seed as number | undefined,
prompt_extend: promptExtend,
watermark: flags.watermark === true ? true : undefined,
watermark,
negative_prompt: (flags.negativePrompt as string) || undefined,
},
};
+18 -6
View File
@@ -15,11 +15,17 @@ import {
resolveCredential,
BailianError,
ExitCode,
resolveBooleanFlag,
resolveWatermark,
} from "bailian-cli-core";
import { poll } from "../../utils/polling.ts";
import { downloadFile, formatBytes } from "../../utils/download.ts";
import { promptText, failIfMissing } from "../../output/prompt.ts";
import { emitResult, emitBare } from "../../output/output.ts";
import {
BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT,
BOOL_FLAG_WATERMARK,
} from "../../utils/flag-descriptions.ts";
export default defineCommand({
name: "video edit",
@@ -51,9 +57,14 @@ export default defineCommand({
flag: "--audio-setting <mode>",
description: "Audio: auto (default) or origin (keep original)",
},
{ flag: "--prompt-extend", description: "Enable prompt intelligent rewriting (default: true)" },
{ flag: "--no-prompt-extend", description: "Disable prompt intelligent rewriting" },
{ flag: "--watermark", description: 'Add "AI生成" watermark' },
{
flag: "--prompt-extend <bool>",
description: BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT,
},
{
flag: "--watermark <bool>",
description: BOOL_FLAG_WATERMARK,
},
{ flag: "--seed <n>", description: "Random seed for reproducible generation", type: "number" },
{ flag: "--download <path>", description: "Save video to file on completion" },
{ flag: "--no-wait", description: "Return task ID immediately without waiting" },
@@ -71,6 +82,7 @@ export default defineCommand({
'bl video edit --video https://example.com/input.mp4 --prompt "将整个画面转换为黏土风格"',
'bl video edit --video https://example.com/input.mp4 --prompt "替换衣服为图片中的款式" --ref-image https://example.com/clothes.png',
'bl video edit --video https://example.com/input.mp4 --prompt "Convert to anime style" --resolution 720P --download output.mp4',
'bl video edit --video https://example.com/input.mp4 --prompt "给视频里的小猫穿上衣服" --watermark false',
],
async run(config: Config, flags: GlobalFlags) {
// --- Validate video URL ---
@@ -127,8 +139,8 @@ export default defineCommand({
}
// --- Build request body ---
const promptExtend =
flags.noPromptExtend === true ? false : flags.promptExtend === true ? true : undefined;
const promptExtend = resolveBooleanFlag(flags.promptExtend, undefined, "prompt-extend");
const watermark = resolveWatermark(flags.watermark);
const body: DashScopeVideoEditRequest = {
model,
@@ -143,7 +155,7 @@ export default defineCommand({
duration: (flags.duration as number) || undefined,
audio_setting: (flags.audioSetting as "auto" | "origin") || undefined,
prompt_extend: promptExtend,
watermark: flags.watermark === true ? true : undefined,
watermark,
seed: flags.seed as number | undefined,
},
};
+20 -4
View File
@@ -15,12 +15,18 @@ import {
resolveCredential,
BailianError,
ExitCode,
resolveBooleanFlag,
resolveWatermark,
} from "bailian-cli-core";
import { poll } from "../../utils/polling.ts";
import { downloadFile, formatBytes } from "../../utils/download.ts";
import { runConcurrent, getConcurrency } from "../../utils/concurrent.ts";
import { promptText, failIfMissing } from "../../output/prompt.ts";
import { emitResult, emitBare } from "../../output/output.ts";
import {
BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT,
BOOL_FLAG_WATERMARK,
} from "../../utils/flag-descriptions.ts";
// Normalize shorthand resolution (720P, 1080P) to pixel format for video generation models
const RESOLUTION_SHORTCUTS: Record<string, string> = {
@@ -58,8 +64,14 @@ export default defineCommand({
description: "Video duration in seconds (default: 5)",
type: "number",
},
{ flag: "--prompt-extend", description: "Automatically extend prompt for better results" },
{ flag: "--watermark", description: "Add watermark to generated video" },
{
flag: "--prompt-extend <bool>",
description: BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT,
},
{
flag: "--watermark <bool>",
description: BOOL_FLAG_WATERMARK,
},
{ flag: "--seed <n>", description: "Random seed for reproducible generation", type: "number" },
{ flag: "--download <path>", description: "Save video to file on completion" },
{ flag: "--no-wait", description: "Return task ID immediately without waiting" },
@@ -78,6 +90,7 @@ export default defineCommand({
'bl video generate --prompt "Ocean waves at sunset." --download sunset.mp4',
'bl video generate --image https://example.com/cat.png --prompt "让画面中的猫动起来"',
'bl video generate --prompt "Mountain landscape" --resolution 1280*720 --duration 5',
'bl video generate --prompt "A cat playing with a ball" --watermark false',
],
async run(config: Config, flags: GlobalFlags) {
let prompt = flags.prompt as string | undefined;
@@ -110,6 +123,9 @@ export default defineCommand({
resolvedImageUrl = await resolveFileUrl(imageUrl, credential.token, model);
}
const watermark = resolveWatermark(flags.watermark);
const promptExtend = resolveBooleanFlag(flags.promptExtend, undefined, "prompt-extend");
const body: DashScopeVideoRequest = {
model,
input: {
@@ -124,8 +140,8 @@ export default defineCommand({
resolution: normalizeResolution(flags.resolution as string) || undefined,
ratio: (flags.ratio as string) || undefined,
duration: (flags.duration as number) || undefined,
prompt_extend: flags.promptExtend === true ? true : undefined,
watermark: flags.watermark === true ? true : undefined,
prompt_extend: promptExtend,
watermark,
seed: flags.seed as number | undefined,
},
};
+18 -6
View File
@@ -15,11 +15,17 @@ import {
resolveCredential,
BailianError,
ExitCode,
resolveBooleanFlag,
resolveWatermark,
} from "bailian-cli-core";
import { poll } from "../../utils/polling.ts";
import { downloadFile, formatBytes } from "../../utils/download.ts";
import { promptText, failIfMissing } from "../../output/prompt.ts";
import { emitResult, emitBare } from "../../output/output.ts";
import {
BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT,
BOOL_FLAG_WATERMARK,
} from "../../utils/flag-descriptions.ts";
export default defineCommand({
name: "video ref",
@@ -61,9 +67,14 @@ export default defineCommand({
description: "Video duration in seconds (2-10, default: 5)",
type: "number",
},
{ flag: "--prompt-extend", description: "Enable prompt intelligent rewriting" },
{ flag: "--no-prompt-extend", description: "Disable prompt intelligent rewriting" },
{ flag: "--watermark", description: "Add watermark to generated video" },
{
flag: "--prompt-extend <bool>",
description: BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT,
},
{
flag: "--watermark <bool>",
description: BOOL_FLAG_WATERMARK,
},
{ flag: "--seed <n>", description: "Random seed for reproducible generation", type: "number" },
{ flag: "--download <path>", description: "Save video to file on completion" },
{ flag: "--no-wait", description: "Return task ID immediately without waiting" },
@@ -82,6 +93,7 @@ export default defineCommand({
'bl video ref --prompt "视频1在弹吉他,图1走过来" --ref-video scene.mp4 --image person.jpg',
'bl video ref --prompt "图1说话" --image person.jpg --image-voice voice.mp3 --resolution 1080P',
'bl video ref --prompt "图1和图2在对话" --image a.jpg --image b.jpg --image-voice va.mp3 --image-voice vb.mp3',
'bl video ref --prompt "图1在喝水" --image person.jpg --watermark false',
],
async run(config: Config, flags: GlobalFlags) {
// --- Validate prompt ---
@@ -157,8 +169,8 @@ export default defineCommand({
}
// --- Build request body ---
const promptExtend =
flags.noPromptExtend === true ? false : flags.promptExtend === true ? true : undefined;
const promptExtend = resolveBooleanFlag(flags.promptExtend, undefined, "prompt-extend");
const watermark = resolveWatermark(flags.watermark);
const body: DashScopeVideoRefRequest = {
model,
@@ -171,7 +183,7 @@ export default defineCommand({
ratio: (flags.ratio as string) || undefined,
duration: (flags.duration as number) || undefined,
prompt_extend: promptExtend,
watermark: flags.watermark === true ? true : undefined,
watermark,
seed: flags.seed as number | undefined,
},
};
+22 -28
View File
@@ -14,6 +14,8 @@ import {
resolveFileUrl,
resolveCredential,
stripUndefined,
resolveBooleanFlag,
resolveWatermark,
type Config,
type ChatRequest,
type ChatResponse,
@@ -163,9 +165,8 @@ export interface ImageGenerateInput {
n?: number;
seed?: number;
"negative-prompt"?: string;
"prompt-extend"?: boolean;
"no-prompt-extend"?: boolean;
watermark?: boolean;
"prompt-extend"?: boolean | string;
watermark?: boolean | string;
"out-dir"?: string;
"out-prefix"?: string;
}
@@ -185,14 +186,11 @@ export async function imageGenerate(
const useSync = isSyncImageModel(model);
const n = input.n ?? 1;
let promptExtend: boolean | undefined;
if (input["no-prompt-extend"]) {
promptExtend = false;
} else if (input["prompt-extend"]) {
promptExtend = true;
} else if (useSync) {
promptExtend = true;
}
const promptExtend = resolveBooleanFlag(
input["prompt-extend"],
useSync ? true : undefined,
"prompt-extend",
);
const body: DashScopeImageRequest = {
model,
@@ -204,7 +202,7 @@ export async function imageGenerate(
n,
seed: input.seed,
prompt_extend: promptExtend,
watermark: input.watermark === true ? true : undefined,
watermark: resolveWatermark(input.watermark),
negative_prompt: input["negative-prompt"] || undefined,
},
};
@@ -252,9 +250,8 @@ export interface ImageEditInput {
n?: number;
seed?: number;
"negative-prompt"?: string;
"prompt-extend"?: boolean;
"no-prompt-extend"?: boolean;
watermark?: boolean;
"prompt-extend"?: boolean | string;
watermark?: boolean | string;
"out-dir"?: string;
"out-prefix"?: string;
}
@@ -275,14 +272,11 @@ export async function imageEdit(
const useSync = isSyncImageModel(model);
const n = input.n ?? 1;
let promptExtend: boolean | undefined;
if (input["no-prompt-extend"]) {
promptExtend = false;
} else if (input["prompt-extend"]) {
promptExtend = true;
} else if (useSync) {
promptExtend = true;
}
const promptExtend = resolveBooleanFlag(
input["prompt-extend"],
useSync ? true : undefined,
"prompt-extend",
);
const content: Array<{ text?: string; image?: string }> = [];
for (const img of images) {
@@ -305,7 +299,7 @@ export async function imageEdit(
n,
seed: input.seed,
prompt_extend: promptExtend,
watermark: input.watermark === true ? true : undefined,
watermark: resolveWatermark(input.watermark),
negative_prompt: input["negative-prompt"] || undefined,
},
};
@@ -380,8 +374,8 @@ export interface VideoGenerateInput {
resolution?: string;
ratio?: string;
duration?: number;
"prompt-extend"?: boolean;
watermark?: boolean;
"prompt-extend"?: boolean | string;
watermark?: boolean | string;
seed?: number;
"poll-interval"?: number;
}
@@ -424,8 +418,8 @@ export async function videoGenerate(
resolution: input.resolution || undefined,
ratio: input.ratio || undefined,
duration: input.duration,
prompt_extend: input["prompt-extend"],
watermark: input.watermark,
prompt_extend: resolveBooleanFlag(input["prompt-extend"], undefined, "prompt-extend"),
watermark: resolveWatermark(input.watermark),
seed: input.seed,
},
};
@@ -0,0 +1,16 @@
/** Shared --foo <bool> help text; keep wording consistent with actual CLI/request behavior. */
export const BOOL_FLAG_WATERMARK =
"Enable watermark (true/false). Omit flag to use CLI default (true).";
/** CLI sends prompt_extend=true when flag omitted (qwen-image edit, etc.). */
export const BOOL_FLAG_PROMPT_EXTEND_CLI_TRUE =
"Enable prompt extend (true/false). Omit flag to use CLI default (true).";
/** Sync qwen-image defaults on; async models omit the field unless flag is set. */
export const BOOL_FLAG_PROMPT_EXTEND_IMAGE_GENERATE =
"Enable prompt extend (true/false). Omit flag: true for qwen-image sync; parameter omitted on async models (API default).";
/** CLI omits prompt_extend in the request when flag is unset (video commands). */
export const BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT =
"Enable prompt extend (true/false). Omit flag to omit the parameter (DashScope default).";
+95
View File
@@ -0,0 +1,95 @@
import { expect, test } from "vite-plus/test";
import { ExitCode, GLOBAL_OPTIONS } from "bailian-cli-core";
import { parseFlags } from "../src/args.ts";
import { BOOL_FLAG_WATERMARK } from "../src/utils/flag-descriptions.ts";
const IMAGE_GENERATE_OPTIONS = [
{ flag: "--prompt <text>", description: "Image description", required: true },
{ flag: "--model <model>", description: "Model ID" },
{ flag: "--watermark <bool>", description: BOOL_FLAG_WATERMARK },
{ flag: "--no-wait", description: "Return task ID immediately without waiting" },
];
test("parseFlags rejects unknown long flags", () => {
expect(() =>
parseFlags(["--prompt", "cat", "--xxxx", "a"], [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS]),
).toThrowError(
expect.objectContaining({
name: "BailianError",
exitCode: ExitCode.USAGE,
message: expect.stringContaining('Unknown flag "--xxxx"'),
}),
);
});
test("parseFlags rejects unknown flags with = syntax", () => {
expect(() =>
parseFlags(
["--prompt=cat", "--unknown-flag=yes"],
[...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS],
),
).toThrow(/Unknown flag "--unknown-flag"/);
});
test("parseFlags accepts defined command and global flags", () => {
const flags = parseFlags(
["--quiet", "--prompt", "cat", "--watermark", "false"],
[...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS],
);
expect(flags.quiet).toBe(true);
expect(flags.prompt).toBe("cat");
expect(flags.watermark).toBe("false");
});
test("parseFlags rejects value flag when next token is another flag", () => {
const opts = [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS];
for (const argv of [
["--watermark", "--prompt", "cat"],
["--watermark", "-h"],
["--prompt", "cat", "--watermark", "--model", "qwen-image-2.0"],
]) {
expect(() => parseFlags(argv, opts)).toThrowError(
expect.objectContaining({
name: "BailianError",
exitCode: ExitCode.USAGE,
message: expect.stringContaining("Flag --watermark requires a value"),
}),
);
}
});
test("parseFlags rejects trailing value flag without value", () => {
expect(() =>
parseFlags(["--prompt", "cat", "--watermark"], [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS]),
).toThrowError(
expect.objectContaining({
message: expect.stringContaining("Flag --watermark requires a value"),
}),
);
});
test("parseFlags allows boolean flags without values adjacent to other flags", () => {
const opts = [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS];
const flags = parseFlags(
["--quiet", "--dry-run", "--no-wait", "--prompt", "cat", "--watermark", "false"],
opts,
);
expect(flags.quiet).toBe(true);
expect(flags.dryRun).toBe(true);
expect(flags.noWait).toBe(true);
expect(flags.prompt).toBe("cat");
expect(flags.watermark).toBe("false");
});
test("parseFlags does not treat the next flag as a boolean flag value", () => {
const opts = [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS];
expect(() => parseFlags(["--dry-run", "--prompt"], opts)).toThrowError(
expect.objectContaining({
message: expect.stringContaining("Flag --prompt requires a value"),
}),
);
// --dry-run is boolean: no value check; parsing continues to --prompt.
const flags = parseFlags(["--dry-run", "--prompt", "cat"], opts);
expect(flags.dryRun).toBe(true);
expect(flags.prompt).toBe("cat");
});
-1
View File
@@ -67,7 +67,6 @@ const PARAM_ALLOWLIST = new Set([
"noWait",
"textOnly",
"promptExtend",
"noPromptExtend",
"enableSsml",
"watermark",
"hasThoughts",
+40
View File
@@ -0,0 +1,40 @@
import { BailianError } from "../errors/base.ts";
import { ExitCode } from "../errors/codes.ts";
/** Parse true/false from CLI flags (e.g. `--watermark <bool>`). */
export function parseBooleanValue(value: unknown, label = "boolean"): boolean {
if (typeof value === "boolean") return value;
if (typeof value === "string") {
const v = value.trim().toLowerCase();
if (v === "true") return true;
if (v === "false") return false;
}
throw new BailianError(
`Invalid ${label} value "${String(value)}". Use true or false.`,
ExitCode.USAGE,
);
}
export function parseOptionalBooleanValue(value: unknown, label = "boolean"): boolean | undefined {
if (value === undefined || value === null) return undefined;
return parseBooleanValue(value, label);
}
/**
* Resolve a tri-state boolean CLI flag (`--name <bool>`).
* Returns `defaultWhenUnset` when the flag is omitted.
*/
export function resolveBooleanFlag(
flagValue: unknown,
defaultWhenUnset: boolean | undefined,
label = "boolean",
): boolean | undefined {
const fromFlag = parseOptionalBooleanValue(flagValue, label);
if (fromFlag !== undefined) return fromFlag;
return defaultWhenUnset;
}
/** Resolve `--watermark` flag; default true when unset. */
export function resolveWatermark(flagValue: unknown): boolean {
return parseOptionalBooleanValue(flagValue, "watermark") ?? true;
}
+6
View File
@@ -5,3 +5,9 @@ export { maskToken } from "./token.ts";
export { isInteractive } from "./env.ts";
export { isCI } from "./env.ts";
export { stripUndefined } from "./object.ts";
export {
parseBooleanValue,
parseOptionalBooleanValue,
resolveBooleanFlag,
resolveWatermark,
} from "./boolean-flag.ts";
+25
View File
@@ -2,6 +2,11 @@ import { expect, test } from "vite-plus/test";
import type { Config } from "../src/index.ts";
import { BailianError, ExitCode, McpClient, mapApiError, request } from "../src/index.ts";
import { parseConfigFile } from "../src/config/schema.ts";
import {
parseBooleanValue,
resolveBooleanFlag,
resolveWatermark,
} from "../src/utils/boolean-flag.ts";
function testConfig(overrides: Partial<Config> = {}): Config {
return {
@@ -175,6 +180,26 @@ test("McpClient uses injected client identity for initialize and User-Agent", as
});
});
test("resolveWatermark uses flag or defaults to true", () => {
expect(resolveWatermark("false")).toBe(false);
expect(resolveWatermark("true")).toBe(true);
expect(resolveWatermark(undefined)).toBe(true);
});
test("resolveBooleanFlag uses flag or defaultWhenUnset", () => {
expect(resolveBooleanFlag("false", true, "prompt-extend")).toBe(false);
expect(resolveBooleanFlag(undefined, true, "prompt-extend")).toBe(true);
expect(resolveBooleanFlag(undefined, undefined, "prompt-extend")).toBeUndefined();
});
test("parseBooleanValue accepts only true and false strings (case-insensitive)", () => {
expect(parseBooleanValue("true")).toBe(true);
expect(parseBooleanValue("FALSE")).toBe(false);
expect(() => parseBooleanValue("1")).toThrow(BailianError);
expect(() => parseBooleanValue("yes")).toThrow(BailianError);
expect(() => parseBooleanValue("maybe")).toThrow(BailianError);
});
test("parseConfigFile accepts only well-formed http(s) base_url / console_gateway_url", () => {
expect(parseConfigFile({ base_url: "https://dashscope.aliyuncs.com" }).base_url).toBe(
"https://dashscope.aliyuncs.com",