mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
refactor(runtime): resolve/middleware kernel + declarative arg validation
把 main 从一堆 if + process.exit 重构为「argv 解析成数据 → 交给统一管线执行」。 内核 - resolve(argv) → Resolution(version/help/run/usageError):路由即数据,dispatch 只 switch - compose 洋葱中间件 (versionCheck/telemetry/auth/runCommand),命令仍收 (config, flags) - registry.locate() 统一 leaf/group/unknown,取代 isGroupPath + 抛异常的 resolve - 删除 command-help.ts 全局可变单例:help 渲染收口到错误边界 错误模型 - 新增 UsageError(写错了 → exit 2) 与 IncompleteCommandError(没写完 → 打 help、exit 0) - version / help / onboarding / 组帮助统一由 resolve 产出、dispatch 分派 参数与校验 - parseFlags 重写:无 positional、新增 switch 类型、值/类型/重复校验 - 无条件必填 → 解析器声明式强制 (OptionDef.required) - 跨 flag / 条件约束 → 新增 command.validate(flags) 钩子 (text-chat / search-web / speech / vision / video-ref) - 移除全部交互式输入 (promptText/Select/Confirm),缺输入直接打 help 输出 - detectOutputFormat 默认 text,不再按 TTY 切 json 测试 - 删除 3 个 stale cli 测试,runtime 单测重写 (29 passed),e2e 适配新行为
This commit is contained in:
@@ -1,95 +0,0 @@
|
||||
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");
|
||||
});
|
||||
@@ -63,10 +63,10 @@ describe("e2e: config", () => {
|
||||
expect(stdout).toMatch(/config_file|timeout|base_url/i);
|
||||
});
|
||||
|
||||
test("config set 缺少 --key / --value 时退出为用法错误 (2)", async () => {
|
||||
test("config set 缺少 --key / --value 时打印子命令帮助并退出 (0)", async () => {
|
||||
const { stderr, exitCode } = await runCli(["config", "set", "--non-interactive"]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/--key|--value|required/i);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/--key|--value|Usage:/i);
|
||||
});
|
||||
|
||||
test("config set 非法 key 时退出为用法错误", async () => {
|
||||
|
||||
@@ -66,7 +66,7 @@ describe("e2e: knowledge retrieve", () => {
|
||||
// ---- Error scenarios (no real credentials needed) ----
|
||||
|
||||
describe("e2e: knowledge retrieve errors", () => {
|
||||
test("无任何凭证时提示 No credentials found 并非零退出", async () => {
|
||||
test("无任何凭证时提示缺少密钥并非零退出", async () => {
|
||||
const { stderr, exitCode } = await runCli(
|
||||
[
|
||||
"knowledge",
|
||||
@@ -88,7 +88,7 @@ describe("e2e: knowledge retrieve errors", () => {
|
||||
},
|
||||
);
|
||||
expect(exitCode).not.toBe(0);
|
||||
expect(stderr).toMatch(/no credentials found/i);
|
||||
expect(stderr).toMatch(/no api key found|no credentials found/i);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -33,13 +33,13 @@ describe("e2e: mcp", () => {
|
||||
test("mcp tools --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCli(["mcp", "tools", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/tools|server-code|--url/i);
|
||||
expect(stderr).toMatch(/tools|--server|--url/i);
|
||||
});
|
||||
|
||||
test("mcp call --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCli(["mcp", "call", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/call|server-code|tool|--arg|--json/i);
|
||||
expect(stderr).toMatch(/call|--target|--arg|--json/i);
|
||||
});
|
||||
|
||||
test("mcp list --help 不暴露 --all 入口(市场全量已下线)", async () => {
|
||||
@@ -103,10 +103,11 @@ describe("e2e: mcp", () => {
|
||||
expect(data.consoleRegion).toBe("cn-hangzhou");
|
||||
});
|
||||
|
||||
test("mcp tools <server-code> --dry-run 输出 /api/v1/mcps/<code>/mcp 形态 URL", async () => {
|
||||
test("mcp tools --server <code> --dry-run 输出 /api/v1/mcps/<code>/mcp 形态 URL", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"mcp",
|
||||
"tools",
|
||||
"--server",
|
||||
"market-cmapi00073529",
|
||||
"--dry-run",
|
||||
"--non-interactive",
|
||||
@@ -126,6 +127,7 @@ describe("e2e: mcp", () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"mcp",
|
||||
"tools",
|
||||
"--server",
|
||||
"my-server",
|
||||
"--url",
|
||||
"https://example.com/custom/mcp",
|
||||
@@ -140,16 +142,17 @@ describe("e2e: mcp", () => {
|
||||
expect(data.url).toBe("https://example.com/custom/mcp");
|
||||
});
|
||||
|
||||
test("mcp tools 缺少 server-code 时打印子命令帮助并退出 (0)", async () => {
|
||||
test("mcp tools 缺少 --server 时打印子命令帮助并退出 (0)", async () => {
|
||||
const { stderr, exitCode } = await runCli(["mcp", "tools", "--non-interactive"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/server-code|Usage:/i);
|
||||
expect(stderr).toMatch(/--server|Usage:/i);
|
||||
});
|
||||
|
||||
test("mcp call <server-code>.<tool> --dry-run 输出工具调用计划", async () => {
|
||||
test("mcp call --target <server.tool> --dry-run 输出工具调用计划", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"mcp",
|
||||
"call",
|
||||
"--target",
|
||||
"market-cmapi00073529.SmartStockSelection",
|
||||
"--query",
|
||||
"筛选ROE>15%的消费股",
|
||||
@@ -176,6 +179,7 @@ describe("e2e: mcp", () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"mcp",
|
||||
"call",
|
||||
"--target",
|
||||
"market-cmapi00073529.FinQuery",
|
||||
"--json",
|
||||
'{"q":"贵州茅台","limit":5,"riskLevel":"R2"}',
|
||||
@@ -208,10 +212,11 @@ describe("e2e: mcp", () => {
|
||||
expect(data.arguments?.query).toBe("招商银行");
|
||||
});
|
||||
|
||||
test("mcp call 目标缺少 . 时报错且非零退出", async () => {
|
||||
test("mcp call --target 缺少 . 时报错且非零退出", async () => {
|
||||
const { stderr, exitCode } = await runCli([
|
||||
"mcp",
|
||||
"call",
|
||||
"--target",
|
||||
"no-dot-target",
|
||||
"--non-interactive",
|
||||
"--output",
|
||||
@@ -225,6 +230,7 @@ describe("e2e: mcp", () => {
|
||||
const { stderr, exitCode } = await runCli([
|
||||
"mcp",
|
||||
"call",
|
||||
"--target",
|
||||
"srv.tool",
|
||||
"--arg",
|
||||
"no-equals-sign",
|
||||
@@ -240,6 +246,7 @@ describe("e2e: mcp", () => {
|
||||
const { stderr, exitCode } = await runCli([
|
||||
"mcp",
|
||||
"call",
|
||||
"--target",
|
||||
"srv.tool",
|
||||
"--json",
|
||||
"{not-json",
|
||||
@@ -251,10 +258,10 @@ describe("e2e: mcp", () => {
|
||||
expect(stderr).toMatch(/--json is not valid JSON|--json must decode/);
|
||||
});
|
||||
|
||||
test("mcp call 缺少 positional 时打印子命令帮助并退出 (0)", async () => {
|
||||
test("mcp call 缺少 --target 时打印子命令帮助并退出 (0)", async () => {
|
||||
const { stderr, exitCode } = await runCli(["mcp", "call", "--non-interactive"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/server-code|Usage:/i);
|
||||
expect(stderr).toMatch(/--target|Usage:/i);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -266,6 +273,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: mcp (live)", () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"mcp",
|
||||
"tools",
|
||||
"--server",
|
||||
"WebSearch",
|
||||
"--non-interactive",
|
||||
"--output",
|
||||
|
||||
@@ -89,6 +89,7 @@ describe("e2e: pipeline", () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"pipeline",
|
||||
"validate",
|
||||
"--file",
|
||||
chatBasicPath,
|
||||
"--output",
|
||||
"json",
|
||||
@@ -100,9 +101,12 @@ describe("e2e: pipeline", () => {
|
||||
});
|
||||
|
||||
test("pipeline validate 使用 config 输出格式", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli(["pipeline", "validate", chatBasicPath], {
|
||||
DASHSCOPE_OUTPUT: "text",
|
||||
});
|
||||
const { stdout, stderr, exitCode } = await runCli(
|
||||
["pipeline", "validate", "--file", chatBasicPath],
|
||||
{
|
||||
DASHSCOPE_OUTPUT: "text",
|
||||
},
|
||||
);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stdout).toBe("Pipeline definition is valid.\n");
|
||||
});
|
||||
@@ -111,6 +115,7 @@ describe("e2e: pipeline", () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"pipeline",
|
||||
"validate",
|
||||
"--file",
|
||||
invalidPipelinePath,
|
||||
"--output",
|
||||
"json",
|
||||
@@ -122,16 +127,17 @@ describe("e2e: pipeline", () => {
|
||||
expect(data.issues?.join("\n")).toMatch(/pipeline graph contains cycle/i);
|
||||
});
|
||||
|
||||
test("pipeline run 缺少 file 时退出为用法错误 (2)", async () => {
|
||||
test("pipeline run 缺少 --file 时打印子命令帮助并退出 (0)", async () => {
|
||||
const { stderr, exitCode } = await runCli(["pipeline", "run", "--non-interactive"]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/pipeline file is required|Usage: bl pipeline run <file>/i);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/Usage: bl pipeline run --file <path>|--file/i);
|
||||
});
|
||||
|
||||
test("pipeline run --dry-run --output json 仅输出计划", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"pipeline",
|
||||
"run",
|
||||
"--file",
|
||||
chatBasicPath,
|
||||
"--input",
|
||||
'{"message":"hello"}',
|
||||
@@ -166,6 +172,7 @@ describe("e2e: pipeline", () => {
|
||||
[
|
||||
"pipeline",
|
||||
"run",
|
||||
"--file",
|
||||
chatBasicPath,
|
||||
"--input",
|
||||
'{"message":"hello"}',
|
||||
@@ -183,6 +190,7 @@ describe("e2e: pipeline", () => {
|
||||
const { stderr, exitCode } = await runCli([
|
||||
"pipeline",
|
||||
"run",
|
||||
"--file",
|
||||
chatBasicPath,
|
||||
"--input",
|
||||
'{"message":"hello"}',
|
||||
@@ -199,6 +207,7 @@ describe("e2e: pipeline", () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"pipeline",
|
||||
"run",
|
||||
"--file",
|
||||
chatBasicPath,
|
||||
"--input",
|
||||
'{"message":"hello"}',
|
||||
@@ -227,6 +236,7 @@ describe("e2e: pipeline", () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"pipeline",
|
||||
"run",
|
||||
"--file",
|
||||
chatBasicPath,
|
||||
"--dry-run",
|
||||
"--events",
|
||||
|
||||
@@ -27,6 +27,19 @@ describe("e2e: search web", () => {
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/web|--query|list-tools|count/i);
|
||||
});
|
||||
|
||||
test("search web --dry-run --list-tools 无需 --query 也无需凭证即可干跑", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli(
|
||||
["search", "web", "--dry-run", "--list-tools", "--non-interactive", "--output", "json"],
|
||||
{
|
||||
DASHSCOPE_API_KEY: undefined,
|
||||
DASHSCOPE_ACCESS_TOKEN: undefined,
|
||||
},
|
||||
);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{ action?: string }>(stdout);
|
||||
expect(data.action).toBe("tools/list");
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!isDashScopeE2EReady())("e2e: search web", () => {
|
||||
@@ -61,21 +74,6 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: search web", () => {
|
||||
expect(data.arguments?.count).toBe(5);
|
||||
});
|
||||
|
||||
test("search web --dry-run --list-tools 仅描述 tools/list", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"search",
|
||||
"web",
|
||||
"--dry-run",
|
||||
"--list-tools",
|
||||
"--non-interactive",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{ action?: string }>(stdout);
|
||||
expect(data.action).toBe("tools/list");
|
||||
});
|
||||
|
||||
test("联网搜索返回 JSON 且含搜索结果", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"search",
|
||||
|
||||
@@ -87,9 +87,9 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
|
||||
const genMp4 = join(outDir, "e2e-gen-for-download.mp4");
|
||||
|
||||
const gen = await runCli([
|
||||
...cliTimeoutPrefix(),
|
||||
"video",
|
||||
"generate",
|
||||
...cliTimeoutPrefix(),
|
||||
"--model",
|
||||
"happyhorse-1.0-t2v",
|
||||
"--duration",
|
||||
@@ -116,9 +116,9 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
|
||||
|
||||
const downloadMp4 = join(outDir, "e2e-download.mp4");
|
||||
const dl = await runCli([
|
||||
...cliTimeoutPrefix(),
|
||||
"video",
|
||||
"download",
|
||||
...cliTimeoutPrefix(),
|
||||
"--task-id",
|
||||
genData.task_id!,
|
||||
"--out",
|
||||
|
||||
@@ -33,9 +33,9 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
|
||||
() => {
|
||||
test("video edit 缺少 --video 时打印子命令帮助并退出 (0)", async () => {
|
||||
const { stderr, exitCode } = await runCli([
|
||||
...cliTimeoutPrefix(),
|
||||
"video",
|
||||
"edit",
|
||||
...cliTimeoutPrefix(),
|
||||
"--model",
|
||||
"happyhorse-1.0-video-edit",
|
||||
"--prompt",
|
||||
@@ -51,9 +51,9 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
|
||||
const t2vPath = join(outDir, "e2e-video-t2v.mp4");
|
||||
|
||||
const t2v = await runCli([
|
||||
...cliTimeoutPrefix(),
|
||||
"video",
|
||||
"generate",
|
||||
...cliTimeoutPrefix(),
|
||||
"--model",
|
||||
"happyhorse-1.0-t2v",
|
||||
"--prompt",
|
||||
@@ -69,9 +69,9 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
|
||||
expect(t2vData.status).toBe("SUCCEEDED");
|
||||
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
...cliTimeoutPrefix(),
|
||||
"video",
|
||||
"edit",
|
||||
...cliTimeoutPrefix(),
|
||||
"--model",
|
||||
"happyhorse-1.0-video-edit",
|
||||
"--video",
|
||||
|
||||
@@ -33,9 +33,9 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
|
||||
() => {
|
||||
test("video generate 缺少 --prompt 时打印子命令帮助并退出 (0)", async () => {
|
||||
const { stderr, exitCode } = await runCli([
|
||||
...cliTimeoutPrefix(),
|
||||
"video",
|
||||
"generate",
|
||||
...cliTimeoutPrefix(),
|
||||
"--model",
|
||||
"happyhorse-1.0-i2v",
|
||||
"--image",
|
||||
@@ -48,9 +48,9 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
|
||||
|
||||
test("video generate --dry-run(无 --image)仅输出 request(t2v 路径不调上传)", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
...cliTimeoutPrefix(),
|
||||
"video",
|
||||
"generate",
|
||||
...cliTimeoutPrefix(),
|
||||
"--dry-run",
|
||||
"--model",
|
||||
"happyhorse-1.0-t2v",
|
||||
@@ -91,9 +91,9 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
|
||||
const imagePath = genData.saved?.[0] ?? png;
|
||||
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
...cliTimeoutPrefix(),
|
||||
"video",
|
||||
"generate",
|
||||
...cliTimeoutPrefix(),
|
||||
"--model",
|
||||
"happyhorse-1.0-i2v",
|
||||
"--image",
|
||||
|
||||
@@ -33,9 +33,9 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
|
||||
() => {
|
||||
test("video generate 缺少 --prompt 时打印子命令帮助并退出 (0)", async () => {
|
||||
const { stderr, exitCode } = await runCli([
|
||||
...cliTimeoutPrefix(),
|
||||
"video",
|
||||
"generate",
|
||||
...cliTimeoutPrefix(),
|
||||
"--model",
|
||||
"happyhorse-1.0-t2v",
|
||||
"--non-interactive",
|
||||
@@ -46,11 +46,11 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
|
||||
|
||||
test("video generate --dry-run(无 --image)仅输出 request 且不调生成接口", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
...cliTimeoutPrefix(),
|
||||
"video",
|
||||
"generate",
|
||||
"--dry-run",
|
||||
"--model",
|
||||
...cliTimeoutPrefix(),
|
||||
"happyhorse-1.0-t2v",
|
||||
"--prompt",
|
||||
"干跑校验",
|
||||
@@ -69,9 +69,9 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
|
||||
test("【happyhorse-1.0-t2v】文本生成视频", async () => {
|
||||
const outDir = makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url));
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
...cliTimeoutPrefix(),
|
||||
"video",
|
||||
"generate",
|
||||
...cliTimeoutPrefix(),
|
||||
"--model",
|
||||
"happyhorse-1.0-t2v",
|
||||
"--prompt",
|
||||
|
||||
@@ -33,9 +33,9 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
|
||||
() => {
|
||||
test("video ref 缺少 --prompt 时打印子命令帮助并退出 (0)", async () => {
|
||||
const { stderr, exitCode } = await runCli([
|
||||
...cliTimeoutPrefix(),
|
||||
"video",
|
||||
"ref",
|
||||
...cliTimeoutPrefix(),
|
||||
"--model",
|
||||
"happyhorse-1.0-r2v",
|
||||
"--image",
|
||||
@@ -48,9 +48,9 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
|
||||
|
||||
test("video ref 缺少 --image 与 --ref-video 时退出为用法错误 (2)", async () => {
|
||||
const { stderr, exitCode } = await runCli([
|
||||
...cliTimeoutPrefix(),
|
||||
"video",
|
||||
"ref",
|
||||
...cliTimeoutPrefix(),
|
||||
"--model",
|
||||
"happyhorse-1.0-r2v",
|
||||
"--prompt",
|
||||
@@ -84,9 +84,9 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())(
|
||||
expect(imagePath).toBeTruthy();
|
||||
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
...cliTimeoutPrefix(),
|
||||
"video",
|
||||
"ref",
|
||||
...cliTimeoutPrefix(),
|
||||
"--model",
|
||||
"happyhorse-1.0-r2v",
|
||||
"--prompt",
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
import { expect, test } from "vite-plus/test";
|
||||
import { createStepDispatcher } from "../src/pipeline/dispatcher.ts";
|
||||
import { executePipeline } from "../src/pipeline/executor.ts";
|
||||
import { collectPipelineIssues } from "../src/pipeline/validation.ts";
|
||||
import { getByJsonPointer } from "../src/pipeline/schema.ts";
|
||||
import { normalizeConcurrency } from "../src/pipeline/scheduler.ts";
|
||||
import { WORKFLOW_VERSION, type PipelineDefinition } from "../src/pipeline/types.ts";
|
||||
|
||||
test("cli package skeleton", () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
test("pipeline execution can use an isolated step dispatcher", async () => {
|
||||
const dispatcher = createStepDispatcher();
|
||||
dispatcher.registerStep("test/echo", (input, ctx) => ({
|
||||
data: { input, hasSignal: !!ctx.signal },
|
||||
}));
|
||||
|
||||
const controller = new AbortController();
|
||||
const pipeline: PipelineDefinition = {
|
||||
version: WORKFLOW_VERSION,
|
||||
steps: [{ id: "echo", type: "test/echo", input: { message: "hello" } }],
|
||||
};
|
||||
|
||||
const report = await executePipeline(
|
||||
pipeline,
|
||||
{},
|
||||
{
|
||||
stepDispatcher: dispatcher,
|
||||
signal: controller.signal,
|
||||
},
|
||||
);
|
||||
|
||||
expect(report.status).toBe("succeeded");
|
||||
expect(report.steps[0]?.output?.data).toEqual({
|
||||
input: { message: "hello" },
|
||||
hasSignal: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("dry-run never executes $js expressions (preview must not run code)", async () => {
|
||||
const dispatcher = createStepDispatcher();
|
||||
dispatcher.registerStep("test/echo", (input) => ({ data: input }));
|
||||
const flag = "__bailian_dryrun_should_not_run__";
|
||||
delete (globalThis as Record<string, unknown>)[flag];
|
||||
|
||||
const pipeline: PipelineDefinition = {
|
||||
version: WORKFLOW_VERSION,
|
||||
steps: [
|
||||
{
|
||||
id: "s1",
|
||||
type: "test/echo",
|
||||
input: { probe: { $js: `(globalThis[${JSON.stringify(flag)}] = true), 1` } },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const report = await executePipeline(pipeline, {}, { stepDispatcher: dispatcher, dryRun: true });
|
||||
expect(report.status).toBe("planned");
|
||||
expect((globalThis as Record<string, unknown>)[flag]).toBeUndefined();
|
||||
});
|
||||
|
||||
test("script/js rejects non-literal code sourced from another step ($from)", () => {
|
||||
const dispatcher = createStepDispatcher();
|
||||
dispatcher.registerStep("test/echo", (input) => ({ data: input }));
|
||||
dispatcher.registerStep("script/js", () => ({ data: {} }));
|
||||
|
||||
const pipeline: PipelineDefinition = {
|
||||
version: WORKFLOW_VERSION,
|
||||
steps: [
|
||||
{ id: "gen", type: "test/echo", input: { message: "x" } },
|
||||
{
|
||||
id: "run",
|
||||
type: "script/js",
|
||||
input: { code: { $from: "gen", path: "/data/message" } as never },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const issues = collectPipelineIssues(pipeline, dispatcher);
|
||||
expect(issues.some((issue) => issue.includes('literal string "code"'))).toBe(true);
|
||||
});
|
||||
|
||||
test("script/js accepts a literal string code", () => {
|
||||
const dispatcher = createStepDispatcher();
|
||||
dispatcher.registerStep("script/js", () => ({ data: {} }));
|
||||
|
||||
const pipeline: PipelineDefinition = {
|
||||
version: WORKFLOW_VERSION,
|
||||
steps: [{ id: "run", type: "script/js", input: { code: "return 1" } }],
|
||||
};
|
||||
|
||||
expect(collectPipelineIssues(pipeline, dispatcher)).toEqual([]);
|
||||
});
|
||||
|
||||
test("getByJsonPointer refuses prototype keys and inherited properties", () => {
|
||||
const obj = { a: { b: 1 } };
|
||||
expect(getByJsonPointer(obj, "/a/b")).toBe(1);
|
||||
expect(getByJsonPointer(obj, "/__proto__")).toBeUndefined();
|
||||
expect(getByJsonPointer(obj, "/constructor")).toBeUndefined();
|
||||
expect(getByJsonPointer(obj, "/a/constructor/constructor")).toBeUndefined();
|
||||
expect(getByJsonPointer(obj, "/toString")).toBeUndefined();
|
||||
});
|
||||
|
||||
test("normalizeConcurrency clamps to a safe maximum", () => {
|
||||
expect(normalizeConcurrency(undefined)).toBe(1);
|
||||
expect(normalizeConcurrency(4)).toBe(4);
|
||||
expect(normalizeConcurrency(100000)).toBe(64);
|
||||
});
|
||||
@@ -1,42 +0,0 @@
|
||||
import { expect, test } from "vite-plus/test";
|
||||
import { readProxyEnv } from "../src/proxy.ts";
|
||||
|
||||
test("readProxyEnv: 未设置任何代理变量时全部为 undefined", () => {
|
||||
expect(readProxyEnv({})).toEqual({
|
||||
httpProxy: undefined,
|
||||
httpsProxy: undefined,
|
||||
noProxy: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test("readProxyEnv: 空白值视为未设置", () => {
|
||||
expect(readProxyEnv({ HTTPS_PROXY: "", HTTP_PROXY: " ", NO_PROXY: "" })).toEqual({
|
||||
httpProxy: undefined,
|
||||
httpsProxy: undefined,
|
||||
noProxy: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test("readProxyEnv: 大小写变量均可识别,小写优先", () => {
|
||||
expect(readProxyEnv({ HTTPS_PROXY: "http://upper:1" }).httpsProxy).toBe("http://upper:1");
|
||||
expect(readProxyEnv({ https_proxy: "http://lower:1" }).httpsProxy).toBe("http://lower:1");
|
||||
expect(
|
||||
readProxyEnv({ https_proxy: "http://lower:1", HTTPS_PROXY: "http://upper:1" }).httpsProxy,
|
||||
).toBe("http://lower:1");
|
||||
});
|
||||
|
||||
test("readProxyEnv: 空字符串小写变量不屏蔽已设置的大写变量", () => {
|
||||
expect(readProxyEnv({ https_proxy: "", HTTPS_PROXY: "http://upper:1" }).httpsProxy).toBe(
|
||||
"http://upper:1",
|
||||
);
|
||||
expect(readProxyEnv({ http_proxy: "", HTTP_PROXY: "http://upper:2" }).httpProxy).toBe(
|
||||
"http://upper:2",
|
||||
);
|
||||
});
|
||||
|
||||
test("readProxyEnv: NO_PROXY 独立读取", () => {
|
||||
const r = readProxyEnv({ NO_PROXY: "*.aliyuncs.com" });
|
||||
expect(r.noProxy).toBe("*.aliyuncs.com");
|
||||
expect(r.httpProxy).toBeUndefined();
|
||||
expect(r.httpsProxy).toBeUndefined();
|
||||
});
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
type GlobalFlags,
|
||||
getModels,
|
||||
type IntentProfile,
|
||||
isInteractive,
|
||||
type PipelineStep,
|
||||
type RecommendedModel,
|
||||
type RecommendResult,
|
||||
@@ -19,7 +18,6 @@ import boxen from "boxen";
|
||||
import chalk, { Chalk, type ChalkInstance } from "chalk";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { createSpinner } from "bailian-cli-runtime";
|
||||
import { failIfMissing, promptText, cmdUsage } from "bailian-cli-runtime";
|
||||
|
||||
function formatContextWindow(tokens: number): string {
|
||||
if (tokens >= 1_000_000)
|
||||
@@ -218,19 +216,12 @@ export default defineCommand({
|
||||
description:
|
||||
"Recommend the best models for your use case (intent analysis → candidate recall → LLM ranking)",
|
||||
auth: "apiKey",
|
||||
usageArgs: "<prompt> [flags]",
|
||||
usageArgs: "--message <text> [flags]",
|
||||
options: [
|
||||
{
|
||||
flag: "--message <text>",
|
||||
description: "Describe your requirements (alternative to positional prompt)",
|
||||
},
|
||||
{
|
||||
flag: "--dry-run",
|
||||
description: "Show intent analysis and candidate list without LLM ranking",
|
||||
},
|
||||
{
|
||||
flag: "--output <format>",
|
||||
description: "Output format: text (default in TTY), json, yaml",
|
||||
description: "Describe your requirements",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
exampleArgs: [
|
||||
@@ -239,24 +230,9 @@ export default defineCommand({
|
||||
'--message "Legal contract review, high precision required"',
|
||||
'--message "Low-cost high-concurrency online customer service" --output json',
|
||||
'--message "Long document summarization" --dry-run',
|
||||
" # Interactive input",
|
||||
],
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
const positional = ((flags as Record<string, unknown>)._positional as string[]) ?? [];
|
||||
let userInput = (flags.message as string) || positional.join(" ");
|
||||
|
||||
if (!userInput.trim()) {
|
||||
if (isInteractive({ nonInteractive: config.nonInteractive })) {
|
||||
const hint = await promptText({ message: "Describe your requirement:" });
|
||||
if (!hint) {
|
||||
process.stderr.write("Cancelled.\n");
|
||||
process.exit(1);
|
||||
}
|
||||
userInput = hint;
|
||||
} else {
|
||||
failIfMissing("message", cmdUsage(config, '"your requirement"'));
|
||||
}
|
||||
}
|
||||
const userInput = flags.message as string;
|
||||
|
||||
const top = 3;
|
||||
const format = detectOutputFormat(config.output);
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
type AppStreamChunk,
|
||||
type AppCompletionResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { failIfMissing, cmdUsage } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
export default defineCommand({
|
||||
@@ -44,10 +43,7 @@ export default defineCommand({
|
||||
],
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
const appId = flags.appId as string;
|
||||
if (!appId) failIfMissing("app-id", cmdUsage(config, "--app-id <id> --prompt <text>"));
|
||||
|
||||
const prompt = flags.prompt as string;
|
||||
if (!prompt) failIfMissing("prompt", cmdUsage(config, "--app-id <id> --prompt <text>"));
|
||||
|
||||
const shouldStream =
|
||||
flags.stream === true || (flags.stream === undefined && process.stdout.isTTY);
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import {
|
||||
defineCommand,
|
||||
isInteractive,
|
||||
maskToken,
|
||||
readConfigFile,
|
||||
writeConfigFile,
|
||||
type Config,
|
||||
type GlobalFlags,
|
||||
} from "bailian-cli-core";
|
||||
import { IncompleteCommandError } from "bailian-cli-core";
|
||||
import { printQuickStart } from "bailian-cli-runtime";
|
||||
import { emitBare } from "bailian-cli-runtime";
|
||||
import { promptConfirm } from "bailian-cli-runtime";
|
||||
import { printCurrentCommandHelp } from "bailian-cli-runtime";
|
||||
import {
|
||||
resolveConsoleOrigin,
|
||||
runConsoleLogin,
|
||||
@@ -51,25 +48,12 @@ export default defineCommand({
|
||||
|
||||
const envKey = process.env.DASHSCOPE_API_KEY;
|
||||
if (envKey && !flags.apiKey) {
|
||||
const maskedEnvKey = maskToken(envKey);
|
||||
if (isInteractive({ nonInteractive: config.nonInteractive })) {
|
||||
const proceed = await promptConfirm({
|
||||
message: `Detected DASHSCOPE_API_KEY in environment (${maskedEnvKey}).\nYou are already authenticated via env.\nDo you still want to configure local persistent credentials?`,
|
||||
initialValue: false,
|
||||
});
|
||||
if (!proceed) {
|
||||
process.stdout.write("Login skipped. Using environment variables.\n");
|
||||
process.exit(0);
|
||||
}
|
||||
} else {
|
||||
process.stderr.write(`Warning: DASHSCOPE_API_KEY is already set in environment.\n`);
|
||||
}
|
||||
process.stderr.write(`Warning: DASHSCOPE_API_KEY is already set in environment.\n`);
|
||||
}
|
||||
|
||||
const key = (flags.apiKey as string) || config.apiKey;
|
||||
if (!key) {
|
||||
printCurrentCommandHelp(process.stderr);
|
||||
process.exit(0);
|
||||
throw new IncompleteCommandError("Missing required argument: --api-key");
|
||||
}
|
||||
|
||||
const baseUrl = (flags.baseUrl as string) || undefined;
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
type GlobalFlags,
|
||||
ExitCode,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, cmdUsage } from "bailian-cli-runtime";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
|
||||
const VALID_KEYS = [
|
||||
"base_url",
|
||||
@@ -58,8 +58,9 @@ export default defineCommand({
|
||||
flag: "--key <key>",
|
||||
description:
|
||||
"Config key (base_url, output, output_dir, timeout, api_key, access_token, default_*_model, access_key_id, access_key_secret, workspace_id)",
|
||||
required: true,
|
||||
},
|
||||
{ flag: "--value <value>", description: "Value to set" },
|
||||
{ flag: "--value <value>", description: "Value to set", required: true },
|
||||
],
|
||||
exampleArgs: [
|
||||
"--key output --value json",
|
||||
@@ -67,16 +68,8 @@ export default defineCommand({
|
||||
"--key base_url --value https://dashscope.aliyuncs.com",
|
||||
],
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
const key = flags.key as string | undefined;
|
||||
const value = flags.value as string | undefined;
|
||||
|
||||
if (!key || value === undefined) {
|
||||
throw new BailianError(
|
||||
"--key and --value are required.",
|
||||
ExitCode.USAGE,
|
||||
cmdUsage(config, "--key <key> --value <value>"),
|
||||
);
|
||||
}
|
||||
const key = flags.key as string;
|
||||
const value = flags.value as string;
|
||||
|
||||
// Resolve hyphen aliases to underscore keys
|
||||
const resolvedKey: string = KEY_ALIASES[key] || key;
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
type Config,
|
||||
type GlobalFlags,
|
||||
} from "bailian-cli-core";
|
||||
import { failIfMissing, cmdUsage } from "bailian-cli-runtime";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
|
||||
export default defineCommand({
|
||||
@@ -44,10 +43,7 @@ export default defineCommand({
|
||||
],
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
const api = flags.api as string;
|
||||
if (!api) failIfMissing("api", cmdUsage(config, "--api <api> --data <json>"));
|
||||
|
||||
const dataRaw = flags.data as string;
|
||||
if (!dataRaw) failIfMissing("data", cmdUsage(config, "--api <api> --data <json>"));
|
||||
|
||||
let data: Record<string, unknown>;
|
||||
try {
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
type GlobalFlags,
|
||||
uploadFile,
|
||||
} from "bailian-cli-core";
|
||||
import { failIfMissing, cmdUsage } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
export default defineCommand({
|
||||
@@ -32,15 +31,8 @@ export default defineCommand({
|
||||
"--file cat.png --model qwen-image-2.0",
|
||||
],
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
const filePath = flags.file as string | undefined;
|
||||
if (!filePath) {
|
||||
failIfMissing("file", cmdUsage(config, "--file <path> --model <model>"));
|
||||
}
|
||||
|
||||
const model = flags.model as string | undefined;
|
||||
if (!model) {
|
||||
failIfMissing("model", cmdUsage(config, "--file <path> --model <model>"));
|
||||
}
|
||||
const filePath = flags.file as string;
|
||||
const model = flags.model as string;
|
||||
|
||||
const format = detectOutputFormat(config.output);
|
||||
|
||||
@@ -54,8 +46,8 @@ export default defineCommand({
|
||||
|
||||
const ossUrl = await uploadFile({
|
||||
apiKey: credential.token,
|
||||
model: model!,
|
||||
filePath: filePath!,
|
||||
model,
|
||||
filePath,
|
||||
});
|
||||
|
||||
if (config.quiet) {
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
resolveFileUrl,
|
||||
resolveOutputDir,
|
||||
generateFilename,
|
||||
isInteractive,
|
||||
stripUndefined,
|
||||
type DashScopeImageRequest,
|
||||
type DashScopeImageSyncResponse,
|
||||
@@ -20,7 +19,6 @@ import {
|
||||
} from "bailian-cli-core";
|
||||
import { downloadFile } from "bailian-cli-runtime";
|
||||
import { runConcurrent, downloadParallel, getConcurrency } from "bailian-cli-runtime";
|
||||
import { promptText, failIfMissing, cmdUsage } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { resolveImageSize } from "bailian-cli-runtime";
|
||||
import { join } from "path";
|
||||
@@ -52,10 +50,12 @@ export default defineCommand({
|
||||
{
|
||||
flag: "--prompt-extend <bool>",
|
||||
description: BOOL_FLAG_PROMPT_EXTEND_CLI_TRUE,
|
||||
type: "boolean",
|
||||
},
|
||||
{
|
||||
flag: "--watermark <bool>",
|
||||
description: BOOL_FLAG_WATERMARK,
|
||||
type: "boolean",
|
||||
},
|
||||
{ flag: "--out-dir <dir>", description: "Download images to directory" },
|
||||
{ flag: "--out-prefix <prefix>", description: "Filename prefix (default: edited)" },
|
||||
@@ -75,25 +75,7 @@ export default defineCommand({
|
||||
} else if (typeof flags.image === "string") {
|
||||
rawImages = [flags.image];
|
||||
}
|
||||
if (rawImages.length === 0) {
|
||||
failIfMissing("image", cmdUsage(config, "--image <url> --prompt <text>"));
|
||||
}
|
||||
|
||||
let prompt = flags.prompt as string | undefined;
|
||||
if (!prompt) {
|
||||
if (isInteractive({ nonInteractive: config.nonInteractive })) {
|
||||
const hint = await promptText({
|
||||
message: "Enter your edit instruction:",
|
||||
});
|
||||
if (!hint) {
|
||||
process.stderr.write("Image editing cancelled.\n");
|
||||
process.exit(1);
|
||||
}
|
||||
prompt = hint;
|
||||
} else {
|
||||
failIfMissing("prompt", cmdUsage(config, "--image <url> --prompt <text>"));
|
||||
}
|
||||
}
|
||||
const prompt = flags.prompt as string;
|
||||
|
||||
const model = (flags.model as string) || config.defaultImageModel || "qwen-image-2.0";
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
type Config,
|
||||
type GlobalFlags,
|
||||
resolveOutputDir,
|
||||
isInteractive,
|
||||
type DashScopeImageRequest,
|
||||
type DashScopeImageSyncResponse,
|
||||
BailianError,
|
||||
@@ -23,7 +22,6 @@ import {
|
||||
import { poll } from "bailian-cli-runtime";
|
||||
import { downloadFile } from "bailian-cli-runtime";
|
||||
import { runConcurrent, downloadParallel, getConcurrency } from "bailian-cli-runtime";
|
||||
import { promptText, failIfMissing, cmdUsage } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { resolveImageSize } from "bailian-cli-runtime";
|
||||
import { BOOL_FLAG_PROMPT_EXTEND_IMAGE_GENERATE, BOOL_FLAG_WATERMARK } from "bailian-cli-runtime";
|
||||
@@ -61,10 +59,12 @@ export default defineCommand({
|
||||
{
|
||||
flag: "--prompt-extend <bool>",
|
||||
description: BOOL_FLAG_PROMPT_EXTEND_IMAGE_GENERATE,
|
||||
type: "boolean",
|
||||
},
|
||||
{
|
||||
flag: "--watermark <bool>",
|
||||
description: BOOL_FLAG_WATERMARK,
|
||||
type: "boolean",
|
||||
},
|
||||
{
|
||||
flag: "--no-wait",
|
||||
@@ -90,24 +90,7 @@ export default defineCommand({
|
||||
'--prompt "Product shots" --n 2 --concurrent 3 # 6 images in parallel',
|
||||
],
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
let prompt = (flags.prompt ?? (flags._positional as string[] | undefined)?.[0]) as
|
||||
| string
|
||||
| undefined;
|
||||
|
||||
if (!prompt) {
|
||||
if (isInteractive({ nonInteractive: config.nonInteractive })) {
|
||||
const hint = await promptText({
|
||||
message: "Enter your image prompt:",
|
||||
});
|
||||
if (!hint) {
|
||||
process.stderr.write("Image generation cancelled.\n");
|
||||
process.exit(1);
|
||||
}
|
||||
prompt = hint;
|
||||
} else {
|
||||
failIfMissing("prompt", cmdUsage(config, "--prompt <text>"));
|
||||
}
|
||||
}
|
||||
const prompt = flags.prompt as string;
|
||||
|
||||
const model = (flags.model as string) || config.defaultImageModel || "qwen-image-2.0";
|
||||
const useSync = isSyncModel(model);
|
||||
@@ -281,8 +264,7 @@ async function saveImages(
|
||||
subDir: flags.outDir ? undefined : "images",
|
||||
});
|
||||
|
||||
const promptText =
|
||||
(flags.prompt as string) || (flags._positional as string[] | undefined)?.[0] || "";
|
||||
const promptText = (flags.prompt as string) || "";
|
||||
const prefix = (flags.outPrefix as string) || generateFilename("image", promptText);
|
||||
|
||||
// Parallel download all images
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
BailianError,
|
||||
ExitCode,
|
||||
} from "bailian-cli-core";
|
||||
import { failIfMissing, cmdUsage } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
const BAILIAN_HOST = "bailian.cn-beijing.aliyuncs.com";
|
||||
@@ -81,10 +80,7 @@ export default defineCommand({
|
||||
],
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
const indexId = flags.indexId as string;
|
||||
if (!indexId) failIfMissing("index-id", cmdUsage(config, "--index-id <id> --query <text>"));
|
||||
|
||||
const query = flags.query as string;
|
||||
if (!query) failIfMissing("query", cmdUsage(config, "--index-id <id> --query <text>"));
|
||||
|
||||
const format = detectOutputFormat(config.output);
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
type Config,
|
||||
type GlobalFlags,
|
||||
} from "bailian-cli-core";
|
||||
import { failIfMissing, cmdUsage } from "bailian-cli-runtime";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
import { ensureApiKey } from "bailian-cli-runtime";
|
||||
|
||||
@@ -32,10 +31,10 @@ function parseArgFlags(raw: string[]): Record<string, unknown> {
|
||||
export default defineCommand({
|
||||
description: "Call a tool on an MCP server (tools/call)",
|
||||
auth: "apiKey",
|
||||
usageArgs: "<server-code>.<tool> [--arg k=v ...] [--json '{...}'] [--url <url>]",
|
||||
usageArgs: "--target <server.tool> [--arg k=v ...] [--json '{...}'] [--url <url>]",
|
||||
options: [
|
||||
{
|
||||
flag: "<server-code>.<tool>",
|
||||
flag: "--target <server.tool>",
|
||||
description:
|
||||
"Server code and tool name joined by a dot, e.g. market-cmapi00073529.SmartStockSelection",
|
||||
required: true,
|
||||
@@ -56,23 +55,20 @@ export default defineCommand({
|
||||
{ flag: "--url <url>", description: "Override the MCP endpoint URL (for non-Bailian servers)" },
|
||||
],
|
||||
exampleArgs: [
|
||||
'market-cmapi00073529.SmartStockSelection --query "Screen consumer stocks with ROE > 15%"',
|
||||
'market-cmapi00073529.FinQuery --json \'{"q":"Guizhou Maotai","limit":5}\'',
|
||||
"market-cmapi00073529.SmartFundSelection --arg riskLevel=R3 --arg minScale=10",
|
||||
'--target market-cmapi00073529.SmartStockSelection --query "Screen consumer stocks with ROE > 15%"',
|
||||
'--target market-cmapi00073529.FinQuery --json \'{"q":"Guizhou Maotai","limit":5}\'',
|
||||
"--target market-cmapi00073529.SmartFundSelection --arg riskLevel=R3 --arg minScale=10",
|
||||
],
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
const positional =
|
||||
((flags as Record<string, unknown>)._positional as string[] | undefined) ?? [];
|
||||
const target = positional[0];
|
||||
if (!target) failIfMissing("<server-code>.<tool>", cmdUsage(config, "<server-code>.<tool>"));
|
||||
const target = flags.target as string;
|
||||
|
||||
const dot = target!.indexOf(".");
|
||||
if (dot <= 0 || dot === target!.length - 1) {
|
||||
const dot = target.indexOf(".");
|
||||
if (dot <= 0 || dot === target.length - 1) {
|
||||
process.stderr.write(`Error: target must be <server-code>.<tool>, got "${target}".\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
const serverCode = target!.slice(0, dot);
|
||||
const toolName = target!.slice(dot + 1);
|
||||
const serverCode = target.slice(0, dot);
|
||||
const toolName = target.slice(dot + 1);
|
||||
|
||||
let toolArgs: Record<string, unknown> = {};
|
||||
if (flags.json) {
|
||||
|
||||
@@ -6,34 +6,30 @@ import {
|
||||
type Config,
|
||||
type GlobalFlags,
|
||||
} from "bailian-cli-core";
|
||||
import { failIfMissing, cmdUsage } from "bailian-cli-runtime";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
import { ensureApiKey } from "bailian-cli-runtime";
|
||||
|
||||
export default defineCommand({
|
||||
description: "List tools exposed by an MCP server (tools/list)",
|
||||
auth: "apiKey",
|
||||
usageArgs: "<server-code> [--url <url>]",
|
||||
usageArgs: "--server <code> [--url <url>]",
|
||||
options: [
|
||||
{
|
||||
flag: "<server-code>",
|
||||
flag: "--server <code>",
|
||||
description: "Server code from `mcp list` (e.g. market-cmapi00073529)",
|
||||
required: true,
|
||||
},
|
||||
{ flag: "--url <url>", description: "Override the MCP endpoint URL (for non-Bailian servers)" },
|
||||
],
|
||||
exampleArgs: [
|
||||
"market-cmapi00073529",
|
||||
"market-cmapi00073529 --output json",
|
||||
"my-server --url https://example.com/mcp",
|
||||
"--server market-cmapi00073529",
|
||||
"--server market-cmapi00073529 --output json",
|
||||
"--server my-server --url https://example.com/mcp",
|
||||
],
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
const positional =
|
||||
((flags as Record<string, unknown>)._positional as string[] | undefined) ?? [];
|
||||
const code = positional[0];
|
||||
if (!code) failIfMissing("server-code", cmdUsage(config, "<server-code>"));
|
||||
const code = flags.server as string;
|
||||
|
||||
const url = (flags.url as string) || bailianMcpUrl(config.baseUrl, code!);
|
||||
const url = (flags.url as string) || bailianMcpUrl(config.baseUrl, code);
|
||||
const format = detectOutputFormat(config.output);
|
||||
|
||||
if (config.dryRun) {
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
type MemoryAddRequest,
|
||||
type MemoryAddResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { failIfMissing, cmdUsage } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
export default defineCommand({
|
||||
@@ -30,9 +29,9 @@ export default defineCommand({
|
||||
'--user-id user1 --messages \'[{"role":"user","content":"I like traveling"}]\'',
|
||||
'--user-id user1 --content "Lives in Beijing" --profile-schema schema_xxx',
|
||||
],
|
||||
validate: (f) => (!f.messages && !f.content ? "Provide --messages or --content." : undefined),
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
const userId = flags.userId as string;
|
||||
if (!userId) failIfMissing("user-id", cmdUsage(config, "--user-id <id>"));
|
||||
|
||||
const body: MemoryAddRequest = { user_id: userId };
|
||||
|
||||
@@ -49,11 +48,6 @@ export default defineCommand({
|
||||
body.custom_content = flags.content as string;
|
||||
}
|
||||
|
||||
if (!body.messages && !body.custom_content) {
|
||||
process.stderr.write("Error: at least one of --messages or --content is required\n");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (flags.profileSchema) body.profile_schema = flags.profileSchema as string;
|
||||
if (flags.memoryLibraryId) body.memory_library_id = flags.memoryLibraryId as string;
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
type Config,
|
||||
type GlobalFlags,
|
||||
} from "bailian-cli-core";
|
||||
import { failIfMissing, cmdUsage } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
export default defineCommand({
|
||||
@@ -21,10 +20,7 @@ export default defineCommand({
|
||||
exampleArgs: ["--node-id node_xxx --user-id user1"],
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
const nodeId = flags.nodeId as string;
|
||||
if (!nodeId) failIfMissing("node-id", cmdUsage(config, "--node-id <id> --user-id <id>"));
|
||||
|
||||
const userId = flags.userId as string;
|
||||
if (!userId) failIfMissing("user-id", cmdUsage(config, "--node-id <id> --user-id <id>"));
|
||||
|
||||
const format = detectOutputFormat(config.output);
|
||||
const params = new URLSearchParams({ user_id: userId });
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
type GlobalFlags,
|
||||
type MemoryNodeListResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { failIfMissing, cmdUsage } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
export default defineCommand({
|
||||
@@ -23,7 +22,6 @@ export default defineCommand({
|
||||
exampleArgs: ["--user-id user1", "--user-id user1 --page-size 20 --page 2"],
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
const userId = flags.userId as string;
|
||||
if (!userId) failIfMissing("user-id", cmdUsage(config, "--user-id <id>"));
|
||||
|
||||
const format = detectOutputFormat(config.output);
|
||||
const params = new URLSearchParams();
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
type ProfileSchemaCreateRequest,
|
||||
type ProfileSchemaCreateResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { failIfMissing, cmdUsage } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
export default defineCommand({
|
||||
@@ -29,11 +28,7 @@ export default defineCommand({
|
||||
],
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
const name = flags.name as string;
|
||||
if (!name) failIfMissing("name", cmdUsage(config, "--name <name> --attributes <json>"));
|
||||
|
||||
const attrStr = flags.attributes as string;
|
||||
if (!attrStr)
|
||||
failIfMissing("attributes", cmdUsage(config, "--name <name> --attributes <json>"));
|
||||
|
||||
let attributes;
|
||||
try {
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
type GlobalFlags,
|
||||
type UserProfileResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { failIfMissing, cmdUsage } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
export default defineCommand({
|
||||
@@ -21,10 +20,7 @@ export default defineCommand({
|
||||
exampleArgs: ["--schema-id schema_xxx --user-id user1"],
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
const schemaId = flags.schemaId as string;
|
||||
if (!schemaId) failIfMissing("schema-id", cmdUsage(config, "--schema-id <id> --user-id <id>"));
|
||||
|
||||
const userId = flags.userId as string;
|
||||
if (!userId) failIfMissing("user-id", cmdUsage(config, "--schema-id <id> --user-id <id>"));
|
||||
|
||||
const format = detectOutputFormat(config.output);
|
||||
const params = new URLSearchParams({ user_id: userId });
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
type MemorySearchRequest,
|
||||
type MemorySearchResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { failIfMissing, cmdUsage } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
export default defineCommand({
|
||||
@@ -30,9 +29,9 @@ export default defineCommand({
|
||||
'--user-id user1 --query "programming preferences"',
|
||||
'--user-id user1 --messages \'[{"role":"user","content":"recommend a book"}]\' --top-k 5',
|
||||
],
|
||||
validate: (f) => (!f.query && !f.messages ? "Provide --query or --messages." : undefined),
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
const userId = flags.userId as string;
|
||||
if (!userId) failIfMissing("user-id", cmdUsage(config, "--user-id <id>"));
|
||||
|
||||
const body: MemorySearchRequest = { user_id: userId };
|
||||
|
||||
@@ -52,11 +51,6 @@ export default defineCommand({
|
||||
body.messages = [{ role: "user", content: body.query }];
|
||||
}
|
||||
|
||||
if (!body.query && !body.messages) {
|
||||
process.stderr.write("Error: at least one of --query or --messages is required\n");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (flags.topK !== undefined) body.top_k = flags.topK as number;
|
||||
if (flags.memoryLibraryId) body.memory_library_id = flags.memoryLibraryId as string;
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
type GlobalFlags,
|
||||
type MemoryNodeUpdateRequest,
|
||||
} from "bailian-cli-core";
|
||||
import { failIfMissing, cmdUsage } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
export default defineCommand({
|
||||
@@ -27,16 +26,8 @@ export default defineCommand({
|
||||
exampleArgs: ['--node-id node_xxx --user-id user1 --content "updated memory content"'],
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
const nodeId = flags.nodeId as string;
|
||||
if (!nodeId)
|
||||
failIfMissing("node-id", cmdUsage(config, "--node-id <id> --user-id <id> --content <text>"));
|
||||
|
||||
const userId = flags.userId as string;
|
||||
if (!userId)
|
||||
failIfMissing("user-id", cmdUsage(config, "--node-id <id> --user-id <id> --content <text>"));
|
||||
|
||||
const content = flags.content as string;
|
||||
if (!content)
|
||||
failIfMissing("content", cmdUsage(config, "--node-id <id> --user-id <id> --content <text>"));
|
||||
|
||||
const body: MemoryNodeUpdateRequest = {
|
||||
user_id: userId,
|
||||
|
||||
@@ -14,10 +14,8 @@ import {
|
||||
type ChatMessageContent,
|
||||
type ChatRequest,
|
||||
type StreamChunk,
|
||||
isInteractive,
|
||||
resolveFileUrl,
|
||||
} from "bailian-cli-core";
|
||||
import { promptText, failIfMissing, cmdUsage } from "bailian-cli-runtime";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
import { resolveOutputDir, resolveCredential } from "bailian-cli-core";
|
||||
|
||||
@@ -130,23 +128,7 @@ export default defineCommand({
|
||||
],
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
// --- Parse messages ---
|
||||
let userMessages: string[] = [];
|
||||
if (flags.message) {
|
||||
userMessages = flags.message as string[];
|
||||
}
|
||||
|
||||
if (userMessages.length === 0) {
|
||||
if (isInteractive({ nonInteractive: config.nonInteractive })) {
|
||||
const hint = await promptText({ message: "Enter your message:" });
|
||||
if (!hint) {
|
||||
process.stderr.write("Omni chat cancelled.\n");
|
||||
process.exit(1);
|
||||
}
|
||||
userMessages = [hint];
|
||||
} else {
|
||||
failIfMissing("message", cmdUsage(config, "--message <text>"));
|
||||
}
|
||||
}
|
||||
const userMessages = flags.message as string[];
|
||||
|
||||
const model = (flags.model as string) || config.defaultOmniModel || "qwen3.5-omni-plus";
|
||||
const voice = (flags.voice as string) || "Cherry";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { defineCommand, type Config, type GlobalFlags } from "bailian-cli-core";
|
||||
import { emitResult, cmdUsage } from "bailian-cli-runtime";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
import { initPipelineSteps } from "bailian-cli-runtime";
|
||||
import { executePipeline, streamPipelineEvents } from "bailian-cli-runtime";
|
||||
import type { PipelineLifecycleEvent } from "bailian-cli-runtime";
|
||||
@@ -10,8 +10,9 @@ import { loadPipelineFile } from "./load-file.ts";
|
||||
export default defineCommand({
|
||||
description: "Run a pipeline workflow definition",
|
||||
auth: "none",
|
||||
usageArgs: "<file> [flags]",
|
||||
usageArgs: "--file <path> [flags]",
|
||||
options: [
|
||||
{ flag: "--file <path>", description: "Pipeline definition file (YAML/JSON)", required: true },
|
||||
{ flag: "--input <json>", description: "Runtime input as inline JSON" },
|
||||
{ flag: "--input-file <path>", description: "Runtime input from a JSON file" },
|
||||
{
|
||||
@@ -27,20 +28,14 @@ export default defineCommand({
|
||||
},
|
||||
],
|
||||
exampleArgs: [
|
||||
'workflow.yaml --input \'{"brief":"hello"}\'',
|
||||
"workflow.json --input-file inputs.json --concurrency 3",
|
||||
"workflow.yaml --dry-run",
|
||||
"workflow.json --events jsonl",
|
||||
"workflow.yaml --output json",
|
||||
'--file workflow.yaml --input \'{"brief":"hello"}\'',
|
||||
"--file workflow.json --input-file inputs.json --concurrency 3",
|
||||
"--file workflow.yaml --dry-run",
|
||||
"--file workflow.json --events jsonl",
|
||||
"--file workflow.yaml --output json",
|
||||
],
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
const file = ((flags._positional as string[] | undefined) ?? [])[0] as string | undefined;
|
||||
if (!file) {
|
||||
process.stderr.write(
|
||||
`Error: pipeline file is required\nUsage: ${cmdUsage(config, "<file>")}\n`,
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
const file = flags.file as string;
|
||||
|
||||
initPipelineSteps();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { resolve } from "node:path";
|
||||
import { defineCommand, type Config, type GlobalFlags } from "bailian-cli-core";
|
||||
import { emitResult, cmdUsage } from "bailian-cli-runtime";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
import { initPipelineSteps } from "bailian-cli-runtime";
|
||||
import { collectPipelineIssues, collectPipelineHints } from "bailian-cli-runtime";
|
||||
import { loadPipelineFile } from "./load-file.ts";
|
||||
@@ -8,17 +8,13 @@ import { loadPipelineFile } from "./load-file.ts";
|
||||
export default defineCommand({
|
||||
description: "Validate a pipeline definition without executing",
|
||||
auth: "none",
|
||||
usageArgs: "<file>",
|
||||
options: [],
|
||||
exampleArgs: ["workflow.yaml", "workflow.json --output json"],
|
||||
usageArgs: "--file <path>",
|
||||
options: [
|
||||
{ flag: "--file <path>", description: "Pipeline definition file (YAML/JSON)", required: true },
|
||||
],
|
||||
exampleArgs: ["--file workflow.yaml", "--file workflow.json --output json"],
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
const file = ((flags._positional as string[] | undefined) ?? [])[0] as string | undefined;
|
||||
if (!file) {
|
||||
process.stderr.write(
|
||||
`Error: pipeline file is required\nUsage: ${cmdUsage(config, "<file>")}\n`,
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
const file = flags.file as string;
|
||||
|
||||
initPipelineSteps();
|
||||
|
||||
|
||||
@@ -4,11 +4,9 @@ import {
|
||||
mcpWebSearchEndpoint,
|
||||
type Config,
|
||||
type GlobalFlags,
|
||||
isInteractive,
|
||||
McpClient,
|
||||
} from "bailian-cli-core";
|
||||
import { createSpinner } from "bailian-cli-runtime";
|
||||
import { promptText, failIfMissing, cmdUsage } from "bailian-cli-runtime";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
|
||||
export default defineCommand({
|
||||
@@ -16,7 +14,7 @@ export default defineCommand({
|
||||
auth: "apiKey",
|
||||
usageArgs: "--query <text> [flags]",
|
||||
options: [
|
||||
{ flag: "--query <text>", description: "Search query text", required: true },
|
||||
{ flag: "--query <text>", description: "Search query text" },
|
||||
{ flag: "--count <n>", description: "Number of search results (default: 10)", type: "number" },
|
||||
{ flag: "--list-tools", description: "List available MCP tools and exit" },
|
||||
],
|
||||
@@ -26,6 +24,7 @@ export default defineCommand({
|
||||
'--query "Today\'s news"',
|
||||
"--list-tools",
|
||||
],
|
||||
validate: (f) => (!f.listTools && !f.query ? "Missing required flag: --query" : undefined),
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
const mcpUrl = mcpWebSearchEndpoint(config.baseUrl);
|
||||
const format = detectOutputFormat(config.output);
|
||||
@@ -46,19 +45,7 @@ export default defineCommand({
|
||||
}
|
||||
|
||||
// --- Search mode ---
|
||||
let query = flags.query as string | undefined;
|
||||
if (!query) {
|
||||
if (isInteractive({ nonInteractive: config.nonInteractive })) {
|
||||
const hint = await promptText({ message: "Enter your search query:" });
|
||||
if (!hint) {
|
||||
process.stderr.write("Search cancelled.\n");
|
||||
process.exit(1);
|
||||
}
|
||||
query = hint;
|
||||
} else {
|
||||
failIfMissing("query", cmdUsage(config, "--query <text>"));
|
||||
}
|
||||
}
|
||||
const query = flags.query as string;
|
||||
|
||||
if (config.dryRun) {
|
||||
emitResult(
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
speechRecognizeEndpoint,
|
||||
} from "bailian-cli-core";
|
||||
import { poll } from "bailian-cli-runtime";
|
||||
import { failIfMissing, cmdUsage } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
export default defineCommand({
|
||||
@@ -68,9 +67,6 @@ export default defineCommand({
|
||||
} else if (typeof flags.url === "string") {
|
||||
rawUrls = [flags.url];
|
||||
}
|
||||
if (rawUrls.length === 0) {
|
||||
failIfMissing("url", cmdUsage(config, "--url <audio-url>"));
|
||||
}
|
||||
|
||||
// Strict validation: --speaker-count requires --diarization
|
||||
const speakerCount = flags.speakerCount as number | undefined;
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
type OutputFormat,
|
||||
speechSynthesizeEndpoint,
|
||||
parseSSE,
|
||||
isInteractive,
|
||||
IncompleteCommandError,
|
||||
resolveOutputDir,
|
||||
request,
|
||||
DOCS_HOSTS,
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
const COSYVOICE_CLONE_DESIGN_DOC = `${DOCS_HOSTS.cn}/cosyvoice-clone-design-api`;
|
||||
import { downloadFile } from "bailian-cli-runtime";
|
||||
import { runConcurrent, downloadParallel, getConcurrency } from "bailian-cli-runtime";
|
||||
import { promptText, promptSelect, failIfMissing, cmdUsage } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
interface VoiceEntry {
|
||||
@@ -146,7 +145,7 @@ export default defineCommand({
|
||||
auth: "apiKey",
|
||||
usageArgs: "--text <text> [flags]",
|
||||
options: [
|
||||
{ flag: "--text <text>", description: "Text to synthesize into speech", required: true },
|
||||
{ flag: "--text <text>", description: "Text to synthesize into speech (or use --text-file)" },
|
||||
{ flag: "--text-file <path>", description: "Read text from a file instead of --text" },
|
||||
{
|
||||
flag: "--model <model>",
|
||||
@@ -193,6 +192,12 @@ export default defineCommand({
|
||||
"# Pipe to ffplay",
|
||||
'--text "Hello" --voice <voice_id> --stream | ffplay -nodisp -autoexit -f s16le -ar 24000 -ac 1 -',
|
||||
],
|
||||
validate: (f) => {
|
||||
if (f.listVoices) return undefined;
|
||||
if (!f.text && !f.textFile) return "Provide --text or --text-file.";
|
||||
if (!f.voice) return "Missing required flag: --voice";
|
||||
return undefined;
|
||||
},
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
const model = (flags.model as string) || config.defaultSpeechModel || "cosyvoice-v3-flash";
|
||||
|
||||
@@ -215,66 +220,9 @@ export default defineCommand({
|
||||
}
|
||||
|
||||
if (!text) {
|
||||
if (isInteractive({ nonInteractive: config.nonInteractive })) {
|
||||
const hint = await promptText({ message: "Enter text to synthesize:" });
|
||||
if (!hint) {
|
||||
process.stderr.write("Speech synthesis cancelled.\n");
|
||||
process.exit(1);
|
||||
}
|
||||
text = hint;
|
||||
} else {
|
||||
failIfMissing("text", cmdUsage(config, "--text <text>"));
|
||||
}
|
||||
}
|
||||
|
||||
let voice = (flags.voice as string) || undefined;
|
||||
|
||||
// In interactive mode, prompt the user to select / enter a voice
|
||||
if (!voice) {
|
||||
if (isInteractive({ nonInteractive: config.nonInteractive })) {
|
||||
const modelVoices = MODEL_VOICES[model];
|
||||
if (modelVoices && modelVoices.length > 0) {
|
||||
const DEFAULT_VOICE = modelVoices[0]!.voice;
|
||||
const choices = modelVoices.map((v) => ({
|
||||
value: v.voice,
|
||||
label: `${v.name} (${v.voice})`,
|
||||
hint: `${v.desc} · ${v.lang}`,
|
||||
}));
|
||||
const selected = await promptSelect({
|
||||
message: `Select a voice (default: ${DEFAULT_VOICE}):`,
|
||||
choices,
|
||||
defaultValue: DEFAULT_VOICE,
|
||||
});
|
||||
if (!selected) {
|
||||
process.stderr.write("Speech synthesis cancelled.\n");
|
||||
process.exit(1);
|
||||
}
|
||||
voice = selected;
|
||||
} else {
|
||||
// No built-in list (v3.5 / v2): prompt for clone/design voice ID
|
||||
const entered = await promptText({ message: "Enter voice ID (clone/design voice):" });
|
||||
if (!entered) {
|
||||
process.stderr.write("Speech synthesis cancelled.\n");
|
||||
process.exit(1);
|
||||
}
|
||||
voice = entered;
|
||||
}
|
||||
} else {
|
||||
// Non-interactive mode: keep original error
|
||||
const modelVoices = MODEL_VOICES[model];
|
||||
if (modelVoices && modelVoices.length > 0) {
|
||||
throw new BailianError(
|
||||
`--voice is required.\nRun the following to see available voices:\n ${cmdUsage(config, `--list-voices --model ${model}`)}`,
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
} else {
|
||||
throw new BailianError(
|
||||
`--voice is required. Model ${model} has no built-in system voices.\nCreate a clone or design voice first, then pass its ID via --voice <voice_id>.\nSee: ${COSYVOICE_CLONE_DESIGN_DOC}`,
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
}
|
||||
throw new IncompleteCommandError("Provide --text or --text-file.");
|
||||
}
|
||||
const voice = flags.voice as string;
|
||||
|
||||
const language = (flags.language as string) || undefined;
|
||||
const instruction = (flags.instruction as string) || undefined;
|
||||
|
||||
@@ -11,9 +11,7 @@ import {
|
||||
type ChatRequest,
|
||||
type ChatResponse,
|
||||
type StreamChunk,
|
||||
isInteractive,
|
||||
} from "bailian-cli-core";
|
||||
import { promptText, failIfMissing, cmdUsage } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { readFileSync } from "fs";
|
||||
|
||||
@@ -75,8 +73,7 @@ export default defineCommand({
|
||||
{ flag: "--model <model>", description: "Model ID (default: qwen3.7-max)" },
|
||||
{
|
||||
flag: "--message <text>",
|
||||
description: "Message text (repeatable, prefix role: to set role)",
|
||||
required: true,
|
||||
description: "Message text (repeatable, prefix role: to set role); or use --messages-file",
|
||||
type: "array",
|
||||
},
|
||||
{
|
||||
@@ -115,24 +112,10 @@ export default defineCommand({
|
||||
'--message "Hello" --output json',
|
||||
'--model qwq-plus --message "Solve 1+1" --enable-thinking',
|
||||
],
|
||||
validate: (f) =>
|
||||
!f.message && !f.messagesFile ? "Provide --message or --messages-file." : undefined,
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
const { system, messages: parsedMessages } = parseMessages(flags);
|
||||
let messages = parsedMessages;
|
||||
|
||||
if (messages.length === 0) {
|
||||
if (isInteractive({ nonInteractive: config.nonInteractive })) {
|
||||
const hint = await promptText({
|
||||
message: "Enter your message:",
|
||||
});
|
||||
if (!hint) {
|
||||
process.stderr.write("Chat cancelled.\n");
|
||||
process.exit(1);
|
||||
}
|
||||
messages = [{ role: "user", content: hint }];
|
||||
} else {
|
||||
failIfMissing("message", cmdUsage(config, "--message <text>"));
|
||||
}
|
||||
}
|
||||
const { system, messages } = parseMessages(flags);
|
||||
|
||||
const model = (flags.model as string) || config.defaultTextModel || "qwen3.7-max";
|
||||
const shouldStream =
|
||||
|
||||
@@ -140,19 +140,13 @@ export default defineCommand({
|
||||
"--off --model qwen3-max",
|
||||
"--off --all",
|
||||
],
|
||||
validate: (f) =>
|
||||
!f.model && !f.all ? "Provide --model <model>[,model2,...] or --all." : undefined,
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
const modelFlag = (flags.model as string) || undefined;
|
||||
const all = Boolean(flags.all);
|
||||
const off = Boolean(flags.off);
|
||||
const format = detectOutputFormat(config.output);
|
||||
|
||||
if (!modelFlag && !all) {
|
||||
process.stderr.write(
|
||||
"Error: missing required flag. Specify --model <model>[,model2,...] or --all\n",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let models: string[];
|
||||
if (modelFlag) {
|
||||
models = [
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
ExitCode,
|
||||
} from "bailian-cli-core";
|
||||
import { downloadFile, formatBytes } from "bailian-cli-runtime";
|
||||
import { failIfMissing, cmdUsage } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
export default defineCommand({
|
||||
@@ -18,19 +17,17 @@ export default defineCommand({
|
||||
auth: "none",
|
||||
usageArgs: "--task-id <id> --out <path>",
|
||||
options: [
|
||||
{ flag: "--task-id <id>", description: "Task ID to download from" },
|
||||
{ flag: "--out <path>", description: "Output file path" },
|
||||
{ flag: "--task-id <id>", description: "Task ID to download from", required: true },
|
||||
{ flag: "--out <path>", description: "Output file path", required: true },
|
||||
],
|
||||
exampleArgs: [
|
||||
"--task-id 3b256896-xxxx --out video.mp4",
|
||||
"--task-id 3b256896-xxxx --out video.mp4 --quiet",
|
||||
],
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
const taskId = flags.taskId as string | undefined;
|
||||
if (!taskId) failIfMissing("task-id", cmdUsage(config, "--task-id <id> --out <path>"));
|
||||
const taskId = flags.taskId as string;
|
||||
|
||||
const outPath = flags.out as string | undefined;
|
||||
if (!outPath) failIfMissing("out", cmdUsage(config, "--task-id <id> --out video.mp4"));
|
||||
const outPath = flags.out as string;
|
||||
|
||||
const format = detectOutputFormat(config.output);
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
type DashScopeVideoEditRequest,
|
||||
type DashScopeAsyncResponse,
|
||||
type DashScopeTaskResponse,
|
||||
isInteractive,
|
||||
resolveOutputDir,
|
||||
resolveFileUrl,
|
||||
resolveCredential,
|
||||
@@ -20,7 +19,6 @@ import {
|
||||
} from "bailian-cli-core";
|
||||
import { poll } from "bailian-cli-runtime";
|
||||
import { downloadFile, formatBytes } from "bailian-cli-runtime";
|
||||
import { promptText, failIfMissing, cmdUsage } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT, BOOL_FLAG_WATERMARK } from "bailian-cli-runtime";
|
||||
|
||||
@@ -59,10 +57,12 @@ export default defineCommand({
|
||||
{
|
||||
flag: "--prompt-extend <bool>",
|
||||
description: BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT,
|
||||
type: "boolean",
|
||||
},
|
||||
{
|
||||
flag: "--watermark <bool>",
|
||||
description: BOOL_FLAG_WATERMARK,
|
||||
type: "boolean",
|
||||
},
|
||||
{ flag: "--seed <n>", description: "Random seed for reproducible generation", type: "number" },
|
||||
{ flag: "--download <path>", description: "Save video to file on completion" },
|
||||
@@ -84,34 +84,10 @@ export default defineCommand({
|
||||
'--video https://example.com/input.mp4 --prompt "Put clothes on the kitten in the video" --watermark false',
|
||||
],
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
// --- Validate video URL ---
|
||||
let videoUrl = flags.video as string | undefined;
|
||||
if (!videoUrl) {
|
||||
if (isInteractive({ nonInteractive: config.nonInteractive })) {
|
||||
const hint = await promptText({ message: "Enter the video URL to edit:" });
|
||||
if (!hint) {
|
||||
process.stderr.write("Video editing cancelled.\n");
|
||||
process.exit(1);
|
||||
}
|
||||
videoUrl = hint;
|
||||
} else {
|
||||
failIfMissing("video", cmdUsage(config, "--video <url> --prompt <text>"));
|
||||
}
|
||||
}
|
||||
const videoUrl = flags.video as string;
|
||||
|
||||
// --- Prompt ---
|
||||
let prompt = flags.prompt as string | undefined;
|
||||
if (!prompt) {
|
||||
if (isInteractive({ nonInteractive: config.nonInteractive })) {
|
||||
const hint = await promptText({ message: "Enter your edit instruction:" });
|
||||
if (!hint) {
|
||||
process.stderr.write("Video editing cancelled.\n");
|
||||
process.exit(1);
|
||||
}
|
||||
prompt = hint;
|
||||
}
|
||||
// prompt is optional for video edit per API spec
|
||||
}
|
||||
// prompt is optional for video edit per API spec
|
||||
const prompt = flags.prompt as string | undefined;
|
||||
|
||||
const model = (flags.model as string) || "happyhorse-1.0-video-edit";
|
||||
const format = detectOutputFormat(config.output);
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
type DashScopeVideoRequest,
|
||||
type DashScopeAsyncResponse,
|
||||
type DashScopeTaskResponse,
|
||||
isInteractive,
|
||||
resolveOutputDir,
|
||||
resolveFileUrl,
|
||||
resolveCredential,
|
||||
@@ -21,7 +20,6 @@ import {
|
||||
import { poll } from "bailian-cli-runtime";
|
||||
import { downloadFile, formatBytes } from "bailian-cli-runtime";
|
||||
import { runConcurrent, getConcurrency } from "bailian-cli-runtime";
|
||||
import { promptText, failIfMissing, cmdUsage } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT, BOOL_FLAG_WATERMARK } from "bailian-cli-runtime";
|
||||
|
||||
@@ -51,10 +49,12 @@ export default defineCommand({
|
||||
{
|
||||
flag: "--prompt-extend <bool>",
|
||||
description: BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT,
|
||||
type: "boolean",
|
||||
},
|
||||
{
|
||||
flag: "--watermark <bool>",
|
||||
description: BOOL_FLAG_WATERMARK,
|
||||
type: "boolean",
|
||||
},
|
||||
{ flag: "--seed <n>", description: "Random seed for reproducible generation", type: "number" },
|
||||
{ flag: "--download <path>", description: "Save video to file on completion" },
|
||||
@@ -77,20 +77,7 @@ export default defineCommand({
|
||||
'--prompt "A cat playing with a ball" --watermark false',
|
||||
],
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
let prompt = flags.prompt as string | undefined;
|
||||
|
||||
if (!prompt) {
|
||||
if (isInteractive({ nonInteractive: config.nonInteractive })) {
|
||||
const hint = await promptText({ message: "Enter your video prompt:" });
|
||||
if (!hint) {
|
||||
process.stderr.write("Video generation cancelled.\n");
|
||||
process.exit(1);
|
||||
}
|
||||
prompt = hint;
|
||||
} else {
|
||||
failIfMissing("prompt", cmdUsage(config, "--prompt <text>"));
|
||||
}
|
||||
}
|
||||
const prompt = flags.prompt as string;
|
||||
|
||||
const model =
|
||||
(flags.model as string) ||
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
type DashScopeVideoRefRequest,
|
||||
type DashScopeAsyncResponse,
|
||||
type DashScopeTaskResponse,
|
||||
isInteractive,
|
||||
resolveOutputDir,
|
||||
resolveFileUrl,
|
||||
resolveCredential,
|
||||
@@ -20,7 +19,6 @@ import {
|
||||
} from "bailian-cli-core";
|
||||
import { poll } from "bailian-cli-runtime";
|
||||
import { downloadFile, formatBytes } from "bailian-cli-runtime";
|
||||
import { promptText, failIfMissing, cmdUsage } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT, BOOL_FLAG_WATERMARK } from "bailian-cli-runtime";
|
||||
|
||||
@@ -66,10 +64,12 @@ export default defineCommand({
|
||||
{
|
||||
flag: "--prompt-extend <bool>",
|
||||
description: BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT,
|
||||
type: "boolean",
|
||||
},
|
||||
{
|
||||
flag: "--watermark <bool>",
|
||||
description: BOOL_FLAG_WATERMARK,
|
||||
type: "boolean",
|
||||
},
|
||||
{ flag: "--seed <n>", description: "Random seed for reproducible generation", type: "number" },
|
||||
{ flag: "--download <path>", description: "Save video to file on completion" },
|
||||
@@ -91,35 +91,16 @@ export default defineCommand({
|
||||
'--prompt "Image 1 and Image 2 have a conversation" --image a.jpg --image b.jpg --image-voice va.mp3 --image-voice vb.mp3',
|
||||
'--prompt "Image 1 drinks water" --image person.jpg --watermark false',
|
||||
],
|
||||
validate: (f) =>
|
||||
!(f.image as string[] | undefined)?.length && !(f.refVideo as string[] | undefined)?.length
|
||||
? "Provide at least one --image or --ref-video."
|
||||
: undefined,
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
// --- Validate prompt ---
|
||||
let prompt = flags.prompt as string | undefined;
|
||||
if (!prompt) {
|
||||
if (isInteractive({ nonInteractive: config.nonInteractive })) {
|
||||
const hint = await promptText({
|
||||
message: "Enter your video prompt (use Image1, Video1 to reference inputs):",
|
||||
});
|
||||
if (!hint) {
|
||||
process.stderr.write("Video generation cancelled.\n");
|
||||
process.exit(1);
|
||||
}
|
||||
prompt = hint;
|
||||
} else {
|
||||
failIfMissing("prompt", cmdUsage(config, "--prompt <text> --image <url>"));
|
||||
}
|
||||
}
|
||||
const prompt = flags.prompt as string;
|
||||
|
||||
const images = (flags.image as string[] | undefined) || [];
|
||||
const refVideos = (flags.refVideo as string[] | undefined) || [];
|
||||
|
||||
if (images.length === 0 && refVideos.length === 0) {
|
||||
throw new BailianError(
|
||||
"At least one --image or --ref-video is required.",
|
||||
ExitCode.USAGE,
|
||||
cmdUsage(config, '--prompt "description" --image person.jpg'),
|
||||
);
|
||||
}
|
||||
|
||||
const imageVoices = (flags.imageVoice as string[] | undefined) || [];
|
||||
const videoVoices = (flags.videoVoice as string[] | undefined) || [];
|
||||
|
||||
|
||||
@@ -7,21 +7,19 @@ import {
|
||||
type GlobalFlags,
|
||||
type DashScopeTaskResponse,
|
||||
} from "bailian-cli-core";
|
||||
import { failIfMissing, cmdUsage } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
|
||||
export default defineCommand({
|
||||
description: "Query async task status",
|
||||
auth: "apiKey",
|
||||
usageArgs: "--task-id <id>",
|
||||
options: [{ flag: "--task-id <id>", description: "Async task ID" }],
|
||||
options: [{ flag: "--task-id <id>", description: "Async task ID", required: true }],
|
||||
exampleArgs: [
|
||||
"--task-id 3b256896-3e70-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
"--task-id 3b256896-3e70-xxxx --output json",
|
||||
],
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
const taskId = flags.taskId as string | undefined;
|
||||
if (!taskId) failIfMissing("task-id", cmdUsage(config, "--task-id <id>"));
|
||||
const taskId = flags.taskId as string;
|
||||
|
||||
const format = detectOutputFormat(config.output);
|
||||
|
||||
|
||||
@@ -8,14 +8,12 @@ import {
|
||||
type ChatRequest,
|
||||
type ChatResponse,
|
||||
type ChatMessageContent,
|
||||
isInteractive,
|
||||
resolveFileUrl,
|
||||
resolveCredential,
|
||||
BailianError,
|
||||
ExitCode,
|
||||
isLocalFile,
|
||||
} from "bailian-cli-core";
|
||||
import { promptText, cmdUsage } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { readFileSync, existsSync } from "fs";
|
||||
import { extname } from "path";
|
||||
@@ -77,10 +75,12 @@ export default defineCommand({
|
||||
"--video ./local-video.mp4",
|
||||
'--image photo.png --prompt "Extract the text" --model qwen-vl-plus',
|
||||
],
|
||||
validate: (f) =>
|
||||
!f.image && !(f.video as string[] | undefined)?.length
|
||||
? "Provide --image or --video."
|
||||
: undefined,
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
let image = (flags.image ?? (flags._positional as string[] | undefined)?.[0]) as
|
||||
| string
|
||||
| undefined;
|
||||
let image = flags.image as string | undefined;
|
||||
const videoInputs = (flags.video as string[] | undefined) ?? [];
|
||||
const model = (flags.model as string) || "qwen3-vl-plus";
|
||||
|
||||
@@ -94,30 +94,6 @@ export default defineCommand({
|
||||
const defaultPrompt = hasVideo ? "Describe the video." : "Describe the image.";
|
||||
const prompt = (flags.prompt as string) || defaultPrompt;
|
||||
|
||||
if (!image && !hasVideo) {
|
||||
if (isInteractive({ nonInteractive: config.nonInteractive })) {
|
||||
const hint = await promptText({
|
||||
message: "Enter image/video path or URL:",
|
||||
});
|
||||
if (!hint) {
|
||||
process.stderr.write("Vision describe cancelled.\n");
|
||||
process.exit(1);
|
||||
}
|
||||
// Detect if user entered a video
|
||||
if (isVideoInput(hint)) {
|
||||
videoInputs.push(hint);
|
||||
} else {
|
||||
image = hint;
|
||||
}
|
||||
} else {
|
||||
throw new BailianError(
|
||||
"Missing required argument --image or --video.",
|
||||
ExitCode.USAGE,
|
||||
`${cmdUsage(config, "--image <path-or-url>")}\n${cmdUsage(config, "--video <url-or-path>")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const format = detectOutputFormat(config.output);
|
||||
|
||||
if (config.dryRun) {
|
||||
|
||||
@@ -45,6 +45,34 @@ export class BailianError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The user invoked the CLI in a structurally invalid way: unknown command path,
|
||||
* unknown/badly-typed flag, or a missing required argument. Always carries
|
||||
* {@link ExitCode.USAGE}. The runtime's error boundary recognises this type and
|
||||
* renders the relevant command's help before printing the message — so command
|
||||
* code never builds usage strings or prints help itself; it just throws this.
|
||||
*/
|
||||
export class UsageError extends BailianError {
|
||||
constructor(message: string, hint?: string) {
|
||||
super(message, ExitCode.USAGE, hint);
|
||||
this.name = "UsageError";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The command is *incomplete* — a valid prefix that stopped short (a missing
|
||||
* required flag / positional). Distinct from {@link UsageError} ("you typed
|
||||
* something wrong"): an incomplete command is not an error. The runtime's error
|
||||
* boundary renders that command's help and exits 0 — exactly like landing on a
|
||||
* command group with no subcommand. Carries {@link ExitCode.SUCCESS}.
|
||||
*/
|
||||
export class IncompleteCommandError extends BailianError {
|
||||
constructor(message: string) {
|
||||
super(message, ExitCode.SUCCESS);
|
||||
this.name = "IncompleteCommandError";
|
||||
}
|
||||
}
|
||||
|
||||
function serializeCause(cause: unknown): Record<string, unknown> | undefined {
|
||||
if (cause == null) return undefined;
|
||||
if (cause instanceof Error) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export { BailianError } from "./errors/base.ts";
|
||||
export { BailianError, UsageError, IncompleteCommandError } from "./errors/base.ts";
|
||||
export { mapApiError, type ApiErrorBody } from "./errors/api.ts";
|
||||
export { ExitCode } from "./errors/codes.ts";
|
||||
|
||||
|
||||
@@ -7,9 +7,6 @@ export function detectOutputFormat(flagValue?: string): OutputFormat {
|
||||
if (flagValue === "json" || flagValue === "text") {
|
||||
return flagValue;
|
||||
}
|
||||
if (!process.stdout.isTTY) {
|
||||
return "json";
|
||||
}
|
||||
return "text";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
import type { Config } from "../config/schema.ts";
|
||||
import type { GlobalFlags } from "./flags.ts";
|
||||
|
||||
/**
|
||||
* Flag value type, driving the parser.
|
||||
* - string : `--flag <value>` → string (default).
|
||||
* - number : `--flag <n>` → coerced + validated finite number.
|
||||
* - boolean : `--flag true|false` → coerced boolean (a *value* flag).
|
||||
* - switch : `--flag` → presence = true, takes NO value (`--flag=x` errors).
|
||||
* - array : repeatable `--flag a --flag b` → string[].
|
||||
* Omitting `type`: a flag string with a `<…>`/`[…]` placeholder defaults to
|
||||
* string, otherwise to switch.
|
||||
*/
|
||||
export interface OptionDef {
|
||||
flag: string;
|
||||
description: string;
|
||||
type?: "string" | "number" | "boolean" | "array";
|
||||
type?: "string" | "number" | "boolean" | "switch" | "array";
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
@@ -36,6 +46,15 @@ export interface Command {
|
||||
/** Credential this command requires. See {@link AuthRequirement}. */
|
||||
auth: AuthRequirement;
|
||||
notes?: string[];
|
||||
/**
|
||||
* Cross-flag validation, run after parsing and before execute. Return an
|
||||
* error message when the flag combination is incomplete (one-of, 3-of-N,
|
||||
* value-conditional, dependency, …); the runtime throws IncompleteCommandError
|
||||
* and renders this command's help. Return undefined to pass. Single-flag
|
||||
* `required: true` is enforced by the parser — use this only for rules that
|
||||
* span multiple flags or depend on a flag's *value*.
|
||||
*/
|
||||
validate?: (flags: GlobalFlags) => string | undefined;
|
||||
execute: (config: Config, flags: GlobalFlags) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -49,6 +68,8 @@ export interface CommandSpec {
|
||||
/** Credential this command requires. See {@link AuthRequirement}. */
|
||||
auth: AuthRequirement;
|
||||
notes?: string[];
|
||||
/** Cross-flag validation — see {@link Command.validate}. */
|
||||
validate?: (flags: GlobalFlags) => string | undefined;
|
||||
run: (config: Config, flags: GlobalFlags) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -60,6 +81,7 @@ export function defineCommand(spec: CommandSpec): Command {
|
||||
exampleArgs: spec.exampleArgs,
|
||||
auth: spec.auth,
|
||||
notes: spec.notes,
|
||||
validate: spec.validate,
|
||||
execute: (config, flags) => spec.run(config, flags),
|
||||
};
|
||||
}
|
||||
@@ -70,11 +92,11 @@ export const GLOBAL_OPTIONS: OptionDef[] = [
|
||||
{ flag: "--base-url <url>", description: "API base URL" },
|
||||
{ flag: "--output <format>", description: "Output format: text, json" },
|
||||
{ flag: "--timeout <seconds>", description: "Request timeout", type: "number" },
|
||||
{ flag: "--quiet", description: "Suppress non-essential output" },
|
||||
{ flag: "--verbose", description: "Print HTTP request/response details" },
|
||||
{ flag: "--no-color", description: "Disable ANSI colors" },
|
||||
{ flag: "--dry-run", description: "Dry run mode" },
|
||||
{ flag: "--non-interactive", description: "Disable interactive prompts" },
|
||||
{ flag: "--quiet", description: "Suppress non-essential output", type: "switch" },
|
||||
{ flag: "--verbose", description: "Print HTTP request/response details", type: "switch" },
|
||||
{ flag: "--no-color", description: "Disable ANSI colors", type: "switch" },
|
||||
{ flag: "--dry-run", description: "Dry run mode", type: "switch" },
|
||||
{ flag: "--non-interactive", description: "Disable interactive prompts", type: "switch" },
|
||||
{ flag: "--concurrent <n>", description: "Run N parallel requests (default: 1)", type: "number" },
|
||||
{
|
||||
flag: "--console-region <region>",
|
||||
@@ -86,6 +108,6 @@ export const GLOBAL_OPTIONS: OptionDef[] = [
|
||||
description: "Switch agent UID for delegated access",
|
||||
type: "number",
|
||||
},
|
||||
{ flag: "--help", description: "Show help" },
|
||||
{ flag: "--version", description: "Print version" },
|
||||
{ flag: "--help", description: "Show help", type: "switch" },
|
||||
{ flag: "--version", description: "Print version", type: "switch" },
|
||||
];
|
||||
|
||||
@@ -8,7 +8,6 @@ export interface GlobalFlags {
|
||||
noColor: boolean;
|
||||
yes: boolean;
|
||||
dryRun: boolean;
|
||||
help: boolean;
|
||||
nonInteractive: boolean;
|
||||
async: boolean;
|
||||
consoleRegion?: string;
|
||||
|
||||
+128
-116
@@ -1,6 +1,6 @@
|
||||
import type { GlobalFlags } from "bailian-cli-core";
|
||||
import type { OptionDef } from "bailian-cli-core";
|
||||
import { BailianError, ExitCode } from "bailian-cli-core";
|
||||
import { UsageError, IncompleteCommandError } from "bailian-cli-core";
|
||||
|
||||
function kebabToCamel(str: string): string {
|
||||
return str.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
|
||||
@@ -12,14 +12,8 @@ function flagKey(def: OptionDef): string | null {
|
||||
return m ? kebabToCamel(m[1]!) : null;
|
||||
}
|
||||
|
||||
/** Boolean when no value placeholder and type is not string/number/array */
|
||||
function isBooleanDef(def: OptionDef): boolean {
|
||||
if (def.type === "boolean") return true;
|
||||
if (def.type === "string" || def.type === "number" || def.type === "array") return false;
|
||||
return !def.flag.includes("<") && !def.flag.includes("[");
|
||||
}
|
||||
|
||||
interface FlagSchema {
|
||||
switches: Set<string>;
|
||||
booleans: Set<string>;
|
||||
numbers: Set<string>;
|
||||
arrays: Set<string>;
|
||||
@@ -34,152 +28,170 @@ function buildAllowedFlagKeys(options: OptionDef[]): Set<string> {
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Classify each option by its declared (or inferred) type. Inference: a flag
|
||||
* with a `<…>`/`[…]` placeholder defaults to string, otherwise to switch.
|
||||
* Strings are the default bucket and need no set.
|
||||
*/
|
||||
function buildSchema(options: OptionDef[]): FlagSchema {
|
||||
const switches = new Set<string>();
|
||||
const booleans = new Set<string>();
|
||||
const numbers = new Set<string>();
|
||||
const arrays = new Set<string>();
|
||||
for (const opt of options) {
|
||||
const key = flagKey(opt);
|
||||
if (!key) continue;
|
||||
if (isBooleanDef(opt)) booleans.add(key);
|
||||
else if (opt.type === "number") numbers.add(key);
|
||||
else if (opt.type === "array") arrays.add(key);
|
||||
switch (opt.type) {
|
||||
case "switch":
|
||||
switches.add(key);
|
||||
break;
|
||||
case "boolean":
|
||||
booleans.add(key);
|
||||
break;
|
||||
case "number":
|
||||
numbers.add(key);
|
||||
break;
|
||||
case "array":
|
||||
arrays.add(key);
|
||||
break;
|
||||
case "string":
|
||||
break;
|
||||
default:
|
||||
if (!opt.flag.includes("<") && !opt.flag.includes("[")) switches.add(key);
|
||||
}
|
||||
}
|
||||
return { booleans, numbers, arrays };
|
||||
return { switches, booleans, numbers, arrays };
|
||||
}
|
||||
|
||||
export interface ParsePathResult {
|
||||
/** Command path: the leading run of bare tokens, e.g. ["speech", "recognize"]. */
|
||||
path: string[];
|
||||
/** Everything from the first flag onward — handed to parseFlags later. */
|
||||
rest: string[];
|
||||
hasHelpFlag: boolean;
|
||||
hasVersionFlag: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick scan: collect positional (non-dash) args to determine the command path.
|
||||
* Skips global flags and their values so that e.g. `--output json text chat`
|
||||
* correctly produces ['text', 'chat'] instead of ['json', 'text', 'chat'].
|
||||
* First pass — routing only. The command path is the leading run of bare
|
||||
* (non-`-`) tokens; the first flag ends it ("command path first, then flags",
|
||||
* oclif-style). There are no positionals, so nothing bare can legitimately
|
||||
* follow a flag — and no flags precede the path, so this needs no schema.
|
||||
*/
|
||||
export function scanCommandPath(argv: string[], globalOptions: OptionDef[] = []): string[] {
|
||||
const globalSchema = buildSchema(globalOptions);
|
||||
const path: string[] = [];
|
||||
export function parsePath(argv: string[]): ParsePathResult {
|
||||
let i = 0;
|
||||
while (i < argv.length) {
|
||||
const arg = argv[i]!;
|
||||
if (arg === "--") break;
|
||||
|
||||
if (arg.startsWith("--")) {
|
||||
const eqIdx = arg.indexOf("=");
|
||||
const key = eqIdx !== -1 ? arg.slice(2, eqIdx) : arg.slice(2);
|
||||
const camelKey = kebabToCamel(key);
|
||||
|
||||
if (!globalSchema.booleans.has(camelKey) && eqIdx === -1) {
|
||||
const next = argv[i + 1];
|
||||
// Command-local booleans (e.g. `--console`) are not in GLOBAL_OPTIONS; if the next
|
||||
// token is another flag, do not consume it as this flag's value.
|
||||
if (next === undefined || next.startsWith("-")) {
|
||||
i += 1;
|
||||
} else {
|
||||
i += 2;
|
||||
}
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.startsWith("-")) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
path.push(arg);
|
||||
i++;
|
||||
}
|
||||
return path;
|
||||
while (i < argv.length && !argv[i]!.startsWith("-")) i++;
|
||||
const rest = argv.slice(i);
|
||||
return {
|
||||
path: argv.slice(0, i),
|
||||
rest,
|
||||
hasHelpFlag: rest.includes("--help"),
|
||||
hasVersionFlag: rest.includes("--version"),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Full flag parse. Types are derived entirely from the provided OptionDef schema:
|
||||
* - boolean: no <value> placeholder in flag string (or type: 'boolean')
|
||||
* - number: type: 'number'
|
||||
* - array: type: 'array' (repeatable via multiple --flag occurrences)
|
||||
* - default: string
|
||||
* Second pass — parse the flag region into typed values, driven entirely by the
|
||||
* OptionDef schema. Pure: returns typed flags or throws — never prints/exits.
|
||||
* The runtime's error boundary decides rendering. Throws IncompleteCommandError
|
||||
* for missing required flags (incomplete → help) and UsageError for malformed
|
||||
* input (unknown/short flag, bad value, unexpected token, duplicate).
|
||||
*/
|
||||
export function parseFlags(argv: string[], options: OptionDef[]): GlobalFlags {
|
||||
export function parseFlags(rest: string[], options: OptionDef[]): GlobalFlags {
|
||||
const allowedKeys = buildAllowedFlagKeys(options);
|
||||
const schema = buildSchema(options);
|
||||
const seen = new Set<string>();
|
||||
const flags: GlobalFlags = {
|
||||
quiet: false,
|
||||
verbose: false,
|
||||
noColor: false,
|
||||
yes: false,
|
||||
dryRun: false,
|
||||
help: false,
|
||||
nonInteractive: false,
|
||||
async: false,
|
||||
};
|
||||
|
||||
let i = 0;
|
||||
while (i < argv.length) {
|
||||
const arg = argv[i]!;
|
||||
while (i < rest.length) {
|
||||
const arg = rest[i]!;
|
||||
|
||||
if (arg === "--help" || arg === "-h") {
|
||||
flags.help = true;
|
||||
if (!arg.startsWith("-")) {
|
||||
throw new UsageError(`Unexpected argument: ${arg}`);
|
||||
}
|
||||
if (!arg.startsWith("--")) {
|
||||
throw new UsageError(`Unknown flag "${arg}". Use the --long form.`);
|
||||
}
|
||||
|
||||
const eqIdx = arg.indexOf("=");
|
||||
const key = eqIdx !== -1 ? arg.slice(2, eqIdx) : arg.slice(2);
|
||||
let value: string | undefined = eqIdx !== -1 ? arg.slice(eqIdx + 1) : undefined;
|
||||
|
||||
if (key === "") {
|
||||
throw new UsageError(`Unknown flag "${arg}".`);
|
||||
}
|
||||
const camelKey = kebabToCamel(key);
|
||||
if (!allowedKeys.has(camelKey)) {
|
||||
throw new UsageError(`Unknown flag "--${key}". Run with --help to see available options.`);
|
||||
}
|
||||
|
||||
if (schema.switches.has(camelKey)) {
|
||||
if (value !== undefined) {
|
||||
throw new UsageError(`Flag --${key} is a switch and takes no value.`);
|
||||
}
|
||||
(flags as Record<string, unknown>)[camelKey] = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--") {
|
||||
break;
|
||||
|
||||
if (value === undefined) {
|
||||
const next = rest[i + 1];
|
||||
if (next === undefined || next.startsWith("--")) {
|
||||
throw new UsageError(`Flag --${key} requires a value.`);
|
||||
}
|
||||
value = next;
|
||||
i += 2;
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
|
||||
if (arg.startsWith("--")) {
|
||||
const eqIdx = arg.indexOf("=");
|
||||
let key: string;
|
||||
let value: string | undefined;
|
||||
|
||||
if (eqIdx !== -1) {
|
||||
key = arg.slice(2, eqIdx);
|
||||
value = arg.slice(eqIdx + 1);
|
||||
} else {
|
||||
key = arg.slice(2);
|
||||
}
|
||||
|
||||
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++;
|
||||
const next = argv[i];
|
||||
if (next === undefined || next.startsWith("-")) {
|
||||
throw new BailianError(`Flag --${key} requires a value.`, ExitCode.USAGE);
|
||||
}
|
||||
value = next;
|
||||
}
|
||||
|
||||
if (schema.arrays.has(camelKey)) {
|
||||
const arr = (flags as Record<string, unknown>)[camelKey] as string[] | undefined;
|
||||
if (arr) arr.push(value);
|
||||
else (flags as Record<string, unknown>)[camelKey] = [value];
|
||||
} else if (schema.numbers.has(camelKey)) {
|
||||
const numericValue = Number(value);
|
||||
if (!Number.isFinite(numericValue)) {
|
||||
throw new BailianError(`Flag --${key} requires a finite number.`, ExitCode.USAGE);
|
||||
}
|
||||
(flags as Record<string, unknown>)[camelKey] = numericValue;
|
||||
} else {
|
||||
(flags as Record<string, unknown>)[camelKey] = value;
|
||||
}
|
||||
if (schema.arrays.has(camelKey)) {
|
||||
const arr = (flags as Record<string, unknown>)[camelKey] as string[] | undefined;
|
||||
if (arr) arr.push(value);
|
||||
else (flags as Record<string, unknown>)[camelKey] = [value];
|
||||
continue;
|
||||
}
|
||||
|
||||
i++;
|
||||
if (seen.has(camelKey)) {
|
||||
throw new UsageError(`Flag --${key} given more than once.`);
|
||||
}
|
||||
seen.add(camelKey);
|
||||
|
||||
if (schema.numbers.has(camelKey)) {
|
||||
const n = Number(value);
|
||||
if (!Number.isFinite(n)) {
|
||||
throw new UsageError(`Flag --${key} requires a finite number.`);
|
||||
}
|
||||
(flags as Record<string, unknown>)[camelKey] = n;
|
||||
} else if (schema.booleans.has(camelKey)) {
|
||||
const v = value.trim().toLowerCase();
|
||||
if (v === "true") (flags as Record<string, unknown>)[camelKey] = true;
|
||||
else if (v === "false") (flags as Record<string, unknown>)[camelKey] = false;
|
||||
else throw new UsageError(`Flag --${key} requires true or false.`);
|
||||
} else {
|
||||
(flags as Record<string, unknown>)[camelKey] = value;
|
||||
}
|
||||
}
|
||||
|
||||
const missing = options.filter((opt) => {
|
||||
if (!opt.required) return false;
|
||||
const key = flagKey(opt);
|
||||
return key !== null && (flags as Record<string, unknown>)[key] === undefined;
|
||||
});
|
||||
if (missing.length > 0) {
|
||||
const names = missing.map((opt) => opt.flag.match(/^(--[a-z][a-z0-9-]*)/i)?.[1] ?? opt.flag);
|
||||
throw new IncompleteCommandError(
|
||||
`Missing required ${names.length > 1 ? "flags" : "flag"}: ${names.join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
return flags;
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
import { scanCommandPath, parseFlags } from "./args.ts";
|
||||
import { parseFlags } from "./args.ts";
|
||||
import { CommandRegistry } from "./registry.ts";
|
||||
import type { Command } from "bailian-cli-core";
|
||||
import { resolve } from "./resolve.ts";
|
||||
import {
|
||||
compose,
|
||||
authStage,
|
||||
telemetryStage,
|
||||
versionCheckStage,
|
||||
runCommandStage,
|
||||
type RunContext,
|
||||
} from "./middleware.ts";
|
||||
import type { Command, Config, GlobalFlags } from "bailian-cli-core";
|
||||
import {
|
||||
GLOBAL_OPTIONS,
|
||||
IncompleteCommandError,
|
||||
loadConfig,
|
||||
resolveCredential,
|
||||
trackCommandExecution,
|
||||
flushTelemetry,
|
||||
} from "bailian-cli-core";
|
||||
import { ensureApiKey } from "./utils/ensure-key.ts";
|
||||
import { setupProxyFromEnv } from "./proxy.ts";
|
||||
import { handleError } from "./error-handler.ts";
|
||||
import { checkForUpdate, getPendingUpdateNotification } from "./utils/update-checker.ts";
|
||||
import { maybeShowStatusBar } from "./output/status-bar.ts";
|
||||
import { printWelcomeBanner, printQuickStart } from "./output/banner.ts";
|
||||
import { registerCommandHelpPrinter, setExecutingCommandPath } from "./utils/command-help.ts";
|
||||
|
||||
/** Per-product identity injected by each CLI entrypoint (bl / rag / …). */
|
||||
export interface CliOptions {
|
||||
@@ -33,30 +37,25 @@ export interface Cli {
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a CLI from an injected command set. The runtime is agnostic to *which*
|
||||
* Build a CLI from an injected command set. The kernel is agnostic to *which*
|
||||
* commands exist — each product (bailian-cli, rag-cli, …) passes its own map and
|
||||
* identity. No module-level singleton: the registry is scoped to this instance.
|
||||
* identity. `run` is a thin orchestrator: resolve argv into a {@link Resolution},
|
||||
* then dispatch — terminal kinds render and return; `run` enters the middleware
|
||||
* stack guarded by a single error boundary. No business `if`s, no scattered
|
||||
* `process.exit`.
|
||||
*/
|
||||
export function createCli(commands: Record<string, Command>, opts: CliOptions): Cli {
|
||||
const registry = new CommandRegistry(commands, opts.binName);
|
||||
const clientName = opts.clientName ?? opts.binName;
|
||||
const npmPackage = opts.npmPackage;
|
||||
const version = opts.version;
|
||||
const { binName, version, npmPackage } = opts;
|
||||
|
||||
// 必须在任何 fetch 发起前安装(含 update-checker / telemetry)
|
||||
try {
|
||||
setupProxyFromEnv();
|
||||
} catch (err) {
|
||||
handleError(err, opts.binName);
|
||||
handleError(err, binName);
|
||||
}
|
||||
|
||||
registerCommandHelpPrinter((commandPath, out) => {
|
||||
registry.printHelp(commandPath, out);
|
||||
});
|
||||
|
||||
// 优雅处理 Ctrl+C
|
||||
// 退出前尝试 best-effort 刷出埋点,让去抖队列中 / 在途的 fetch 请求有机会
|
||||
// 落网络;flush 与较短超时 race,保证 SIGINT 仍然响应及时。
|
||||
process.on("SIGINT", () => {
|
||||
process.stderr.write("\nInterrupted. Exiting.\n");
|
||||
void flushTelemetry(500).finally(() => process.exit(130));
|
||||
@@ -68,107 +67,89 @@ export function createCli(commands: Record<string, Command>, opts: CliOptions):
|
||||
else throw e;
|
||||
});
|
||||
|
||||
async function main(): Promise<void> {
|
||||
let argv = process.argv.slice(2);
|
||||
if (argv[0] === "--") argv = argv.slice(1);
|
||||
const runMiddleware = compose([versionCheckStage, telemetryStage, authStage, runCommandStage]);
|
||||
|
||||
if (argv.includes("--version") || argv.includes("-v")) {
|
||||
process.stdout.write(`${opts.binName} ${version}\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
function buildConfig(flags: GlobalFlags): Config {
|
||||
const config = loadConfig(flags);
|
||||
config.clientName = clientName;
|
||||
config.clientVersion = version;
|
||||
config.binName = binName;
|
||||
config.npmPackage = npmPackage;
|
||||
return config;
|
||||
}
|
||||
|
||||
const commandPath = scanCommandPath(argv, GLOBAL_OPTIONS);
|
||||
/** Render help for `path`; root ([]) doubles as the onboarding / login guide. */
|
||||
function renderHelp(path: string[], argv: string[]): void {
|
||||
registry.printHelp(path, process.stderr);
|
||||
if (path.length > 0) return;
|
||||
|
||||
if (argv.includes("--help") || argv.includes("-h")) {
|
||||
registry.printHelp(commandPath, process.stderr);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// 未传任何命令:展示帮助信息与登录引导
|
||||
if (commandPath.length === 0) {
|
||||
registry.printHelp([], process.stderr);
|
||||
|
||||
const flags = parseFlags(argv, GLOBAL_OPTIONS);
|
||||
const config = loadConfig(flags);
|
||||
config.clientName = clientName;
|
||||
config.clientVersion = version;
|
||||
config.binName = opts.binName;
|
||||
config.npmPackage = npmPackage;
|
||||
|
||||
const hasKey = !!(
|
||||
let hasKey = false;
|
||||
try {
|
||||
const config = buildConfig(parseFlags(argv, GLOBAL_OPTIONS));
|
||||
hasKey = !!(
|
||||
config.apiKey ||
|
||||
config.fileApiKey ||
|
||||
config.fileAccessToken ||
|
||||
config.accessTokenEnv
|
||||
);
|
||||
if (hasKey) printQuickStart();
|
||||
else printWelcomeBanner(opts.binName);
|
||||
process.exit(0);
|
||||
} catch {
|
||||
/* unparseable global flags on the bare invocation — fall through to welcome */
|
||||
}
|
||||
if (hasKey) printQuickStart();
|
||||
else printWelcomeBanner(binName);
|
||||
}
|
||||
|
||||
// 组路径(例如 `bl speech` 未接子命令):展示帮助后干净退出
|
||||
if (registry.isGroupPath(commandPath)) {
|
||||
registry.printHelp(commandPath, process.stderr);
|
||||
process.exit(0);
|
||||
}
|
||||
async function dispatch(argv: string[]): Promise<void> {
|
||||
const res = resolve(argv, registry);
|
||||
|
||||
const { command, extra } = registry.resolve(commandPath);
|
||||
const flags = parseFlags(argv, [...GLOBAL_OPTIONS, ...(command.options ?? [])]);
|
||||
switch (res.kind) {
|
||||
case "version":
|
||||
process.stdout.write(`${binName} ${version}\n`);
|
||||
return;
|
||||
|
||||
if (extra.length > 0) (flags as Record<string, unknown>)._positional = extra;
|
||||
case "help":
|
||||
renderHelp(res.path, argv);
|
||||
return;
|
||||
|
||||
const config = loadConfig(flags);
|
||||
config.clientName = clientName;
|
||||
config.clientVersion = version;
|
||||
config.binName = opts.binName;
|
||||
config.npmPackage = npmPackage;
|
||||
case "usageError":
|
||||
handleError(res.error, binName);
|
||||
return;
|
||||
|
||||
// 仅 apiKey 类命令由框架统一准备 API Key;dry-run 时跳过(只打印请求,不要求凭证)。
|
||||
// console 命令在命令体内解析 Console Gateway 凭证;none 命令无需凭证。
|
||||
if (command.auth === "apiKey" && !config.dryRun) {
|
||||
await ensureApiKey(config);
|
||||
try {
|
||||
const credential = await resolveCredential(config);
|
||||
maybeShowStatusBar(config, credential.token, credential);
|
||||
} catch {
|
||||
/* 没有凭证,不展示状态栏 */
|
||||
case "run": {
|
||||
try {
|
||||
const flags = parseFlags(res.rest, [...GLOBAL_OPTIONS, ...(res.command.options ?? [])]);
|
||||
const invalid = res.command.validate?.(flags);
|
||||
if (invalid) throw new IncompleteCommandError(invalid);
|
||||
const config = buildConfig(flags);
|
||||
const ctx: RunContext = {
|
||||
binName,
|
||||
version,
|
||||
npmPackage,
|
||||
path: res.path,
|
||||
command: res.command,
|
||||
config,
|
||||
flags,
|
||||
};
|
||||
await runMiddleware(ctx);
|
||||
await flushTelemetry(1000);
|
||||
} catch (err) {
|
||||
await flushTelemetry(1000);
|
||||
if (err instanceof IncompleteCommandError) {
|
||||
registry.printHelp(res.path, process.stderr);
|
||||
return;
|
||||
}
|
||||
handleError(err, binName);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const updateCheckPromise = checkForUpdate(version, npmPackage).catch(() => {});
|
||||
|
||||
setExecutingCommandPath(commandPath);
|
||||
|
||||
await trackCommandExecution(config, commandPath, flags, () => command.execute(config, flags));
|
||||
|
||||
await updateCheckPromise;
|
||||
const isUpdateCommand = commandPath.length === 1 && commandPath[0] === "update";
|
||||
const newVersion = getPendingUpdateNotification();
|
||||
if (newVersion && !config.quiet && !isUpdateCommand) {
|
||||
const isTTY = process.stderr.isTTY;
|
||||
const yellow = isTTY ? "\x1b[33m" : "";
|
||||
const cyan = isTTY ? "\x1b[36m" : "";
|
||||
const reset = isTTY ? "\x1b[0m" : "";
|
||||
process.stderr.write(`\n ${yellow}Update available: ${version} → ${newVersion}${reset}\n`);
|
||||
process.stderr.write(` Run ${cyan}${opts.binName} update${reset} to upgrade\n\n`);
|
||||
}
|
||||
|
||||
// 进程退出前尽力等待在途的埋点完成。
|
||||
// 使用较短超时兜底,避免慢网拖慢用户感知。
|
||||
await flushTelemetry(1000);
|
||||
}
|
||||
|
||||
return {
|
||||
run() {
|
||||
return main().catch((err) => {
|
||||
// 在 handleError() 调用 process.exit() 之前刷出在途埋点。
|
||||
// 命令抛出的错误已被 trackCommandExecution 的 finally 块记录,
|
||||
// 但底层 tracker 有 ~500ms 的发送去抖。不主动 flush 的话,
|
||||
// 错误事件会随进程退出丢掉。
|
||||
return flushTelemetry(1000).finally(() =>
|
||||
handleError(err, opts.binName),
|
||||
) as unknown as void;
|
||||
});
|
||||
run(argv: string[] = process.argv.slice(2)) {
|
||||
return dispatch(argv).catch(
|
||||
(err) => flushTelemetry(1000).finally(() => handleError(err, binName)) as unknown as void,
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,10 +7,14 @@ export type { Cli, CliOptions } from "./create-cli.ts";
|
||||
|
||||
// Command routing
|
||||
export { CommandRegistry } from "./registry.ts";
|
||||
export type { Command, OptionDef } from "./registry.ts";
|
||||
export type { Command, OptionDef, LocateResult } from "./registry.ts";
|
||||
export { resolve } from "./resolve.ts";
|
||||
export type { Resolution } from "./resolve.ts";
|
||||
export { compose, type RunContext, type Middleware } from "./middleware.ts";
|
||||
|
||||
// Arg parsing
|
||||
export { scanCommandPath, parseFlags } from "./args.ts";
|
||||
export { parsePath, parseFlags } from "./args.ts";
|
||||
export type { ParsePathResult } from "./args.ts";
|
||||
|
||||
// Process-level setup / error handling
|
||||
export { setupProxyFromEnv } from "./proxy.ts";
|
||||
@@ -22,13 +26,7 @@ export { BAILIAN_CONSOLE_ROOT, BAILIAN_CONSOLE, API_KEY_PAGE } from "./urls.ts";
|
||||
|
||||
// Output facilities consumed by commands
|
||||
export { emitResult, emitBare } from "./output/output.ts";
|
||||
export {
|
||||
promptText,
|
||||
promptSelect,
|
||||
promptConfirm,
|
||||
failIfMissing,
|
||||
cmdUsage,
|
||||
} from "./output/prompt.ts";
|
||||
export { promptText, promptSelect, promptConfirm, cmdUsage } from "./output/prompt.ts";
|
||||
export { createSpinner, createProgressBar } from "./output/progress.ts";
|
||||
export { printWelcomeBanner, printQuickStart } from "./output/banner.ts";
|
||||
export { maybeShowStatusBar } from "./output/status-bar.ts";
|
||||
@@ -40,12 +38,6 @@ export { downloadFile, formatBytes } from "./utils/download.ts";
|
||||
export { runConcurrent, getConcurrency, downloadParallel } from "./utils/concurrent.ts";
|
||||
export { resolveImageSize } from "./utils/image-size.ts";
|
||||
export { ensureApiKey } from "./utils/ensure-key.ts";
|
||||
export {
|
||||
printCurrentCommandHelp,
|
||||
setExecutingCommandPath,
|
||||
getExecutingCommandPath,
|
||||
registerCommandHelpPrinter,
|
||||
} from "./utils/command-help.ts";
|
||||
export {
|
||||
checkForUpdate,
|
||||
getPendingUpdateNotification,
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { Command, Config, GlobalFlags } from "bailian-cli-core";
|
||||
import { resolveCredential, trackCommandExecution } from "bailian-cli-core";
|
||||
import { ensureApiKey } from "./utils/ensure-key.ts";
|
||||
import { maybeShowStatusBar } from "./output/status-bar.ts";
|
||||
import { checkForUpdate, getPendingUpdateNotification } from "./utils/update-checker.ts";
|
||||
|
||||
/**
|
||||
* Everything a stage needs about the invocation in flight. Built once per `run`
|
||||
* by the kernel and threaded through the middleware stack. The command itself
|
||||
* still receives `(config, flags)` — this context is the pipeline's, not the
|
||||
* command's — so adding cross-cutting concerns never touches command code.
|
||||
*/
|
||||
export interface RunContext {
|
||||
readonly binName: string;
|
||||
readonly version: string;
|
||||
readonly npmPackage: string;
|
||||
/** The matched command path, e.g. ["speech","recognize"]. */
|
||||
readonly path: string[];
|
||||
readonly command: Command;
|
||||
config: Config;
|
||||
flags: GlobalFlags;
|
||||
}
|
||||
|
||||
/** Koa-style onion middleware: do work, call `next()`, do work after it returns. */
|
||||
export type Middleware = (ctx: RunContext, next: () => Promise<void>) => Promise<void>;
|
||||
|
||||
/** Fold a middleware list into a single runnable function. */
|
||||
export function compose(stack: Middleware[]): (ctx: RunContext) => Promise<void> {
|
||||
return (ctx) => {
|
||||
const dispatch = (i: number): Promise<void> => {
|
||||
const mw = stack[i];
|
||||
if (!mw) return Promise.resolve();
|
||||
return mw(ctx, () => dispatch(i + 1));
|
||||
};
|
||||
return dispatch(0);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare credentials for commands that need an API key. console / none
|
||||
* commands resolve their own (or no) credential inside the command body.
|
||||
*/
|
||||
export const authStage: Middleware = async (ctx, next) => {
|
||||
if (ctx.command.auth === "apiKey" && !ctx.config.dryRun) {
|
||||
await ensureApiKey(ctx.config);
|
||||
try {
|
||||
const credential = await resolveCredential(ctx.config);
|
||||
maybeShowStatusBar(ctx.config, credential.token, credential);
|
||||
} catch {
|
||||
/* no credential resolved — skip the status bar */
|
||||
}
|
||||
}
|
||||
await next();
|
||||
};
|
||||
|
||||
/** Record command execution (start / success / failure) around the command. */
|
||||
export const telemetryStage: Middleware = (ctx, next) =>
|
||||
trackCommandExecution(ctx.config, ctx.path, ctx.flags, next);
|
||||
|
||||
/**
|
||||
* Kick off a debounced update check before the command, then — only on success
|
||||
* — surface any pending notification. Never affects the command's exit code:
|
||||
* if `next()` throws, the notice is skipped (no update nag on failure).
|
||||
*/
|
||||
export const versionCheckStage: Middleware = async (ctx, next) => {
|
||||
const pending = checkForUpdate(ctx.version, ctx.npmPackage).catch(() => {});
|
||||
await next();
|
||||
await pending;
|
||||
|
||||
const isUpdateCommand = ctx.path.length === 1 && ctx.path[0] === "update";
|
||||
const newVersion = getPendingUpdateNotification();
|
||||
if (newVersion && !ctx.config.quiet && !isUpdateCommand) {
|
||||
const isTTY = process.stderr.isTTY;
|
||||
const yellow = isTTY ? "\x1b[33m" : "";
|
||||
const cyan = isTTY ? "\x1b[36m" : "";
|
||||
const reset = isTTY ? "\x1b[0m" : "";
|
||||
process.stderr.write(`\n ${yellow}Update available: ${ctx.version} → ${newVersion}${reset}\n`);
|
||||
process.stderr.write(` Run ${cyan}${ctx.binName} update${reset} to upgrade\n\n`);
|
||||
}
|
||||
};
|
||||
|
||||
/** Innermost stage: hand control to the command. */
|
||||
export const runCommandStage: Middleware = (ctx) => ctx.command.execute(ctx.config, ctx.flags);
|
||||
@@ -10,18 +10,16 @@
|
||||
* case explicitly.
|
||||
*/
|
||||
|
||||
import { BailianError, ExitCode, isInteractive, type Config } from "bailian-cli-core";
|
||||
import { printCurrentCommandHelp, getExecutingCommandPath } from "../utils/command-help.ts";
|
||||
import { isInteractive, type Config } from "bailian-cli-core";
|
||||
|
||||
/**
|
||||
* Build a command-usage string for the running command: `<binName> <path> <args>`.
|
||||
* Both the product binary name and the command path come from the runtime, so
|
||||
* callers never hardcode "bl" or their own path — the same code renders as
|
||||
* `bl knowledge retrieve …` under bl and `rag retrieve …` under rag.
|
||||
* Build a command-usage string prefixed with the product binary name, e.g.
|
||||
* `bl --list-voices --model x`. Used for actionable hints inside error messages
|
||||
* (the error boundary renders full command help separately).
|
||||
*/
|
||||
export function cmdUsage(config: Config, args = ""): string {
|
||||
const parts = [config.binName, ...getExecutingCommandPath()].filter(Boolean);
|
||||
return args ? `${parts.join(" ")} ${args}` : parts.join(" ");
|
||||
const bin = config.binName ?? "";
|
||||
return args ? `${bin} ${args}` : bin;
|
||||
}
|
||||
|
||||
// Dynamic import to avoid loading @clack/prompts in non-interactive envs unnecessarily
|
||||
@@ -105,21 +103,3 @@ export async function promptSelect(options: {
|
||||
if (typeof val === "symbol") return undefined;
|
||||
return val as string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fail fast with a user-friendly error when a required option is missing
|
||||
* in non-interactive (agent / CI) mode.
|
||||
*/
|
||||
export function failIfMissing(flagName: string, context: string): never {
|
||||
if (getExecutingCommandPath().length > 0) {
|
||||
printCurrentCommandHelp(process.stderr);
|
||||
process.exit(0);
|
||||
}
|
||||
throw new BailianError(
|
||||
`Missing required argument: --${flagName}\n` +
|
||||
`Hint: In non-interactive (CI / agent) environments all required flags must be provided.\n` +
|
||||
` In an interactive terminal, run without --${flagName} and the CLI will prompt for it.`,
|
||||
ExitCode.USAGE,
|
||||
context,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Command } from "bailian-cli-core";
|
||||
import { BailianError } from "bailian-cli-core";
|
||||
import { ExitCode } from "bailian-cli-core";
|
||||
import { UsageError } from "bailian-cli-core";
|
||||
import { GLOBAL_OPTIONS } from "bailian-cli-core";
|
||||
|
||||
export type { Command, OptionDef } from "bailian-cli-core";
|
||||
@@ -10,6 +9,19 @@ interface CommandNode {
|
||||
children: Map<string, CommandNode>;
|
||||
}
|
||||
|
||||
/**
|
||||
* What a command path resolves to in the registry. The single judgement that
|
||||
* feeds `resolve()` — no scattered `isGroupPath` + throwing `resolve`.
|
||||
* - leaf: landed *exactly* on an executable command (no leftover tokens).
|
||||
* - group: landed on a command group with no executable of its own (incl. root []).
|
||||
* - unknown: the path doesn't exist, or a valid command had unexpected trailing
|
||||
* tokens (no positionals); `error` carries the message + hint.
|
||||
*/
|
||||
export type LocateResult =
|
||||
| { kind: "leaf"; command: Command; matched: string[] }
|
||||
| { kind: "group"; matched: string[] }
|
||||
| { kind: "unknown"; error: UsageError };
|
||||
|
||||
export class CommandRegistry {
|
||||
private root: CommandNode = { children: new Map() };
|
||||
/** Binary name shown in usage/help/error strings (e.g. "bl", "rag"). */
|
||||
@@ -59,17 +71,13 @@ export class CommandRegistry {
|
||||
return walk(this.root, []) ?? "<resource> <command>";
|
||||
}
|
||||
|
||||
isGroupPath(commandPath: string[]): boolean {
|
||||
let node = this.root;
|
||||
for (const part of commandPath) {
|
||||
const child = node.children.get(part);
|
||||
if (!child) return false;
|
||||
node = child;
|
||||
}
|
||||
return !node.command && node.children.size > 0;
|
||||
}
|
||||
|
||||
resolve(commandPath: string[]): { command: Command; extra: string[] } {
|
||||
/**
|
||||
* Resolve a command path to a leaf / group / unknown outcome. Pure: walks the
|
||||
* trie taking the longest registered prefix as the command. There are no
|
||||
* positionals, so a valid command followed by leftover tokens is `unknown`
|
||||
* (unexpected argument). Never throws — unknown paths return a carried UsageError.
|
||||
*/
|
||||
locate(commandPath: string[]): LocateResult {
|
||||
let node = this.root;
|
||||
const matched: string[] = [];
|
||||
|
||||
@@ -81,18 +89,23 @@ export class CommandRegistry {
|
||||
}
|
||||
|
||||
if (node.command) {
|
||||
return { command: node.command, extra: commandPath.slice(matched.length) };
|
||||
}
|
||||
|
||||
// Single child: auto-forward (e.g. `bl config` → `bl config show`)
|
||||
if (matched.length > 0 && node.children.size === 1) {
|
||||
const [, child] = node.children.entries().next().value as [string, CommandNode];
|
||||
if (child.command) {
|
||||
return { command: child.command, extra: commandPath.slice(matched.length) };
|
||||
const leftover = commandPath.slice(matched.length);
|
||||
if (leftover.length === 0) {
|
||||
return { kind: "leaf", command: node.command, matched };
|
||||
}
|
||||
return {
|
||||
kind: "unknown",
|
||||
error: new UsageError(
|
||||
`Unexpected argument: ${leftover.join(" ")}`,
|
||||
`${this.cliName} ${matched.join(" ")} --help`,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (matched.length === commandPath.length && node.children.size > 0) {
|
||||
return { kind: "group", matched };
|
||||
}
|
||||
|
||||
// If we matched some path but no command, show help for that group
|
||||
if (matched.length > 0 && node.children.size > 0) {
|
||||
const subcommands = Array.from(node.children.entries())
|
||||
.map(([name, n]) => {
|
||||
@@ -101,18 +114,22 @@ export class CommandRegistry {
|
||||
return ` ${matched.join(" ")} ${name} [${subs}]`;
|
||||
})
|
||||
.join("\n");
|
||||
throw new BailianError(
|
||||
`Unknown command: ${this.cliName} ${commandPath.join(" ")}\n\nAvailable commands:\n${subcommands}`,
|
||||
ExitCode.USAGE,
|
||||
`${this.cliName} ${matched.join(" ")} --help`,
|
||||
);
|
||||
return {
|
||||
kind: "unknown",
|
||||
error: new UsageError(
|
||||
`Unknown command: ${this.cliName} ${commandPath.join(" ")}\n\nAvailable commands:\n${subcommands}`,
|
||||
`${this.cliName} ${matched.join(" ")} --help`,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
throw new BailianError(
|
||||
`Unknown command: ${this.cliName} ${commandPath.join(" ")}`,
|
||||
ExitCode.USAGE,
|
||||
`${this.cliName} --help`,
|
||||
);
|
||||
return {
|
||||
kind: "unknown",
|
||||
error: new UsageError(
|
||||
`Unknown command: ${this.cliName} ${commandPath.join(" ")}`,
|
||||
`${this.cliName} --help`,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
private buildResourceLines(a: (s: string) => string, d: (s: string) => string): string {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { Command } from "bailian-cli-core";
|
||||
import { type UsageError } from "bailian-cli-core";
|
||||
import type { CommandRegistry } from "./registry.ts";
|
||||
import { parsePath } from "./args.ts";
|
||||
|
||||
/**
|
||||
* What an invocation resolves to — "what to do" expressed as data, not control
|
||||
* flow. A single pure function (`resolve`) produces one of these; `createCli`'s
|
||||
* dispatch switches on it. No scattered `argv.includes(...)` + `process.exit`.
|
||||
* - version: print the version and stop.
|
||||
* - help: print help for `path` (root [] / a group / an explicit --help). Terminal.
|
||||
* - run: execute `command`; `rest` is the flag region for parseFlags.
|
||||
* - usageError: the path doesn't exist (or has unexpected args); render and stop.
|
||||
*/
|
||||
export type Resolution =
|
||||
| { kind: "version" }
|
||||
| { kind: "help"; path: string[] }
|
||||
| { kind: "run"; path: string[]; command: Command; rest: string[] }
|
||||
| { kind: "usageError"; error: UsageError };
|
||||
|
||||
/**
|
||||
* Classify argv into a {@link Resolution}. Pure over (argv, registry): one
|
||||
* `parsePath` scan for the command path + flag region, one `registry.locate`
|
||||
* for the routing decision. Never throws. Trivially unit-testable.
|
||||
*/
|
||||
export function resolve(argv: string[], registry: CommandRegistry): Resolution {
|
||||
const { path, rest, hasHelpFlag, hasVersionFlag } = parsePath(argv);
|
||||
if (hasVersionFlag) return { kind: "version" };
|
||||
|
||||
const target = registry.locate(path);
|
||||
switch (target.kind) {
|
||||
case "leaf":
|
||||
return hasHelpFlag
|
||||
? { kind: "help", path: target.matched }
|
||||
: { kind: "run", path: target.matched, command: target.command, rest };
|
||||
case "group":
|
||||
return { kind: "help", path: target.matched };
|
||||
case "unknown":
|
||||
return { kind: "usageError", error: target.error };
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/** Current command path (e.g. `["auth","login"]`) for help-on-missing; set by `main` before `execute`. */
|
||||
let executingCommandPath: string[] = [];
|
||||
|
||||
let printCommandHelpImpl: ((commandPath: string[], out: NodeJS.WriteStream) => void) | null = null;
|
||||
|
||||
export function setExecutingCommandPath(path: string[]): void {
|
||||
executingCommandPath = path;
|
||||
}
|
||||
|
||||
export function getExecutingCommandPath(): string[] {
|
||||
return executingCommandPath;
|
||||
}
|
||||
|
||||
export function registerCommandHelpPrinter(
|
||||
fn: (commandPath: string[], out: NodeJS.WriteStream) => void,
|
||||
): void {
|
||||
printCommandHelpImpl = fn;
|
||||
}
|
||||
|
||||
/** Print help for the command currently being executed (must call `setExecutingCommandPath` first). */
|
||||
export function printCurrentCommandHelp(out: NodeJS.WriteStream = process.stderr): void {
|
||||
if (printCommandHelpImpl && executingCommandPath.length > 0) {
|
||||
printCommandHelpImpl(executingCommandPath, out);
|
||||
}
|
||||
}
|
||||
@@ -1,95 +1,140 @@
|
||||
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";
|
||||
import { ExitCode, GLOBAL_OPTIONS, type OptionDef } from "bailian-cli-core";
|
||||
import { parsePath, parseFlags } from "../src/args.ts";
|
||||
|
||||
const IMAGE_GENERATE_OPTIONS = [
|
||||
const IMAGE_GENERATE_OPTIONS: OptionDef[] = [
|
||||
{ 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" },
|
||||
{ flag: "--image <url>", description: "Image URL (repeatable)", type: "array" },
|
||||
{ flag: "--n <count>", description: "Number of images", type: "number" },
|
||||
{ flag: "--watermark <bool>", description: "Watermark", type: "boolean" },
|
||||
{ flag: "--no-wait", description: "Return immediately", type: "switch" },
|
||||
];
|
||||
const OPTS = [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS];
|
||||
|
||||
// ---- parsePath: routing only (command path first, then flags) ----
|
||||
|
||||
test("parsePath splits leading bare tokens as the command path", () => {
|
||||
const r = parsePath(["image", "generate", "--prompt", "cat"]);
|
||||
expect(r.path).toEqual(["image", "generate"]);
|
||||
expect(r.rest).toEqual(["--prompt", "cat"]);
|
||||
});
|
||||
|
||||
test("parsePath stops the path at the first flag", () => {
|
||||
const r = parsePath(["speech"]);
|
||||
expect(r.path).toEqual(["speech"]);
|
||||
expect(r.rest).toEqual([]);
|
||||
});
|
||||
|
||||
test("parsePath detects --help and --version in the flag region", () => {
|
||||
expect(parsePath(["image", "generate", "--help"]).hasHelpFlag).toBe(true);
|
||||
const v = parsePath(["--version"]);
|
||||
expect(v.hasVersionFlag).toBe(true);
|
||||
expect(v.path).toEqual([]);
|
||||
});
|
||||
|
||||
// ---- parseFlags: typed parsing ----
|
||||
|
||||
test("parseFlags parses string / number / switch", () => {
|
||||
const flags = parseFlags(["--prompt", "cat", "--n", "3", "--no-wait"], OPTS);
|
||||
expect(flags.prompt).toBe("cat");
|
||||
expect(flags.n).toBe(3);
|
||||
expect(flags.noWait).toBe(true);
|
||||
});
|
||||
|
||||
test("parseFlags supports the --flag=value form", () => {
|
||||
expect(parseFlags(["--prompt=cat"], OPTS).prompt).toBe("cat");
|
||||
});
|
||||
|
||||
test("parseFlags coerces boolean flags to real booleans", () => {
|
||||
expect(parseFlags(["--prompt", "x", "--watermark", "false"], OPTS).watermark).toBe(false);
|
||||
expect(parseFlags(["--prompt", "x", "--watermark=true"], OPTS).watermark).toBe(true);
|
||||
});
|
||||
|
||||
test("parseFlags collects repeated array flags", () => {
|
||||
expect(parseFlags(["--prompt", "x", "--image", "a", "--image", "b"], OPTS).image).toEqual([
|
||||
"a",
|
||||
"b",
|
||||
]);
|
||||
});
|
||||
|
||||
test("parseFlags accepts a lone - (stdin) and negative numbers as values", () => {
|
||||
expect(parseFlags(["--prompt", "x", "--model", "-"], OPTS).model).toBe("-");
|
||||
expect(parseFlags(["--prompt", "x", "--n", "-5"], OPTS).n).toBe(-5);
|
||||
});
|
||||
|
||||
// ---- parseFlags: validation (all UsageError = exit 2) ----
|
||||
|
||||
test("parseFlags rejects a non true/false boolean value", () => {
|
||||
expect(() => parseFlags(["--prompt", "x", "--watermark", "yes"], OPTS)).toThrowError(
|
||||
expect.objectContaining({
|
||||
name: "UsageError",
|
||||
message: expect.stringContaining("true or false"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("parseFlags rejects a repeated non-array flag", () => {
|
||||
expect(() => parseFlags(["--prompt", "a", "--prompt", "b"], OPTS)).toThrowError(
|
||||
expect.objectContaining({
|
||||
name: "UsageError",
|
||||
message: expect.stringContaining("more than once"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("parseFlags rejects a switch given a value", () => {
|
||||
expect(() => parseFlags(["--prompt", "x", "--no-wait=true"], OPTS)).toThrowError(
|
||||
expect.objectContaining({
|
||||
name: "UsageError",
|
||||
message: expect.stringContaining("takes no value"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("parseFlags rejects unknown long flags", () => {
|
||||
expect(() =>
|
||||
parseFlags(["--prompt", "cat", "--xxxx", "a"], [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS]),
|
||||
).toThrowError(
|
||||
expect(() => parseFlags(["--prompt", "cat", "--xxxx", "a"], OPTS)).toThrowError(
|
||||
expect.objectContaining({
|
||||
name: "BailianError",
|
||||
name: "UsageError",
|
||||
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],
|
||||
test("parseFlags rejects short flags", () => {
|
||||
expect(() => parseFlags(["--prompt", "x", "-h"], OPTS)).toThrowError(
|
||||
expect.objectContaining({ name: "UsageError" }),
|
||||
);
|
||||
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(
|
||||
test("parseFlags rejects unexpected bare tokens (no positionals)", () => {
|
||||
expect(() => parseFlags(["--prompt", "x", "stray"], OPTS)).toThrowError(
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining("Flag --watermark requires a value"),
|
||||
name: "UsageError",
|
||||
message: expect.stringContaining("Unexpected argument"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
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,
|
||||
test("parseFlags rejects a value flag whose value is missing", () => {
|
||||
expect(() => parseFlags(["--prompt", "cat", "--model"], OPTS)).toThrowError(
|
||||
expect.objectContaining({ message: expect.stringContaining("Flag --model requires a value") }),
|
||||
);
|
||||
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(
|
||||
test("parseFlags validates number flags", () => {
|
||||
expect(() => parseFlags(["--prompt", "x", "--n", "abc"], OPTS)).toThrowError(
|
||||
expect.objectContaining({ message: expect.stringContaining("finite number") }),
|
||||
);
|
||||
});
|
||||
|
||||
test("parseFlags throws IncompleteCommandError when a required flag is missing", () => {
|
||||
expect(() => parseFlags(["--model", "qwen-image-2.0"], OPTS)).toThrowError(
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining("Flag --prompt requires a value"),
|
||||
name: "IncompleteCommandError",
|
||||
exitCode: ExitCode.SUCCESS,
|
||||
message: expect.stringContaining("Missing required flag: --prompt"),
|
||||
}),
|
||||
);
|
||||
// --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");
|
||||
});
|
||||
|
||||
@@ -19,15 +19,13 @@ Index: [index.md](index.md)
|
||||
| --------------- | ---------------------------------------------------------------------------------------------- |
|
||||
| **Name** | `advisor recommend` |
|
||||
| **Description** | Recommend the best models for your use case (intent analysis → candidate recall → LLM ranking) |
|
||||
| **Usage** | `bl advisor recommend <prompt> [flags]` |
|
||||
| **Usage** | `bl advisor recommend --message <text> [flags]` |
|
||||
|
||||
#### Options
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------- | ------- | -------- | ------------------------------------------------------------- |
|
||||
| `--message <text>` | string | no | Describe your requirements (alternative to positional prompt) |
|
||||
| `--dry-run` | boolean | no | Show intent analysis and candidate list without LLM ranking |
|
||||
| `--output <format>` | string | no | Output format: text (default in TTY), json, yaml |
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------ | ------ | -------- | -------------------------- |
|
||||
| `--message <text>` | string | yes | Describe your requirements |
|
||||
|
||||
#### Examples
|
||||
|
||||
@@ -50,7 +48,3 @@ bl advisor recommend --message "Low-cost high-concurrency online customer servic
|
||||
```bash
|
||||
bl advisor recommend --message "Long document summarization" --dry-run
|
||||
```
|
||||
|
||||
```bash
|
||||
bl advisor recommend # Interactive input
|
||||
```
|
||||
|
||||
@@ -26,8 +26,8 @@ Index: [index.md](index.md)
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ----------------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--key <key>` | string | no | Config key (base*url, output, output_dir, timeout, api_key, access_token, default*\*\_model, access_key_id, access_key_secret, workspace_id) |
|
||||
| `--value <value>` | string | no | Value to set |
|
||||
| `--key <key>` | string | yes | Config key (base*url, output, output_dir, timeout, api_key, access_token, default*\*\_model, access_key_id, access_key_secret, workspace_id) |
|
||||
| `--value <value>` | string | yes | Value to set |
|
||||
|
||||
#### Examples
|
||||
|
||||
|
||||
@@ -24,19 +24,19 @@ Index: [index.md](index.md)
|
||||
|
||||
#### Options
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| -------------------------- | ------ | -------- | ----------------------------------------------------------------------- |
|
||||
| `--image <url>` | array | yes | Source image URL or local file path (repeatable for multi-image merge) |
|
||||
| `--prompt <text>` | string | yes | Edit instruction text |
|
||||
| `--model <model>` | string | no | Model ID (default: qwen-image-2.0) |
|
||||
| `--size <W*H>` | string | no | Output image size: ratio (3:4, 16:9) or pixels (2048\*2048) |
|
||||
| `--n <count>` | number | no | Number of images (default: 1, max: 6) |
|
||||
| `--seed <n>` | number | no | Random seed for reproducible results |
|
||||
| `--negative-prompt <text>` | string | no | Negative prompt to exclude unwanted content |
|
||||
| `--prompt-extend <bool>` | string | no | Enable prompt extend (true/false). Omit flag to use CLI default (true). |
|
||||
| `--watermark <bool>` | string | no | Enable watermark (true/false). Omit flag to use CLI default (true). |
|
||||
| `--out-dir <dir>` | string | no | Download images to directory |
|
||||
| `--out-prefix <prefix>` | string | no | Filename prefix (default: edited) |
|
||||
| Flag | Type | Required | Description |
|
||||
| -------------------------- | ------- | -------- | ----------------------------------------------------------------------- |
|
||||
| `--image <url>` | array | yes | Source image URL or local file path (repeatable for multi-image merge) |
|
||||
| `--prompt <text>` | string | yes | Edit instruction text |
|
||||
| `--model <model>` | string | no | Model ID (default: qwen-image-2.0) |
|
||||
| `--size <W*H>` | string | no | Output image size: ratio (3:4, 16:9) or pixels (2048\*2048) |
|
||||
| `--n <count>` | number | no | Number of images (default: 1, max: 6) |
|
||||
| `--seed <n>` | number | no | Random seed for reproducible results |
|
||||
| `--negative-prompt <text>` | string | no | Negative prompt to exclude unwanted content |
|
||||
| `--prompt-extend <bool>` | boolean | no | Enable prompt extend (true/false). Omit flag to use CLI default (true). |
|
||||
| `--watermark <bool>` | boolean | no | Enable watermark (true/false). Omit flag to use CLI default (true). |
|
||||
| `--out-dir <dir>` | string | no | Download images to directory |
|
||||
| `--out-prefix <prefix>` | string | no | Filename prefix (default: edited) |
|
||||
|
||||
#### Examples
|
||||
|
||||
@@ -78,8 +78,8 @@ bl image edit --image ./photo.png --prompt "Replace the background with a beach"
|
||||
| `--n <count>` | number | no | Number of images per request (default: 1, max: 6) |
|
||||
| `--seed <n>` | number | no | Random seed for reproducible generation |
|
||||
| `--negative-prompt <text>` | string | no | Negative prompt to exclude unwanted content |
|
||||
| `--prompt-extend <bool>` | string | no | Enable prompt extend (true/false). Omit flag: true for qwen-image sync; parameter omitted on async models (API default). |
|
||||
| `--watermark <bool>` | string | no | Enable watermark (true/false). Omit flag to use CLI default (true). |
|
||||
| `--prompt-extend <bool>` | boolean | no | Enable prompt extend (true/false). Omit flag: true for qwen-image sync; parameter omitted on async models (API default). |
|
||||
| `--watermark <bool>` | boolean | no | Enable watermark (true/false). Omit flag to use CLI default (true). |
|
||||
| `--no-wait` | boolean | no | Return task ID immediately without waiting (async models only) |
|
||||
| `--out-dir <dir>` | string | no | Download images to directory |
|
||||
| `--out-prefix <prefix>` | string | no | Filename prefix (default: image) |
|
||||
|
||||
@@ -86,23 +86,23 @@ Use this index for the full quick index and global flags.
|
||||
|
||||
Available on every command (in addition to command-specific options):
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------------------ | ------- | -------- | -------------------------------------------------------- |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
| `--output <format>` | string | no | Output format: text, json |
|
||||
| `--timeout <seconds>` | number | no | Request timeout |
|
||||
| `--quiet` | boolean | no | Suppress non-essential output |
|
||||
| `--verbose` | boolean | no | Print HTTP request/response details |
|
||||
| `--no-color` | boolean | no | Disable ANSI colors |
|
||||
| `--dry-run` | boolean | no | Dry run mode |
|
||||
| `--non-interactive` | boolean | no | Disable interactive prompts |
|
||||
| `--concurrent <n>` | number | no | Run N parallel requests (default: 1) |
|
||||
| `--console-region <region>` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) |
|
||||
| `--console-site <site>` | string | no | Console site: domestic, international |
|
||||
| `--console-switch-agent <uid>` | number | no | Switch agent UID for delegated access |
|
||||
| `--help` | boolean | no | Show help |
|
||||
| `--version` | boolean | no | Print version |
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------------------ | ------ | -------- | -------------------------------------------------------- |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
| `--output <format>` | string | no | Output format: text, json |
|
||||
| `--timeout <seconds>` | number | no | Request timeout |
|
||||
| `--quiet` | switch | no | Suppress non-essential output |
|
||||
| `--verbose` | switch | no | Print HTTP request/response details |
|
||||
| `--no-color` | switch | no | Disable ANSI colors |
|
||||
| `--dry-run` | switch | no | Dry run mode |
|
||||
| `--non-interactive` | switch | no | Disable interactive prompts |
|
||||
| `--concurrent <n>` | number | no | Run N parallel requests (default: 1) |
|
||||
| `--console-region <region>` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) |
|
||||
| `--console-site <site>` | string | no | Console site: domestic, international |
|
||||
| `--console-switch-agent <uid>` | number | no | Switch agent UID for delegated access |
|
||||
| `--help` | switch | no | Show help |
|
||||
| `--version` | switch | no | Print version |
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
@@ -17,34 +17,34 @@ Index: [index.md](index.md)
|
||||
|
||||
### `bl mcp call`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | --------------------------------------------------------------------------------- |
|
||||
| **Name** | `mcp call` |
|
||||
| **Description** | Call a tool on an MCP server (tools/call) |
|
||||
| **Usage** | `bl mcp call <server-code>.<tool> [--arg k=v ...] [--json '{...}'] [--url <url>]` |
|
||||
| Field | Value |
|
||||
| --------------- | ----------------------------------------------------------------------------------- |
|
||||
| **Name** | `mcp call` |
|
||||
| **Description** | Call a tool on an MCP server (tools/call) |
|
||||
| **Usage** | `bl mcp call --target <server.tool> [--arg k=v ...] [--json '{...}'] [--url <url>]` |
|
||||
|
||||
#### Options
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ---------------------- | ------ | -------- | ---------------------------------------------------------------------------------------- |
|
||||
| `<server-code>.<tool>` | string | yes | Server code and tool name joined by a dot, e.g. market-cmapi00073529.SmartStockSelection |
|
||||
| `--arg <kv>` | array | no | Tool argument (repeatable). Values parsed as JSON if possible, else string. |
|
||||
| `--json <obj>` | string | no | Full arguments object as JSON; merged with --arg (arg wins). |
|
||||
| `--query <text>` | string | no | Shortcut for --arg query=<text> (mirrors many DashScope MCP tools). |
|
||||
| `--url <url>` | string | no | Override the MCP endpoint URL (for non-Bailian servers) |
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------------ | ------ | -------- | ---------------------------------------------------------------------------------------- |
|
||||
| `--target <server.tool>` | string | yes | Server code and tool name joined by a dot, e.g. market-cmapi00073529.SmartStockSelection |
|
||||
| `--arg <kv>` | array | no | Tool argument (repeatable). Values parsed as JSON if possible, else string. |
|
||||
| `--json <obj>` | string | no | Full arguments object as JSON; merged with --arg (arg wins). |
|
||||
| `--query <text>` | string | no | Shortcut for --arg query=<text> (mirrors many DashScope MCP tools). |
|
||||
| `--url <url>` | string | no | Override the MCP endpoint URL (for non-Bailian servers) |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl mcp call market-cmapi00073529.SmartStockSelection --query "Screen consumer stocks with ROE > 15%"
|
||||
bl mcp call --target market-cmapi00073529.SmartStockSelection --query "Screen consumer stocks with ROE > 15%"
|
||||
```
|
||||
|
||||
```bash
|
||||
bl mcp call market-cmapi00073529.FinQuery --json '{"q":"Guizhou Maotai","limit":5}'
|
||||
bl mcp call --target market-cmapi00073529.FinQuery --json '{"q":"Guizhou Maotai","limit":5}'
|
||||
```
|
||||
|
||||
```bash
|
||||
bl mcp call market-cmapi00073529.SmartFundSelection --arg riskLevel=R3 --arg minScale=10
|
||||
bl mcp call --target market-cmapi00073529.SmartFundSelection --arg riskLevel=R3 --arg minScale=10
|
||||
```
|
||||
|
||||
### `bl mcp list`
|
||||
@@ -87,25 +87,25 @@ bl mcp list --output json
|
||||
| --------------- | ------------------------------------------------ |
|
||||
| **Name** | `mcp tools` |
|
||||
| **Description** | List tools exposed by an MCP server (tools/list) |
|
||||
| **Usage** | `bl mcp tools <server-code> [--url <url>]` |
|
||||
| **Usage** | `bl mcp tools --server <code> [--url <url>]` |
|
||||
|
||||
#### Options
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| --------------- | ------ | -------- | ------------------------------------------------------- |
|
||||
| `<server-code>` | string | yes | Server code from `mcp list` (e.g. market-cmapi00073529) |
|
||||
| `--url <url>` | string | no | Override the MCP endpoint URL (for non-Bailian servers) |
|
||||
| Flag | Type | Required | Description |
|
||||
| ----------------- | ------ | -------- | ------------------------------------------------------- |
|
||||
| `--server <code>` | string | yes | Server code from `mcp list` (e.g. market-cmapi00073529) |
|
||||
| `--url <url>` | string | no | Override the MCP endpoint URL (for non-Bailian servers) |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl mcp tools market-cmapi00073529
|
||||
bl mcp tools --server market-cmapi00073529
|
||||
```
|
||||
|
||||
```bash
|
||||
bl mcp tools market-cmapi00073529 --output json
|
||||
bl mcp tools --server market-cmapi00073529 --output json
|
||||
```
|
||||
|
||||
```bash
|
||||
bl mcp tools my-server --url https://example.com/mcp
|
||||
bl mcp tools --server my-server --url https://example.com/mcp
|
||||
```
|
||||
|
||||
@@ -16,42 +16,43 @@ Index: [index.md](index.md)
|
||||
|
||||
### `bl pipeline run`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ---------------------------------- |
|
||||
| **Name** | `pipeline run` |
|
||||
| **Description** | Run a pipeline workflow definition |
|
||||
| **Usage** | `bl pipeline run <file> [flags]` |
|
||||
| Field | Value |
|
||||
| --------------- | --------------------------------------- |
|
||||
| **Name** | `pipeline run` |
|
||||
| **Description** | Run a pipeline workflow definition |
|
||||
| **Usage** | `bl pipeline run --file <path> [flags]` |
|
||||
|
||||
#### Options
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| --------------------- | ------ | -------- | ------------------------------- |
|
||||
| `--input <json>` | string | no | Runtime input as inline JSON |
|
||||
| `--input-file <path>` | string | no | Runtime input from a JSON file |
|
||||
| `--concurrency <n>` | number | no | Max parallel steps (default: 1) |
|
||||
| `--events <format>` | string | no | Emit lifecycle events: jsonl |
|
||||
| `--timeout <seconds>` | number | no | Default step timeout in seconds |
|
||||
| Flag | Type | Required | Description |
|
||||
| --------------------- | ------ | -------- | ------------------------------------ |
|
||||
| `--file <path>` | string | yes | Pipeline definition file (YAML/JSON) |
|
||||
| `--input <json>` | string | no | Runtime input as inline JSON |
|
||||
| `--input-file <path>` | string | no | Runtime input from a JSON file |
|
||||
| `--concurrency <n>` | number | no | Max parallel steps (default: 1) |
|
||||
| `--events <format>` | string | no | Emit lifecycle events: jsonl |
|
||||
| `--timeout <seconds>` | number | no | Default step timeout in seconds |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl pipeline run workflow.yaml --input '{"brief":"hello"}'
|
||||
bl pipeline run --file workflow.yaml --input '{"brief":"hello"}'
|
||||
```
|
||||
|
||||
```bash
|
||||
bl pipeline run workflow.json --input-file inputs.json --concurrency 3
|
||||
bl pipeline run --file workflow.json --input-file inputs.json --concurrency 3
|
||||
```
|
||||
|
||||
```bash
|
||||
bl pipeline run workflow.yaml --dry-run
|
||||
bl pipeline run --file workflow.yaml --dry-run
|
||||
```
|
||||
|
||||
```bash
|
||||
bl pipeline run workflow.json --events jsonl
|
||||
bl pipeline run --file workflow.json --events jsonl
|
||||
```
|
||||
|
||||
```bash
|
||||
bl pipeline run workflow.yaml --output json
|
||||
bl pipeline run --file workflow.yaml --output json
|
||||
```
|
||||
|
||||
### `bl pipeline validate`
|
||||
@@ -60,18 +61,20 @@ bl pipeline run workflow.yaml --output json
|
||||
| --------------- | ------------------------------------------------ |
|
||||
| **Name** | `pipeline validate` |
|
||||
| **Description** | Validate a pipeline definition without executing |
|
||||
| **Usage** | `bl pipeline validate <file>` |
|
||||
| **Usage** | `bl pipeline validate --file <path>` |
|
||||
|
||||
#### Options
|
||||
|
||||
_No command-specific options._
|
||||
| Flag | Type | Required | Description |
|
||||
| --------------- | ------ | -------- | ------------------------------------ |
|
||||
| `--file <path>` | string | yes | Pipeline definition file (YAML/JSON) |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl pipeline validate workflow.yaml
|
||||
bl pipeline validate --file workflow.yaml
|
||||
```
|
||||
|
||||
```bash
|
||||
bl pipeline validate workflow.json --output json
|
||||
bl pipeline validate --file workflow.json --output json
|
||||
```
|
||||
|
||||
@@ -25,7 +25,7 @@ Index: [index.md](index.md)
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ---------------- | ------- | -------- | -------------------------------------- |
|
||||
| `--query <text>` | string | yes | Search query text |
|
||||
| `--query <text>` | string | no | Search query text |
|
||||
| `--count <n>` | number | no | Number of search results (default: 10) |
|
||||
| `--list-tools` | boolean | no | List available MCP tools and exit |
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ bl speech recognize --url https://example.com/audio.mp3 --no-wait --quiet
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ---------------------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--text <text>` | string | yes | Text to synthesize into speech |
|
||||
| `--text <text>` | string | no | Text to synthesize into speech (or use --text-file) |
|
||||
| `--text-file <path>` | string | no | Read text from a file instead of --text |
|
||||
| `--model <model>` | string | no | Model ID (default: cosyvoice-v3-flash). System voices available for cosyvoice-v3-flash |
|
||||
| `--voice <voice>` | string | no | Voice ID. Use --list-voices to see system voices for cosyvoice-v3-flash; for v3.5-flash provide a clone/design voice ID |
|
||||
|
||||
@@ -23,19 +23,19 @@ Index: [index.md](index.md)
|
||||
|
||||
#### Options
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------------ | ------- | -------- | ----------------------------------------------------- |
|
||||
| `--model <model>` | string | no | Model ID (default: qwen3.7-max) |
|
||||
| `--message <text>` | array | yes | Message text (repeatable, prefix role: to set role) |
|
||||
| `--messages-file <path>` | string | no | JSON file with messages array (use - for stdin) |
|
||||
| `--system <text>` | string | no | System prompt |
|
||||
| `--max-tokens <n>` | number | no | Maximum tokens to generate (default: 4096) |
|
||||
| `--temperature <n>` | number | no | Sampling temperature (0.0, 2.0] |
|
||||
| `--top-p <n>` | number | no | Nucleus sampling threshold |
|
||||
| `--stream` | boolean | no | Stream response tokens (default: on in TTY) |
|
||||
| `--tool <json-or-path>` | array | no | Tool definition as JSON or file path (repeatable) |
|
||||
| `--enable-thinking` | boolean | no | Enable thinking/reasoning mode (for qwen3/qwq models) |
|
||||
| `--thinking-budget <n>` | number | no | Max tokens for thinking (default: 4096) |
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------------ | ------- | -------- | --------------------------------------------------------------------------- |
|
||||
| `--model <model>` | string | no | Model ID (default: qwen3.7-max) |
|
||||
| `--message <text>` | array | no | Message text (repeatable, prefix role: to set role); or use --messages-file |
|
||||
| `--messages-file <path>` | string | no | JSON file with messages array (use - for stdin) |
|
||||
| `--system <text>` | string | no | System prompt |
|
||||
| `--max-tokens <n>` | number | no | Maximum tokens to generate (default: 4096) |
|
||||
| `--temperature <n>` | number | no | Sampling temperature (0.0, 2.0] |
|
||||
| `--top-p <n>` | number | no | Nucleus sampling threshold |
|
||||
| `--stream` | boolean | no | Stream response tokens (default: on in TTY) |
|
||||
| `--tool <json-or-path>` | array | no | Tool definition as JSON or file path (repeatable) |
|
||||
| `--enable-thinking` | boolean | no | Enable thinking/reasoning mode (for qwen3/qwq models) |
|
||||
| `--thinking-budget <n>` | number | no | Max tokens for thinking (default: 4096) |
|
||||
|
||||
#### Examples
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@ Index: [index.md](index.md)
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ---------------- | ------ | -------- | ------------------------ |
|
||||
| `--task-id <id>` | string | no | Task ID to download from |
|
||||
| `--out <path>` | string | no | Output file path |
|
||||
| `--task-id <id>` | string | yes | Task ID to download from |
|
||||
| `--out <path>` | string | yes | Output file path |
|
||||
|
||||
#### Examples
|
||||
|
||||
@@ -63,8 +63,8 @@ bl video download --task-id 3b256896-xxxx --out video.mp4 --quiet
|
||||
| `--ratio <ratio>` | string | no | Aspect ratio (16:9, 9:16, 1:1, 4:3, 3:4) |
|
||||
| `--duration <seconds>` | number | no | Output video duration in seconds (2-10) |
|
||||
| `--audio-setting <mode>` | string | no | Audio: auto (default) or origin (keep original) |
|
||||
| `--prompt-extend <bool>` | string | no | Enable prompt extend (true/false). Omit flag to omit the parameter (DashScope default). |
|
||||
| `--watermark <bool>` | string | no | Enable watermark (true/false). Omit flag to use CLI default (true). |
|
||||
| `--prompt-extend <bool>` | boolean | no | Enable prompt extend (true/false). Omit flag to omit the parameter (DashScope default). |
|
||||
| `--watermark <bool>` | boolean | no | Enable watermark (true/false). Omit flag to use CLI default (true). |
|
||||
| `--seed <n>` | number | no | Random seed for reproducible generation |
|
||||
| `--download <path>` | string | no | Save video to file on completion |
|
||||
| `--no-wait` | boolean | no | Return task ID immediately without waiting |
|
||||
@@ -108,8 +108,8 @@ bl video edit --video https://example.com/input.mp4 --prompt "Put clothes on the
|
||||
| `--resolution <res>` | string | no | Resolution: 720P or 1080P (default: 1080P) |
|
||||
| `--ratio <ratio>` | string | no | Aspect ratio (e.g. 16:9, 9:16, 1:1) |
|
||||
| `--duration <seconds>` | number | no | Video duration in seconds (default: 5) |
|
||||
| `--prompt-extend <bool>` | string | no | Enable prompt extend (true/false). Omit flag to omit the parameter (DashScope default). |
|
||||
| `--watermark <bool>` | string | no | Enable watermark (true/false). Omit flag to use CLI default (true). |
|
||||
| `--prompt-extend <bool>` | boolean | no | Enable prompt extend (true/false). Omit flag to omit the parameter (DashScope default). |
|
||||
| `--watermark <bool>` | boolean | no | Enable watermark (true/false). Omit flag to use CLI default (true). |
|
||||
| `--seed <n>` | number | no | Random seed for reproducible generation |
|
||||
| `--download <path>` | string | no | Save video to file on completion |
|
||||
| `--no-wait` | boolean | no | Return task ID immediately without waiting |
|
||||
@@ -159,8 +159,8 @@ bl video generate --prompt "A cat playing with a ball" --watermark false
|
||||
| `--resolution <res>` | string | no | Resolution: 720P or 1080P (default: 1080P) |
|
||||
| `--ratio <ratio>` | string | no | Aspect ratio (16:9, 9:16, 1:1) |
|
||||
| `--duration <seconds>` | number | no | Video duration in seconds (default: 5) |
|
||||
| `--prompt-extend <bool>` | string | no | Enable prompt extend (true/false). Omit flag to omit the parameter (DashScope default). |
|
||||
| `--watermark <bool>` | string | no | Enable watermark (true/false). Omit flag to use CLI default (true). |
|
||||
| `--prompt-extend <bool>` | boolean | no | Enable prompt extend (true/false). Omit flag to omit the parameter (DashScope default). |
|
||||
| `--watermark <bool>` | boolean | no | Enable watermark (true/false). Omit flag to use CLI default (true). |
|
||||
| `--seed <n>` | number | no | Random seed for reproducible generation |
|
||||
| `--download <path>` | string | no | Save video to file on completion |
|
||||
| `--no-wait` | boolean | no | Return task ID immediately without waiting |
|
||||
@@ -201,7 +201,7 @@ bl video ref --prompt "Image 1 drinks water" --image person.jpg --watermark fals
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ---------------- | ------ | -------- | ------------- |
|
||||
| `--task-id <id>` | string | no | Async task ID |
|
||||
| `--task-id <id>` | string | yes | Async task ID |
|
||||
|
||||
#### Examples
|
||||
|
||||
|
||||
Reference in New Issue
Block a user