mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a78ed7ffe8 | |||
| 5a1dfa5227 | |||
| b5dec926b7 | |||
| 9e6f5ca9cb | |||
| 3f78de2dc1 | |||
| 38926aeb22 | |||
| e3f2996291 | |||
| 59250c4f44 | |||
| 98ebeb398c |
@@ -6,6 +6,16 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
||||
|
||||
[中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md)
|
||||
|
||||
## [1.18.2] - 2026-09-01
|
||||
|
||||
### Changed
|
||||
|
||||
- **Confirmation before deleting or clearing resources** — `bl finetune delete`, `bl deploy delete`, `bl dataset delete`, and `bl quota update --delete` now ask for confirmation; pass `--yes` for non-interactive use.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Skill installation reliability** — `bl skill init` now retries transient network failures, and completed Skill updates are no longer reported as failed when backup cleanup is blocked.
|
||||
|
||||
## [1.18.1] - 2026-08-28
|
||||
|
||||
### Removed
|
||||
|
||||
@@ -6,6 +6,16 @@
|
||||
|
||||
[English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md)
|
||||
|
||||
## [1.18.2] - 2026-09-01
|
||||
|
||||
### 变更
|
||||
|
||||
- **删除与清除操作增加确认** —— `bl finetune delete`、`bl deploy delete`、`bl dataset delete` 和 `bl quota update --delete` 现在会在执行前要求确认;非交互场景请传入 `--yes`。
|
||||
|
||||
### 修复
|
||||
|
||||
- **Skill 安装可靠性** —— `bl skill init` 现在会重试临时性网络故障;备份清理受阻时,已完成的 Skill 更新不再被误报为失败。
|
||||
|
||||
## [1.18.1] - 2026-08-28
|
||||
|
||||
### 已移除
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
|
||||
## 架构分层
|
||||
|
||||
| 层级 | 路径 | 测什么 |
|
||||
| --------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| **共享基建** | `packages/e2e` | gating、子进程 runner、output、globalSetup(`private`,不发布) |
|
||||
| **命令 E2E** | `packages/commands/tests/e2e` | help、缺参、dry-run、live(gated);每用例最小路由 |
|
||||
| **Journey E2E** | `packages/commands/tests/e2e/knowledge/journeys` | 用户旅程全链路(跨命令回路 + 标记词召回闭环),全部 live gated;见 `journeys/README.md` |
|
||||
| **bl smoke** | `packages/cli/tests/e2e/registry.smoke.e2e.test.ts` | 产品 map 全部 path `--help`、分组 help、根 help |
|
||||
| **kscli smoke** | `packages/kscli/tests/e2e/registry.smoke.e2e.test.ts` | 从 `kscli/src/commands.ts` 推导 path/分组;identity(`--version`、`search --help` path) |
|
||||
| **runtime** | `packages/runtime/tests` | `proxy.e2e`、console 跨域 flag 拒绝 |
|
||||
| 层级 | 路径 | 测什么 |
|
||||
| --------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
||||
| **共享基建** | `packages/e2e` | gating、子进程 runner、registry help 捕获、output、globalSetup(`private`,不发布) |
|
||||
| **命令 E2E** | `packages/commands/tests/e2e` | 进程内 help、子进程缺参/dry-run/live(gated);每用例最小路由 |
|
||||
| **Journey E2E** | `packages/commands/tests/e2e/knowledge/journeys` | 用户旅程全链路(跨命令回路 + 标记词召回闭环),全部 live gated;见 `journeys/README.md` |
|
||||
| **bl smoke** | `packages/cli/tests/e2e/registry.smoke.e2e.test.ts` | 产品 map 全部 path/分组的进程内 help;根 help、鉴权域等代表性子进程冒烟 |
|
||||
| **kscli smoke** | `packages/kscli/tests/e2e/registry.smoke.e2e.test.ts` | map 全部 path/分组的进程内 help;`--version`、`search --help` 等代表性子进程冒烟 |
|
||||
| **runtime** | `packages/runtime/tests` | `proxy.e2e`、console 跨域 flag 拒绝 |
|
||||
|
||||
**依赖边界**:`e2e` → `core`;`commands/tests` → `e2e` + `commands/src`;产品 tests → `e2e` + 各自 `src`。**禁止**产品 import `commands/tests/**`(子进程 spawn harness 路径除外)。
|
||||
**依赖边界**:`e2e` → `core`;`commands/tests` → `e2e` + `commands/src`;产品 tests → `e2e` + 各自 `src` + `runtime` 公共 API。**禁止**产品 import `commands/tests/**`(子进程 spawn harness 路径除外)。
|
||||
|
||||
## 触发条件
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
### commands E2E
|
||||
|
||||
- 路径:`packages/commands/tests/e2e/<kebab-topic>.e2e.test.ts`;knowledge 领域集中在 `packages/commands/tests/e2e/knowledge/` 子目录(新增 knowledge 命令测试放这里)
|
||||
- 子进程:`runCommandE2e(routes, args)` from `./helpers.ts`(spawn `harness/main.ts`,`routes` 为本 topic 最小 path → export 映射)
|
||||
- help:`runCommandHelp(routes, [...path, "--help"])` from `./helpers.ts`(当前 Vitest worker 内用真实 command + `CommandRegistry` 渲染,不启动子进程)
|
||||
- 子进程:缺参、dry-run、live 使用 `runCommandE2e(routes, args)`(spawn `harness/main.ts`,`routes` 为本 topic 最小 path → export 映射)
|
||||
- fixtures:`packages/commands/tests/e2e/fixtures/`
|
||||
- 路由常量:`topic-routes.ts`(按 topic 维护,**非**全量产品 map)
|
||||
|
||||
@@ -37,10 +38,12 @@
|
||||
|
||||
- bl:`runCli` from `packages/cli/tests/e2e/helpers.ts`
|
||||
- kscli:`runKscli` from `packages/kscli/tests/e2e/helpers.ts`
|
||||
- 全量 leaf/group help 使用产品 `commands` 创建 `CommandRegistry`,先通过 `resolve([...path, "--help"])` 检查 help 路由,再用 `captureRegistryHelp` 检查完整 Usage;禁止在 `test.each(commandPaths/groupPaths)` 中逐条启动 `tsx` 子进程
|
||||
- 真实子进程只保留根 help/version、产品身份、代表性叶子 help/鉴权域和缺参退出码等 shell/stdio/env 契约
|
||||
|
||||
### 共享
|
||||
|
||||
- gating / output / runner:`e2e/gating`、`e2e/output`、`e2e/runner`
|
||||
- gating / output / runner:`e2e/gating`、`e2e/output`、`e2e/runner`;runner 使用 `node --import tsx` 执行 TypeScript 入口,不启动 tsx CLI IPC server
|
||||
- globalSetup:根 `vite.config.ts` → `packages/e2e/src/global-setup.ts`
|
||||
- 解析 JSON stdout:`parseStdoutJson`;输出目录:`makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url))`
|
||||
- 长任务:`cliTimeoutPrefix()`;视频用例加 `test(..., 3_600_000)` 等显式超时
|
||||
@@ -48,9 +51,12 @@
|
||||
## 双层 describe(固定结构)
|
||||
|
||||
```ts
|
||||
// 1) 不 skip:--help,无密钥、无真实 API(分组 help 由 bl registry.smoke 覆盖)
|
||||
// 1) 不 skip:进程内 --help,无密钥、无真实 API(分组 help 由 bl registry.smoke 覆盖)
|
||||
describe("e2e: <topic>", () => {
|
||||
test("<subcommand> --help 正常退出", ...);
|
||||
test("<subcommand> --help 正常退出", async () => {
|
||||
const result = await runCommandHelp(FOO_ROUTES, ["foo", "bar", "--help"]);
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// 2) skipIf:缺参 / dry-run / 真实集成
|
||||
@@ -74,7 +80,7 @@ describe.skipIf(<ready>)("e2e: <topic>(DashScope …)", () => {
|
||||
|
||||
## 用例类型
|
||||
|
||||
1. **--help**:`runCommandE2e(ROUTES, [..., "--help"])` → stderr 含主要 flags
|
||||
1. **--help**:`runCommandHelp(ROUTES, [..., "--help"])` → stderr 含主要 flags;产品层另保留少量真实子进程 help 验证 shell/stdio/env
|
||||
2. **缺参**:带无害全局 flag(如 `--quiet`)且不传 required flag → `exitCode === 2`
|
||||
3. **--dry-run**:实现在联网/上传/写盘**之前**返回;断言 stdout JSON/文本
|
||||
4. **真实集成**:放在 skip 块**末尾**
|
||||
@@ -105,7 +111,7 @@ describe.skipIf(<ready>)("e2e: <topic>(DashScope …)", () => {
|
||||
- [ ] `packages/commands/src/index.ts` 导出 + `packages/cli/src/commands.ts` 暴露路径 + `topic-routes.ts` 补最小路由
|
||||
- [ ] `packages/commands/tests/e2e/<topic>.e2e.test.ts`(新建或扩展)
|
||||
- [ ] 若改了 `usageArgs` / `flags` / `exampleArgs`,跑 `pnpm --filter bailian-cli run generate:reference` 更新各 `skills/<skill>/reference/` 并提交
|
||||
- [ ] 子命令 `--help`(分组 help 由 bl `registry.smoke` 覆盖)
|
||||
- [ ] 子命令 `--help` 使用 `runCommandHelp`(分组 help 由 bl `registry.smoke` 覆盖)
|
||||
- [ ] skip 块:每个 required flag 缺参;可 dry-run 则加一条
|
||||
- [ ] 至少一条真实集成(或说明为何仅 smoke);不破坏已有集成用例顺序
|
||||
- [ ] `vp test packages/commands/tests/e2e/<file>` 通过
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli",
|
||||
"version": "1.18.1",
|
||||
"version": "1.18.2",
|
||||
"description": "CLI for Aliyun Model Studio (DashScope) AI Platform.",
|
||||
"keywords": [
|
||||
"agent",
|
||||
|
||||
@@ -181,7 +181,13 @@ function atomicSwap(tmpDir, catalogDir) {
|
||||
if (existsSync(backup) && !existsSync(catalogDir)) renameSync(backup, catalogDir);
|
||||
throw err;
|
||||
}
|
||||
if (existsSync(backup)) rmSync(backup, { recursive: true, force: true });
|
||||
// Best-effort cleanup (symmetric with core skills/extract.ts): the swap already
|
||||
// succeeded, so a backup deletion failure must not fail the pre-download
|
||||
try {
|
||||
if (existsSync(backup)) rmSync(backup, { recursive: true, force: true });
|
||||
} catch {
|
||||
/* keep the backup on disk rather than report a completed swap as failed */
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
@@ -214,7 +220,11 @@ async function main() {
|
||||
}
|
||||
atomicSwap(tmpDir, catalogDir);
|
||||
} catch (err) {
|
||||
if (existsSync(tmpDir)) rmSync(tmpDir, { recursive: true, force: true });
|
||||
try {
|
||||
if (existsSync(tmpDir)) rmSync(tmpDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
/* cleanup must not mask the original error */
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { deriveGroupPaths } from "e2e/registry-smoke";
|
||||
import { mkdtempSync, rmSync } from "fs";
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
import { afterAll, describe, expect, test } from "vite-plus/test";
|
||||
import { captureRegistryHelp, deriveGroupPaths } from "e2e/registry-smoke";
|
||||
import { CommandRegistry, resolve } from "bailian-cli-runtime";
|
||||
import { commands } from "../../src/commands.ts";
|
||||
import { runCli } from "./helpers.ts";
|
||||
|
||||
const commandPaths = Object.keys(commands).sort();
|
||||
const groupPaths = deriveGroupPaths(commandPaths);
|
||||
const registry = new CommandRegistry(commands, "bl");
|
||||
const isolatedConfigDir = mkdtempSync(join(tmpdir(), "bl-registry-smoke-"));
|
||||
|
||||
afterAll(() => rmSync(isolatedConfigDir, { recursive: true, force: true }));
|
||||
|
||||
function runCliSmoke(args: string[]) {
|
||||
return runCli(args, { BAILIAN_CONFIG_DIR: isolatedConfigDir });
|
||||
}
|
||||
|
||||
describe("e2e: bl registry smoke", () => {
|
||||
test("根帮助展示 bl、逐命令鉴权域与全局 flag", async () => {
|
||||
const { stderr, exitCode } = await runCli(["--help"]);
|
||||
const { stderr, exitCode } = await runCliSmoke(["--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/\bbl\b/i);
|
||||
expect(stderr).not.toMatch(/COMMAND\s+AUTH\s+DESCRIPTION/);
|
||||
@@ -24,7 +36,7 @@ describe("e2e: bl registry smoke", () => {
|
||||
});
|
||||
|
||||
test("分组帮助按叶子命令展示不同鉴权域", async () => {
|
||||
const { stderr, exitCode } = await runCli(["app", "--help"]);
|
||||
const { stderr, exitCode } = await runCliSmoke(["app", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/app call\s+\[API Key\]\s+Call a Bailian application/);
|
||||
expect(stderr).toMatch(/app list\s+\[Console\]\s+List Bailian applications/);
|
||||
@@ -36,13 +48,13 @@ describe("e2e: bl registry smoke", () => {
|
||||
[["token-plan", "list-seats"], "AK/SK"],
|
||||
[["config", "show"], "No Auth"],
|
||||
] as const)("%s --help 明确展示鉴权域 %s", async (commandPath, authLabel) => {
|
||||
const { stderr, exitCode } = await runCli([...commandPath, "--help"]);
|
||||
const { stderr, exitCode } = await runCliSmoke([...commandPath, "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain(`Authentication: ${authLabel}`);
|
||||
});
|
||||
|
||||
test("quota check --help:Flags 含 console 域鉴权 flag,Global Flags 全量列出", async () => {
|
||||
const { stderr, exitCode } = await runCli(["quota", "check", "--help"]);
|
||||
const { stderr, exitCode } = await runCliSmoke(["quota", "check", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/Global Flags:/);
|
||||
expect(stderr).toMatch(/--console-region <region>/);
|
||||
@@ -52,13 +64,25 @@ describe("e2e: bl registry smoke", () => {
|
||||
expect(stderr).not.toMatch(/API region \(default: cn-beijing\)/);
|
||||
});
|
||||
|
||||
test.each(commandPaths)("已注册命令 %s --help 成功", async (path) => {
|
||||
const { stderr, exitCode } = await runCli([...path.split(" "), "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
test.each(commandPaths)("已注册命令 %s --help 成功", (path) => {
|
||||
const commandPath = path.split(" ");
|
||||
|
||||
expect(resolve([...commandPath, "--help"], registry)).toEqual({
|
||||
kind: "help",
|
||||
path: commandPath,
|
||||
});
|
||||
expect(captureRegistryHelp(registry, commandPath)).toContain(`Usage: bl ${path}`);
|
||||
});
|
||||
|
||||
test.each(groupPaths)("命令分组 %s --help 成功", async (path) => {
|
||||
const { stderr, exitCode } = await runCli([...path.split(" "), "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
test.each(groupPaths)("命令分组 %s --help 成功", (path) => {
|
||||
const commandPath = path.split(" ");
|
||||
|
||||
expect(resolve([...commandPath, "--help"], registry)).toEqual({
|
||||
kind: "help",
|
||||
path: commandPath,
|
||||
});
|
||||
expect(captureRegistryHelp(registry, commandPath)).toContain(
|
||||
`Usage: bl ${path} <command> [flags]`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli-commands",
|
||||
"version": "1.18.1",
|
||||
"version": "1.18.2",
|
||||
"description": "Command library for bailian-cli products (knowledge, memory, media, …). See https://www.npmjs.com/package/bailian-cli for usage.",
|
||||
"homepage": "https://bailian.console.aliyun.com/cli",
|
||||
"bugs": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineCommand, deleteDataset, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
|
||||
const DELETE_FLAGS = {
|
||||
fileId: {
|
||||
@@ -8,14 +8,28 @@ const DELETE_FLAGS = {
|
||||
description: { "en-US": "Dataset file ID (required)", "zh-CN": "数据集文件 ID(必填)" },
|
||||
required: true,
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: { "en-US": "Delete a dataset file by ID", "zh-CN": "通过 ID 删除数据集文件" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "--file-id <id>",
|
||||
usageArgs: "--file-id <id> [--yes]",
|
||||
flags: DELETE_FLAGS,
|
||||
exampleArgs: ["--file-id file-id-xxx", "--file-id file-id-xxx --dry-run"],
|
||||
exampleArgs: [
|
||||
"--file-id file-id-xxx",
|
||||
"--file-id file-id-xxx --dry-run",
|
||||
"--file-id file-id-xxx --yes",
|
||||
],
|
||||
notes: [
|
||||
{
|
||||
"en-US": "Irreversible — the dataset file is permanently removed.",
|
||||
"zh-CN": "该操作不可撤销——数据集文件将被永久删除。",
|
||||
},
|
||||
],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const fileId = flags.fileId;
|
||||
@@ -25,6 +39,11 @@ export default defineCommand({
|
||||
return;
|
||||
}
|
||||
|
||||
await confirmDangerousAction(
|
||||
`Delete dataset file ${fileId}.\nThe file is permanently removed. This cannot be undone.`,
|
||||
flags.yes ?? false,
|
||||
);
|
||||
|
||||
const response = await deleteDataset(ctx.client, fileId);
|
||||
|
||||
if (settings.quiet) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
ExitCode,
|
||||
type FlagsDef,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
|
||||
const DELETE_FLAGS = {
|
||||
deployedModel: {
|
||||
@@ -25,6 +25,10 @@ const DELETE_FLAGS = {
|
||||
"zh-CN": "跳过本地 STOPPED/FAILED 状态预检查",
|
||||
},
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/**
|
||||
@@ -40,9 +44,19 @@ export default defineCommand({
|
||||
"zh-CN": "删除模型部署(状态必须为 STOPPED 或 FAILED)",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "--deployed-model <id> [--skip-precheck]",
|
||||
usageArgs: "--deployed-model <id> [--skip-precheck] [--yes]",
|
||||
flags: DELETE_FLAGS,
|
||||
exampleArgs: ["--deployed-model dep-...", "--deployed-model dep-... --dry-run"],
|
||||
notes: [
|
||||
{
|
||||
"en-US": "Irreversible — the deployment is permanently destroyed.",
|
||||
"zh-CN": "该操作不可撤销——部署将被永久销毁。",
|
||||
},
|
||||
],
|
||||
exampleArgs: [
|
||||
"--deployed-model dep-...",
|
||||
"--deployed-model dep-... --dry-run",
|
||||
"--deployed-model dep-... --yes",
|
||||
],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const deployedModel = flags.deployedModel;
|
||||
@@ -73,6 +87,11 @@ export default defineCommand({
|
||||
}
|
||||
}
|
||||
|
||||
await confirmDangerousAction(
|
||||
`Delete deployment ${deployedModel}.\nThe deployment is permanently destroyed. This cannot be undone.`,
|
||||
flags.yes ?? false,
|
||||
);
|
||||
|
||||
const response = await deleteDeployment(ctx.client, deployedModel);
|
||||
|
||||
if (settings.quiet) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineCommand, deleteFineTune, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
|
||||
const DELETE_FLAGS = {
|
||||
jobId: {
|
||||
@@ -8,15 +8,23 @@ const DELETE_FLAGS = {
|
||||
description: { "en-US": "Fine-tune job ID (required)", "zh-CN": "微调任务 ID(必填)" },
|
||||
required: true,
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: { "en-US": "Delete a fine-tune job record", "zh-CN": "删除微调任务记录" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "--job-id <id>",
|
||||
usageArgs: "--job-id <id> [--yes]",
|
||||
flags: DELETE_FLAGS,
|
||||
exampleArgs: ["--job-id ft-xxx", "--job-id ft-xxx --dry-run"],
|
||||
exampleArgs: ["--job-id ft-xxx", "--job-id ft-xxx --dry-run", "--job-id ft-xxx --yes"],
|
||||
notes: [
|
||||
{
|
||||
"en-US": "Irreversible — the job record is permanently removed.",
|
||||
"zh-CN": "该操作不可撤销——任务记录将被永久删除。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"Cancel a RUNNING job first via `finetune cancel` — the platform refuses to delete jobs that are still in flight.",
|
||||
@@ -32,6 +40,11 @@ export default defineCommand({
|
||||
return;
|
||||
}
|
||||
|
||||
await confirmDangerousAction(
|
||||
`Delete fine-tune job record ${jobId}.\nThe job record is permanently removed. This cannot be undone.`,
|
||||
flags.yes ?? false,
|
||||
);
|
||||
|
||||
const response = await deleteFineTune(ctx.client, jobId);
|
||||
|
||||
if (settings.quiet) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineCommand, detectOutputFormat, modelsLimitsPath } from "bailian-cli-core";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
import { emitResult, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
import { formatNumber } from "../shared/format.ts";
|
||||
|
||||
const MINUTE_SECONDS = 60;
|
||||
@@ -10,7 +10,7 @@ export default defineCommand({
|
||||
"zh-CN": "更新模型限流配置(QPM/TPM),或使用 --delete 清除配置",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "--model <model> [--rpm <n>] [--tpm <n>] [--delete]",
|
||||
usageArgs: "--model <model> [--rpm <n>] [--tpm <n>] [--delete] [--yes]",
|
||||
flags: {
|
||||
model: {
|
||||
type: "string",
|
||||
@@ -41,11 +41,19 @@ export default defineCommand({
|
||||
"zh-CN": "清除该模型的所有自定义限流配置",
|
||||
},
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: {
|
||||
"en-US": "Skip the confirmation prompt for --delete",
|
||||
"zh-CN": "使用 --delete 时跳过确认提示",
|
||||
},
|
||||
},
|
||||
},
|
||||
exampleArgs: [
|
||||
"--model qwen-plus --rpm 60 --tpm 100000",
|
||||
"--model qwen3-max --tpm 500000",
|
||||
"--model qwen-plus --delete",
|
||||
"--model qwen-plus --delete --yes",
|
||||
"--model qwen-plus --rpm 60 --output json",
|
||||
],
|
||||
notes: [
|
||||
@@ -55,6 +63,10 @@ export default defineCommand({
|
||||
"zh-CN":
|
||||
"未指定的字段将保留当前值(服务端 OVERLAY 合并);--delete 会清除所有自定义限流配置。",
|
||||
},
|
||||
{
|
||||
"en-US": "--delete requires confirmation; pass --yes to skip the prompt in scripts.",
|
||||
"zh-CN": "--delete 需要确认;脚本中可加 --yes 跳过交互提示。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"Setting TPM without an existing QPM limit is rejected server-side — pass --rpm first or together.",
|
||||
@@ -98,6 +110,13 @@ export default defineCommand({
|
||||
return;
|
||||
}
|
||||
|
||||
if (flags.delete) {
|
||||
await confirmDangerousAction(
|
||||
`Clear all custom rate limits for model ${modelName}.\nYour custom QPM/TPM configuration will be removed.`,
|
||||
flags.yes ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
const result = await ctx.client.requestJson<{ request_id?: string }>({
|
||||
path: modelsLimitsPath(),
|
||||
method: "POST",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts";
|
||||
import { isDashScopeE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
|
||||
import { ADVISOR_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: advisor recommend", () => {
|
||||
test("advisor recommend --help exits successfully", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(ADVISOR_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(ADVISOR_ROUTES, [
|
||||
"advisor",
|
||||
"recommend",
|
||||
"--help",
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
isOpenApiE2EReady,
|
||||
makeE2eOutputDir,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { AUTH_ROUTES } from "./topic-routes.ts";
|
||||
@@ -15,7 +16,7 @@ import { AUTH_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: auth", () => {
|
||||
test("auth login --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, ["auth", "login", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(AUTH_ROUTES, ["auth", "login", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/login|api-key/i);
|
||||
expect(stderr).toMatch(/--console-site/);
|
||||
@@ -108,13 +109,13 @@ describe("e2e: auth", () => {
|
||||
});
|
||||
|
||||
test("auth logout --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, ["auth", "logout", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(AUTH_ROUTES, ["auth", "logout", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/logout|dry-run|yes/i);
|
||||
});
|
||||
|
||||
test("auth status --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, ["auth", "status", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(AUTH_ROUTES, ["auth", "status", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/status|output/i);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { expect, test } from "vite-plus/test";
|
||||
import { captureCommandHelp, runCommandHelp } from "./helpers.ts";
|
||||
import { QUOTA_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
test("captureCommandHelp 用 topic 最小路由渲染真实命令帮助", async () => {
|
||||
const helpOutput = await captureCommandHelp(QUOTA_ROUTES, ["quota", "list"]);
|
||||
|
||||
expect(helpOutput).toContain("Usage: bl quota list");
|
||||
expect(helpOutput).toContain("--model <model>");
|
||||
expect(helpOutput).toContain("--name <name>");
|
||||
expect(helpOutput).toContain("--output <format>");
|
||||
});
|
||||
|
||||
test("runCommandHelp 保持 commands E2E 的 stderr 与 exitCode 断言接口", async () => {
|
||||
const result = await runCommandHelp(QUOTA_ROUTES, ["quota", "list", "--help"]);
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toBe("");
|
||||
expect(result.stderr).toContain("Usage: bl quota list");
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import { mkdtempSync, readFileSync, rmSync, writeFileSync, existsSync } from "fs
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { parseStdoutJson, runCommandE2e } from "./helpers.ts";
|
||||
import { parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
|
||||
import { CONFIG_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
/**
|
||||
@@ -11,29 +11,29 @@ import { CONFIG_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: config", () => {
|
||||
test("config show --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, ["config", "show", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(CONFIG_ROUTES, ["config", "show", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/show|config/i);
|
||||
});
|
||||
|
||||
test("config set --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, ["config", "set", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(CONFIG_ROUTES, ["config", "set", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/set|--key|--value/i);
|
||||
});
|
||||
|
||||
test("config list/use --help 正常退出", async () => {
|
||||
const listResult = await runCommandE2e(CONFIG_ROUTES, ["config", "list", "--help"]);
|
||||
const listResult = await runCommandHelp(CONFIG_ROUTES, ["config", "list", "--help"]);
|
||||
expect(listResult.exitCode, listResult.stderr).toBe(0);
|
||||
expect(listResult.stderr).toMatch(/list|active|profile/i);
|
||||
|
||||
const useResult = await runCommandE2e(CONFIG_ROUTES, ["config", "use", "--help"]);
|
||||
const useResult = await runCommandHelp(CONFIG_ROUTES, ["config", "use", "--help"]);
|
||||
expect(useResult.exitCode, useResult.stderr).toBe(0);
|
||||
expect(useResult.stderr).toMatch(/use|--name|active/i);
|
||||
});
|
||||
|
||||
test("config ui --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, ["config", "ui", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(CONFIG_ROUTES, ["config", "ui", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/ui|--port|--no-open|web/i);
|
||||
});
|
||||
@@ -464,7 +464,7 @@ describe("e2e: config", () => {
|
||||
});
|
||||
|
||||
test("config agent --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, ["config", "agent", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(CONFIG_ROUTES, ["config", "agent", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/agent|--base-url|--model/i);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { parseStdoutJson, runCommandE2e } from "./helpers.ts";
|
||||
import { parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
|
||||
import { CONSOLE_FLAGS_DRY_RUN_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
type ConsoleDryRunMeta = {
|
||||
@@ -10,7 +10,7 @@ type ConsoleDryRunMeta = {
|
||||
|
||||
describe("e2e: console global flags (dry-run)", () => {
|
||||
test("console call --help 不暴露命令级 region/site,示例使用 --console-region", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(CONSOLE_FLAGS_DRY_RUN_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(CONSOLE_FLAGS_DRY_RUN_ROUTES, [
|
||||
"console",
|
||||
"call",
|
||||
"--help",
|
||||
@@ -24,7 +24,7 @@ describe("e2e: console global flags (dry-run)", () => {
|
||||
});
|
||||
|
||||
test("auth login --help:自有 flag 含 --console-site,不含其余 console 域 flag", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(CONSOLE_FLAGS_DRY_RUN_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(CONSOLE_FLAGS_DRY_RUN_ROUTES, [
|
||||
"auth",
|
||||
"login",
|
||||
"--help",
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { join } from "path";
|
||||
import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e, e2eFixturesDir } from "./helpers.ts";
|
||||
import {
|
||||
isDashScopeE2EReady,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
e2eFixturesDir,
|
||||
} from "./helpers.ts";
|
||||
import { DATASET_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
/**
|
||||
@@ -18,7 +24,7 @@ import { DATASET_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (offline)", () => {
|
||||
test("dataset upload --help 正常退出并展示 --file", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(DATASET_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(DATASET_ROUTES, [
|
||||
"dataset",
|
||||
"upload",
|
||||
"--help",
|
||||
@@ -286,6 +292,42 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (offline)", () => {
|
||||
expect(exitCode, stdout + stderr).not.toBe(0);
|
||||
expect(`${stdout}\n${stderr}`).toMatch(/--schema video is not supported/);
|
||||
});
|
||||
|
||||
test("dataset delete --help 展示 --yes", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(DATASET_ROUTES, [
|
||||
"dataset",
|
||||
"delete",
|
||||
"--help",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/--yes/i);
|
||||
});
|
||||
|
||||
test("dataset delete --dry-run 发出结构化动作", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCommandE2e(DATASET_ROUTES, [
|
||||
"dataset",
|
||||
"delete",
|
||||
"--file-id",
|
||||
"file-id-xxx",
|
||||
"--dry-run",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{ action: string }>(stdout);
|
||||
expect(data.action).toBe("dataset.delete");
|
||||
});
|
||||
|
||||
test("dataset delete 非 TTY 无 --yes 报 USAGE (2)", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(DATASET_ROUTES, [
|
||||
"dataset",
|
||||
"delete",
|
||||
"--file-id",
|
||||
"file-id-xxx",
|
||||
]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/--yes/);
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (DashScope)", () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts";
|
||||
import { isDashScopeE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
|
||||
import { DEPLOY_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
/**
|
||||
@@ -16,14 +16,13 @@ import { DEPLOY_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe.skipIf(!isDashScopeE2EReady())("e2e: deploy (offline)", () => {
|
||||
test("deploy 列出子命令", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCommandE2e(DEPLOY_ROUTES, ["deploy"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(DEPLOY_ROUTES, ["deploy", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const out = `${stdout}\n${stderr}`;
|
||||
expect(out).toMatch(/create|list|get|delete|update|scale|models/);
|
||||
expect(stderr).toMatch(/create|list|get|delete|update|scale|models/);
|
||||
});
|
||||
|
||||
test("deploy create --help 正常退出并展示必填项", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(DEPLOY_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(DEPLOY_ROUTES, [
|
||||
"deploy",
|
||||
"text",
|
||||
"create",
|
||||
@@ -202,6 +201,25 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: deploy (offline)", () => {
|
||||
const data = parseStdoutJson<{ action: string }>(stdout);
|
||||
expect(data.action).toBe(`deploy.${sub}`);
|
||||
});
|
||||
|
||||
test("deploy delete --help 展示 --yes", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(DEPLOY_ROUTES, ["deploy", "delete", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/--yes/i);
|
||||
});
|
||||
|
||||
test("deploy delete 非 TTY 无 --yes 报 USAGE (2)", async () => {
|
||||
// --skip-precheck 保证确认门在发任何网络请求前触发
|
||||
const { stderr, exitCode } = await runCommandE2e(DEPLOY_ROUTES, [
|
||||
"deploy",
|
||||
"delete",
|
||||
"--deployed-model",
|
||||
"dep-xxx",
|
||||
"--skip-precheck",
|
||||
]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/--yes/);
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!isDashScopeE2EReady())("e2e: deploy (DashScope)", () => {
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { join } from "path";
|
||||
import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e, e2eFixturesDir } from "./helpers.ts";
|
||||
import {
|
||||
isDashScopeE2EReady,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
e2eFixturesDir,
|
||||
} from "./helpers.ts";
|
||||
import { FILE_UPLOAD_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
/**
|
||||
@@ -9,7 +15,7 @@ import { FILE_UPLOAD_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: file upload", () => {
|
||||
test("file upload --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(FILE_UPLOAD_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(FILE_UPLOAD_ROUTES, [
|
||||
"file",
|
||||
"upload",
|
||||
"--help",
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { join } from "path";
|
||||
import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e, e2eFixturesDir } from "./helpers.ts";
|
||||
import {
|
||||
isDashScopeE2EReady,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
e2eFixturesDir,
|
||||
} from "./helpers.ts";
|
||||
import { FINETUNE_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
/**
|
||||
@@ -17,14 +23,15 @@ import { FINETUNE_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => {
|
||||
test("finetune 列出子命令", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, ["finetune"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(FINETUNE_ROUTES, ["finetune", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const out = `${stdout}\n${stderr}`;
|
||||
expect(out).toMatch(/create|list|get|cancel|delete|logs|checkpoints|export|watch|capability/);
|
||||
expect(stderr).toMatch(
|
||||
/create|list|get|cancel|delete|logs|checkpoints|export|watch|capability/,
|
||||
);
|
||||
});
|
||||
|
||||
test("finetune create --help 正常退出并展示必填项", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(FINETUNE_ROUTES, [
|
||||
"finetune",
|
||||
"text",
|
||||
"create",
|
||||
@@ -287,6 +294,27 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => {
|
||||
expect(data.action).toBe(`finetune.${sub}`);
|
||||
});
|
||||
|
||||
test("finetune delete --help 展示 --yes", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [
|
||||
"finetune",
|
||||
"delete",
|
||||
"--help",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/--yes/i);
|
||||
});
|
||||
|
||||
test("finetune delete 非 TTY 无 --yes 报 USAGE (2)", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [
|
||||
"finetune",
|
||||
"delete",
|
||||
"--job-id",
|
||||
"ft-xxx",
|
||||
]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/--yes/);
|
||||
});
|
||||
|
||||
test("finetune create --dry-run 解析多 datasets 中的空白", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [
|
||||
"finetune",
|
||||
@@ -336,7 +364,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => {
|
||||
test("finetune audio create --help 不暴露文本超参 flag", async () => {
|
||||
// Audio models don't consume --training-type / --n-epochs / --batch-size /
|
||||
// --learning-rate / --max-length, so those flags are not offered.
|
||||
const { stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(FINETUNE_ROUTES, [
|
||||
"finetune",
|
||||
"audio",
|
||||
"create",
|
||||
@@ -369,7 +397,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => {
|
||||
test("finetune video create --help 暴露视频超参且不含文本超参", async () => {
|
||||
// Video exposes --n-epochs / --batch-size / --learning-rate; the text-only
|
||||
// --training-type / --max-length surface is not offered.
|
||||
const { stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(FINETUNE_ROUTES, [
|
||||
"finetune",
|
||||
"video",
|
||||
"create",
|
||||
|
||||
@@ -8,7 +8,10 @@ import {
|
||||
makeE2eOutputDir,
|
||||
parseStdoutJson,
|
||||
} from "e2e/output";
|
||||
import { captureRegistryHelp } from "e2e/registry-smoke";
|
||||
import { runNodeMain, type RunCliResult } from "e2e/runner";
|
||||
import type { AnyCommand } from "bailian-cli-core";
|
||||
import { CommandRegistry, resolve } from "bailian-cli-runtime";
|
||||
import type { E2eRouteExports } from "./topic-routes.ts";
|
||||
|
||||
export {
|
||||
@@ -42,6 +45,7 @@ export {
|
||||
|
||||
const e2eDir = dirname(fileURLToPath(import.meta.url));
|
||||
const harnessMainTs = join(e2eDir, "harness", "main.ts");
|
||||
const registryCache = new WeakMap<E2eRouteExports, Promise<CommandRegistry>>();
|
||||
|
||||
/** `packages/commands` 根目录 */
|
||||
export const commandsPackageRoot = join(e2eDir, "..", "..");
|
||||
@@ -55,6 +59,51 @@ function serializeRoutes(routes: E2eRouteExports): string {
|
||||
);
|
||||
}
|
||||
|
||||
function getCommandRegistry(routes: E2eRouteExports): Promise<CommandRegistry> {
|
||||
const cachedRegistry = registryCache.get(routes);
|
||||
if (cachedRegistry) return cachedRegistry;
|
||||
|
||||
const registryPromise = import("bailian-cli-commands").then((commandExports) => {
|
||||
const commandLibrary = commandExports as Record<string, unknown>;
|
||||
const commands: Record<string, AnyCommand> = {};
|
||||
for (const [path, exportName] of Object.entries(routes)) {
|
||||
const command = commandLibrary[exportName];
|
||||
if (typeof command !== "object" || command === null || !("run" in command)) {
|
||||
throw new Error(`Unknown bailian-cli-commands export: ${exportName}`);
|
||||
}
|
||||
commands[path] = command as AnyCommand;
|
||||
}
|
||||
return new CommandRegistry(commands, "bl");
|
||||
});
|
||||
registryCache.set(routes, registryPromise);
|
||||
return registryPromise;
|
||||
}
|
||||
|
||||
/** 使用 topic 最小路由在当前 Vitest worker 中渲染命令 help,不启动 CLI 子进程。 */
|
||||
export async function captureCommandHelp(
|
||||
routes: E2eRouteExports,
|
||||
commandPath: string[],
|
||||
): Promise<string> {
|
||||
const registry = await getCommandRegistry(routes);
|
||||
const resolution = resolve([...commandPath, "--help"], registry);
|
||||
if (resolution.kind !== "help" || resolution.path.join(" ") !== commandPath.join(" ")) {
|
||||
throw new Error(`Command help did not resolve: ${commandPath.join(" ")}`);
|
||||
}
|
||||
return captureRegistryHelp(registry, commandPath);
|
||||
}
|
||||
|
||||
/** 与 runCommandE2e 的结果结构兼容,但 help 在当前 Vitest worker 中渲染。 */
|
||||
export async function runCommandHelp(
|
||||
routes: E2eRouteExports,
|
||||
args: string[],
|
||||
): Promise<RunCliResult> {
|
||||
if (args.at(-1) !== "--help") {
|
||||
throw new Error("runCommandHelp requires --help as the final argument");
|
||||
}
|
||||
const stderr = await captureCommandHelp(routes, args.slice(0, -1));
|
||||
return { stdout: "", stderr, exitCode: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 harness 子进程执行命令 E2E。
|
||||
* `routes` 为本用例所需的最小 path → export 映射,不维护全量产品 map。
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
isDashScopeE2EReady,
|
||||
makeE2eOutputDir,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { IMAGE_ROUTES } from "./topic-routes.ts";
|
||||
@@ -18,7 +19,7 @@ import { IMAGE_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: image edit", () => {
|
||||
test("image edit --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, ["image", "edit", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(IMAGE_ROUTES, ["image", "edit", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/edit|--image|--prompt|--async|--concurrent/i);
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
isDashScopeE2EReady,
|
||||
makeE2eOutputDir,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { IMAGE_ROUTES } from "./topic-routes.ts";
|
||||
@@ -19,7 +20,11 @@ import { IMAGE_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: image generate", () => {
|
||||
test("image generate --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, ["image", "generate", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(IMAGE_ROUTES, [
|
||||
"image",
|
||||
"generate",
|
||||
"--help",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/generate|--prompt|--model/i);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
isChatE2EReady,
|
||||
isMultimodalChatE2EReady,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "../helpers.ts";
|
||||
import { KNOWLEDGE_CHAT_ROUTES } from "../topic-routes.ts";
|
||||
@@ -31,7 +32,7 @@ interface DryRunBody {
|
||||
|
||||
describe("e2e: knowledge chat", () => {
|
||||
test("knowledge chat --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_CHAT_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_CHAT_ROUTES, [
|
||||
"knowledge",
|
||||
"chat",
|
||||
"--help",
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
isOssImportE2EReady,
|
||||
isTableKbE2EReady,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "../helpers.ts";
|
||||
import {
|
||||
@@ -47,7 +48,7 @@ const HELP_SMOKE_CASES: Array<[string[], RegExp]> = [
|
||||
describe("e2e: chunk/category/file 全命令 --help 冒烟", () => {
|
||||
test("12 条命令 help 退出 0 且展示代表性 flag(chunk add 单独测)", async () => {
|
||||
for (const [commandPath, flagPattern] of HELP_SMOKE_CASES) {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
|
||||
...commandPath,
|
||||
"--help",
|
||||
]);
|
||||
@@ -59,7 +60,7 @@ describe("e2e: chunk/category/file 全命令 --help 冒烟", () => {
|
||||
|
||||
describe("e2e: knowledge chunk 组 (静态)", () => {
|
||||
test("chunk add: --help 展示双通道 flags", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
|
||||
"knowledge",
|
||||
"chunk",
|
||||
"add",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { parseStdoutJson, runCommandE2e } from "../helpers.ts";
|
||||
import { parseStdoutJson, runCommandHelp, runCommandE2e } from "../helpers.ts";
|
||||
import { KNOWLEDGE_DOC_DELETE_ROUTES } from "../topic-routes.ts";
|
||||
|
||||
describe("e2e: knowledge doc delete", () => {
|
||||
test("--help 展示 flags", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_DOC_DELETE_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_DOC_DELETE_ROUTES, [
|
||||
"knowledge",
|
||||
"doc",
|
||||
"delete",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isKbAdminE2EReady, parseStdoutJson, runCommandE2e } from "../helpers.ts";
|
||||
import { isKbAdminE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "../helpers.ts";
|
||||
import { KNOWLEDGE_DOC_LIST_ROUTES } from "../topic-routes.ts";
|
||||
|
||||
describe("e2e: knowledge doc list", () => {
|
||||
test("--help 展示 flags", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_DOC_LIST_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_DOC_LIST_ROUTES, [
|
||||
"knowledge",
|
||||
"doc",
|
||||
"list",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { mkdtempSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isKbAdminE2EReady, parseStdoutJson, runCommandE2e } from "../helpers.ts";
|
||||
import { isKbAdminE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "../helpers.ts";
|
||||
import { KNOWLEDGE_DOC_STATUS_ROUTES } from "../topic-routes.ts";
|
||||
|
||||
// Live coverage depends on a real job_id produced by doc upload; it is exercised
|
||||
@@ -10,7 +10,7 @@ import { KNOWLEDGE_DOC_STATUS_ROUTES } from "../topic-routes.ts";
|
||||
|
||||
describe("e2e: knowledge doc status", () => {
|
||||
test("--help 展示 flags", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_DOC_STATUS_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_DOC_STATUS_ROUTES, [
|
||||
"knowledge",
|
||||
"doc",
|
||||
"status",
|
||||
|
||||
@@ -2,12 +2,12 @@ import { mkdtempSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isKbAdminE2EReady, parseStdoutJson, runCommandE2e } from "../helpers.ts";
|
||||
import { isKbAdminE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "../helpers.ts";
|
||||
import { KNOWLEDGE_DOC_TAG_ROUTES } from "../topic-routes.ts";
|
||||
|
||||
describe("e2e: knowledge doc tag", () => {
|
||||
test("--help 展示 flags", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_DOC_TAG_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_DOC_TAG_ROUTES, [
|
||||
"knowledge",
|
||||
"doc",
|
||||
"tag",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isKbAdminE2EReady, parseStdoutJson, runCommandE2e } from "../helpers.ts";
|
||||
import { isKbAdminE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "../helpers.ts";
|
||||
import { KNOWLEDGE_DOC_UPLOAD_ROUTES } from "../topic-routes.ts";
|
||||
|
||||
const fixtureDir = mkdtempSync(join(tmpdir(), "doc-upload-e2e-"));
|
||||
@@ -17,7 +17,7 @@ interface DryRunStep {
|
||||
|
||||
describe("e2e: knowledge doc upload", () => {
|
||||
test("--help 展示 flags", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_DOC_UPLOAD_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_DOC_UPLOAD_ROUTES, [
|
||||
"knowledge",
|
||||
"doc",
|
||||
"upload",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { parseStdoutJson, runCommandE2e } from "../helpers.ts";
|
||||
import { parseStdoutJson, runCommandHelp, runCommandE2e } from "../helpers.ts";
|
||||
import { KNOWLEDGE_KB_CREATE_ROUTES } from "../topic-routes.ts";
|
||||
|
||||
interface DryRunBody {
|
||||
@@ -17,7 +17,7 @@ interface DryRunBody {
|
||||
|
||||
describe("e2e: knowledge kb create", () => {
|
||||
test("--help 展示 flags", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_KB_CREATE_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_KB_CREATE_ROUTES, [
|
||||
"knowledge",
|
||||
"create",
|
||||
"--help",
|
||||
|
||||
@@ -4,13 +4,13 @@ import { mkdtempSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { basename, join } from "node:path";
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isKbAdminE2EReady, parseStdoutJson, runCommandE2e } from "../helpers.ts";
|
||||
import { isKbAdminE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "../helpers.ts";
|
||||
import { deleteKbWithRetry } from "./journeys/journey-helpers.ts";
|
||||
import { KNOWLEDGE_KB_DELETE_ROUTES } from "../topic-routes.ts";
|
||||
|
||||
describe("e2e: knowledge kb delete", () => {
|
||||
test("--help 展示 flags", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_KB_DELETE_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_KB_DELETE_ROUTES, [
|
||||
"knowledge",
|
||||
"delete",
|
||||
"--help",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isKbAdminE2EReady, parseStdoutJson, runCommandE2e } from "../helpers.ts";
|
||||
import { isKbAdminE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "../helpers.ts";
|
||||
import { KNOWLEDGE_KB_INFO_ROUTES } from "../topic-routes.ts";
|
||||
|
||||
describe("e2e: knowledge kb info", () => {
|
||||
test("--help 展示 flags", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_KB_INFO_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_KB_INFO_ROUTES, [
|
||||
"knowledge",
|
||||
"info",
|
||||
"--help",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isKbAdminE2EReady, parseStdoutJson, runCommandE2e } from "../helpers.ts";
|
||||
import { isKbAdminE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "../helpers.ts";
|
||||
import { KNOWLEDGE_KB_LIST_ROUTES } from "../topic-routes.ts";
|
||||
|
||||
interface DryRunBody {
|
||||
@@ -9,7 +9,7 @@ interface DryRunBody {
|
||||
|
||||
describe("e2e: knowledge kb list", () => {
|
||||
test("--help 展示 flags", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_KB_LIST_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_KB_LIST_ROUTES, [
|
||||
"knowledge",
|
||||
"list",
|
||||
"--help",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { parseStdoutJson, runCommandE2e } from "../helpers.ts";
|
||||
import { parseStdoutJson, runCommandHelp, runCommandE2e } from "../helpers.ts";
|
||||
import { KNOWLEDGE_KB_UPDATE_ROUTES } from "../topic-routes.ts";
|
||||
|
||||
describe("e2e: knowledge kb update", () => {
|
||||
test("--help 展示 flags", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_KB_UPDATE_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_KB_UPDATE_ROUTES, [
|
||||
"knowledge",
|
||||
"update",
|
||||
"--help",
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
isSearchE2EReady,
|
||||
isTableSearchE2EReady,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "../helpers.ts";
|
||||
import { KNOWLEDGE_SEARCH_ROUTES } from "../topic-routes.ts";
|
||||
@@ -20,7 +21,7 @@ interface DryRunBody {
|
||||
|
||||
describe("e2e: knowledge search", () => {
|
||||
test("knowledge search --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_SEARCH_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_SEARCH_ROUTES, [
|
||||
"knowledge",
|
||||
"search",
|
||||
"--help",
|
||||
|
||||
@@ -5,7 +5,7 @@ import { mkdtempSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isKbAdminE2EReady, parseStdoutJson, runCommandE2e } from "../helpers.ts";
|
||||
import { isKbAdminE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "../helpers.ts";
|
||||
import { KNOWLEDGE_SERVICE_ROUTES } from "../topic-routes.ts";
|
||||
import { pickDifferentAgentModel } from "./verified-models.ts";
|
||||
|
||||
@@ -16,7 +16,7 @@ interface DryRunBody {
|
||||
|
||||
describe("e2e: knowledge service list", () => {
|
||||
test("--help 展示 flags", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_SERVICE_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_SERVICE_ROUTES, [
|
||||
"knowledge",
|
||||
"service",
|
||||
"list",
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
isDashScopeE2EReady,
|
||||
isKbAdminE2EReady,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "../helpers.ts";
|
||||
import { KNOWLEDGE_ROUTES } from "../topic-routes.ts";
|
||||
@@ -28,7 +29,7 @@ interface DryRunBody {
|
||||
|
||||
describe("e2e: knowledge retrieve", () => {
|
||||
test("knowledge retrieve --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_ROUTES, [
|
||||
"knowledge",
|
||||
"retrieve",
|
||||
"--help",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { e2eFixturesDir, parseStdoutJson, runCommandE2e } from "./helpers.ts";
|
||||
import { e2eFixturesDir, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
|
||||
import { MANAGED_AGENT_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
const AGENTS_DEPLOYMENT_YAML = join(e2eFixturesDir, "managed-agent", "agents-deployment.yaml");
|
||||
@@ -123,7 +123,7 @@ describe("e2e: managed-agent", () => {
|
||||
});
|
||||
|
||||
test("managed-agent apply --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(MANAGED_AGENT_ROUTES, [
|
||||
"managed-agent",
|
||||
"apply",
|
||||
"--help",
|
||||
@@ -157,7 +157,7 @@ describe("e2e: managed-agent", () => {
|
||||
});
|
||||
|
||||
test("managed-agent skill-list --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(MANAGED_AGENT_ROUTES, [
|
||||
"managed-agent",
|
||||
"skill-list",
|
||||
"--help",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts";
|
||||
import { isDashScopeE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
|
||||
import { MCP_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
/**
|
||||
@@ -18,25 +18,25 @@ import { MCP_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: mcp", () => {
|
||||
test("mcp list --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(MCP_ROUTES, ["mcp", "list", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(MCP_ROUTES, ["mcp", "list", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/list|--name|--type|--page/i);
|
||||
});
|
||||
|
||||
test("mcp tools --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(MCP_ROUTES, ["mcp", "tools", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(MCP_ROUTES, ["mcp", "tools", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/tools|--server|--url/i);
|
||||
});
|
||||
|
||||
test("mcp call --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(MCP_ROUTES, ["mcp", "call", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(MCP_ROUTES, ["mcp", "call", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/call|--target|--arg|--json/i);
|
||||
});
|
||||
|
||||
test("mcp list --help 不暴露 --all 入口(市场全量已下线)", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(MCP_ROUTES, ["mcp", "list", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(MCP_ROUTES, ["mcp", "list", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).not.toMatch(/--all/);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
isBailianE2EEnabled,
|
||||
isDashScopeE2EReady,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { MEMORY_ROUTES } from "./topic-routes.ts";
|
||||
@@ -38,25 +39,29 @@ function memoryLibraryCliArgs(): string[] {
|
||||
|
||||
describe("e2e: memory", () => {
|
||||
test("memory add --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(MEMORY_ROUTES, ["memory", "add", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(MEMORY_ROUTES, ["memory", "add", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/add|--user-id|--content|messages/i);
|
||||
});
|
||||
|
||||
test("memory list --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(MEMORY_ROUTES, ["memory", "list", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(MEMORY_ROUTES, ["memory", "list", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/list|--user-id|memory-library/i);
|
||||
});
|
||||
|
||||
test("memory search --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(MEMORY_ROUTES, ["memory", "search", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(MEMORY_ROUTES, [
|
||||
"memory",
|
||||
"search",
|
||||
"--help",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/search|--query|user-id/i);
|
||||
});
|
||||
|
||||
test("memory profile create --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(MEMORY_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(MEMORY_ROUTES, [
|
||||
"memory",
|
||||
"profile",
|
||||
"create",
|
||||
|
||||
@@ -6,13 +6,14 @@ import {
|
||||
isDashScopeE2EReady,
|
||||
makeE2eOutputDir,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { OMNI_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: omni", () => {
|
||||
test("omni --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(OMNI_ROUTES, ["omni", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(OMNI_ROUTES, ["omni", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/omni|--message|--audio|text-only/i);
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts";
|
||||
import { isDashScopeE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
|
||||
import { PERMISSION_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: permission", () => {
|
||||
test("permission list --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(PERMISSION_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(PERMISSION_ROUTES, [
|
||||
"permission",
|
||||
"list",
|
||||
"--help",
|
||||
@@ -17,7 +17,7 @@ describe("e2e: permission", () => {
|
||||
});
|
||||
|
||||
test("permission grant --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(PERMISSION_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(PERMISSION_ROUTES, [
|
||||
"permission",
|
||||
"grant",
|
||||
"--help",
|
||||
@@ -29,7 +29,7 @@ describe("e2e: permission", () => {
|
||||
});
|
||||
|
||||
test("permission revoke --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(PERMISSION_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(PERMISSION_ROUTES, [
|
||||
"permission",
|
||||
"revoke",
|
||||
"--help",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { afterAll, beforeAll, describe, expect, test } from "vite-plus/test";
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { parseStdoutJson, runCommandE2e } from "./helpers.ts";
|
||||
import { parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
|
||||
import { PIPELINE_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: pipeline", () => {
|
||||
@@ -65,7 +65,7 @@ describe("e2e: pipeline", () => {
|
||||
});
|
||||
|
||||
test("pipeline run --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(PIPELINE_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(PIPELINE_ROUTES, [
|
||||
"pipeline",
|
||||
"run",
|
||||
"--help",
|
||||
@@ -76,7 +76,7 @@ describe("e2e: pipeline", () => {
|
||||
});
|
||||
|
||||
test("pipeline validate --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(PIPELINE_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(PIPELINE_ROUTES, [
|
||||
"pipeline",
|
||||
"validate",
|
||||
"--help",
|
||||
|
||||
@@ -4,20 +4,21 @@ import {
|
||||
isConsoleAuthFailure,
|
||||
isDashScopeE2EReady,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { QUOTA_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: quota", () => {
|
||||
test("quota list --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, ["quota", "list", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(QUOTA_ROUTES, ["quota", "list", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("--model");
|
||||
expect(stderr).toContain("--name");
|
||||
});
|
||||
|
||||
test("quota list --help 包含所有示例", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, ["quota", "list", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(QUOTA_ROUTES, ["quota", "list", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("bl quota list");
|
||||
expect(stderr).toContain("bl quota list --model qwen3-max");
|
||||
@@ -25,30 +26,31 @@ describe("e2e: quota", () => {
|
||||
});
|
||||
|
||||
test("quota update --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, ["quota", "update", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(QUOTA_ROUTES, ["quota", "update", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("--model");
|
||||
expect(stderr).toContain("--rpm");
|
||||
expect(stderr).toContain("--tpm");
|
||||
expect(stderr).toContain("--delete");
|
||||
expect(stderr).toContain("--yes");
|
||||
});
|
||||
|
||||
test("quota request 作为 quota update 的兼容别名可用", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, ["quota", "request", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(QUOTA_ROUTES, ["quota", "request", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("--rpm");
|
||||
expect(stderr).toContain("--delete");
|
||||
});
|
||||
|
||||
test("quota history --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, ["quota", "history", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(QUOTA_ROUTES, ["quota", "history", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("--page");
|
||||
expect(stderr).toContain("--model");
|
||||
});
|
||||
|
||||
test("quota check --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, ["quota", "check", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(QUOTA_ROUTES, ["quota", "check", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("--model");
|
||||
expect(stderr).toContain("--period");
|
||||
@@ -91,6 +93,17 @@ describe("e2e: quota", () => {
|
||||
expect(stderr).toContain("cannot be combined");
|
||||
});
|
||||
|
||||
test("quota update --delete 非 TTY 无 --yes 报 USAGE (2)", async () => {
|
||||
// 注入假 key 让 apiKey 鉴权通过;确认门在发任何网络请求前触发
|
||||
const { stderr, exitCode } = await runCommandE2e(
|
||||
QUOTA_ROUTES,
|
||||
["quota", "update", "--model", "qwen-plus", "--delete"],
|
||||
{ DASHSCOPE_API_KEY: "sk-e2e-quota-delete" },
|
||||
);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/--yes/);
|
||||
});
|
||||
|
||||
test("quota update --rpm 负数报错", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, [
|
||||
"quota",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts";
|
||||
import { isDashScopeE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
|
||||
import { SEARCH_WEB_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
function pagesFromSearchWebStdout(stdout: string): Array<{ title?: string; url?: string }> {
|
||||
@@ -17,7 +17,7 @@ function pagesFromSearchWebStdout(stdout: string): Array<{ title?: string; url?:
|
||||
|
||||
describe("e2e: search web", () => {
|
||||
test("search web --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(SEARCH_WEB_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(SEARCH_WEB_ROUTES, [
|
||||
"search",
|
||||
"web",
|
||||
"--help",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { existsSync, mkdtempSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isBailianE2EEnabled, parseStdoutJson, runCommandE2e } from "./helpers.ts";
|
||||
import { isBailianE2EEnabled, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
|
||||
import { SKILL_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
/** Canonical always-published skill; also the backbone of advisor wiki sync */
|
||||
@@ -15,33 +15,33 @@ function makeTempConfigDir(): string {
|
||||
|
||||
describe("e2e: skill", () => {
|
||||
test("skill add --help exits successfully", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, ["skill", "add", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(SKILL_ROUTES, ["skill", "add", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/--all/);
|
||||
expect(stderr).toMatch(/--name/);
|
||||
});
|
||||
|
||||
test("skill update --help exits successfully", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, ["skill", "update", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(SKILL_ROUTES, ["skill", "update", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/--all/);
|
||||
expect(stderr).toMatch(/--name/);
|
||||
});
|
||||
|
||||
test("skill remove --help exits successfully", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, ["skill", "remove", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(SKILL_ROUTES, ["skill", "remove", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/--name/);
|
||||
});
|
||||
|
||||
test("skill list --help exits successfully", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, ["skill", "list", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(SKILL_ROUTES, ["skill", "list", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/list|registry/i);
|
||||
});
|
||||
|
||||
test("skill init --help exits successfully", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, ["skill", "init", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(SKILL_ROUTES, ["skill", "init", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/bailian/i);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isDashScopeE2EReady, runCommandE2e } from "./helpers.ts";
|
||||
import { isDashScopeE2EReady, runCommandHelp, runCommandE2e } from "./helpers.ts";
|
||||
import { SPEECH_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
/**
|
||||
@@ -8,7 +8,7 @@ import { SPEECH_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: speech list-voices", () => {
|
||||
test("speech synthesize --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(SPEECH_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(SPEECH_ROUTES, [
|
||||
"speech",
|
||||
"synthesize",
|
||||
"--help",
|
||||
@@ -18,7 +18,7 @@ describe("e2e: speech list-voices", () => {
|
||||
});
|
||||
|
||||
test("speech recognize --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(SPEECH_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(SPEECH_ROUTES, [
|
||||
"speech",
|
||||
"recognize",
|
||||
"--help",
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
isDashScopeE2EReady,
|
||||
makeE2eOutputDir,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { SPEECH_ROUTES } from "./topic-routes.ts";
|
||||
@@ -50,7 +51,7 @@ describe("e2e: speech recognize", () => {
|
||||
}
|
||||
|
||||
test("speech recognize --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(SPEECH_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(SPEECH_ROUTES, [
|
||||
"speech",
|
||||
"recognize",
|
||||
"--help",
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
isDashScopeE2EReady,
|
||||
makeE2eOutputDir,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { SPEECH_ROUTES } from "./topic-routes.ts";
|
||||
@@ -17,7 +18,7 @@ import { SPEECH_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: speech synthesize", () => {
|
||||
test("speech synthesize --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(SPEECH_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(SPEECH_ROUTES, [
|
||||
"speech",
|
||||
"synthesize",
|
||||
"--help",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts";
|
||||
import { isDashScopeE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
|
||||
import { TEXT_CHAT_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
/**
|
||||
@@ -8,7 +8,7 @@ import { TEXT_CHAT_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: text chat", () => {
|
||||
test("text chat --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(TEXT_CHAT_ROUTES, ["text", "chat", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(TEXT_CHAT_ROUTES, ["text", "chat", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/--api\s+<chat\|responses>/i);
|
||||
expect(stderr).toMatch(/chat|--message|model|stream/i);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { makeE2eOutputDir, parseStdoutJson, runCommandE2e } from "./helpers.ts";
|
||||
import { makeE2eOutputDir, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
|
||||
import { TOKEN_PLAN_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: token-plan", () => {
|
||||
test("token-plan help shows centralized OpenAPI auth flags", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(TOKEN_PLAN_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(TOKEN_PLAN_ROUTES, [
|
||||
"token-plan",
|
||||
"list-seats",
|
||||
"--help",
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { runCommandE2e } from "./helpers.ts";
|
||||
import { runCommandHelp, runCommandE2e } from "./helpers.ts";
|
||||
import { UPDATE_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: update", () => {
|
||||
test("update --help 正常退出并展示 --to", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(UPDATE_ROUTES, ["update", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(UPDATE_ROUTES, ["update", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/--to/);
|
||||
expect(stderr).toMatch(/<version>/);
|
||||
});
|
||||
|
||||
test("update --help 包含 --to 示例", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(UPDATE_ROUTES, ["update", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(UPDATE_ROUTES, ["update", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("--to 0.1.14");
|
||||
});
|
||||
|
||||
@@ -3,13 +3,14 @@ import {
|
||||
isConsoleAuthFailure,
|
||||
isConsoleE2EReady,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { USAGE_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: usage coding-plan", () => {
|
||||
test("usage coding-plan --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(USAGE_ROUTES, [
|
||||
"usage",
|
||||
"coding-plan",
|
||||
"--help",
|
||||
@@ -19,7 +20,7 @@ describe("e2e: usage coding-plan", () => {
|
||||
});
|
||||
|
||||
test("usage coding-plan --help 包含 --output json 示例", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(USAGE_ROUTES, [
|
||||
"usage",
|
||||
"coding-plan",
|
||||
"--help",
|
||||
|
||||
@@ -3,19 +3,20 @@ import {
|
||||
isConsoleE2EReady,
|
||||
isConsoleAuthFailure,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { USAGE_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: usage free", () => {
|
||||
test("usage free --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, ["usage", "free", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(USAGE_ROUTES, ["usage", "free", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/--model|quota|free-tier/i);
|
||||
});
|
||||
|
||||
test("usage free --help 包含所有示例", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, ["usage", "free", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(USAGE_ROUTES, ["usage", "free", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("bl usage free");
|
||||
expect(stderr).toContain("bl usage free --model qwen3-max");
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
isConsoleE2EReady,
|
||||
isConsoleAuthFailure,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { USAGE_ROUTES } from "./topic-routes.ts";
|
||||
@@ -42,13 +43,13 @@ async function fetchDefaultWorkspaceId(): Promise<string> {
|
||||
|
||||
describe("e2e: usage stats", () => {
|
||||
test("usage stats --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, ["usage", "stats", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(USAGE_ROUTES, ["usage", "stats", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/--model|--days|stats/i);
|
||||
});
|
||||
|
||||
test("usage stats --help 包含所有示例", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, ["usage", "stats", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(USAGE_ROUTES, ["usage", "stats", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("bl usage stats");
|
||||
expect(stderr).toContain("bl usage stats --model qwen-turbo");
|
||||
@@ -56,7 +57,7 @@ describe("e2e: usage stats", () => {
|
||||
});
|
||||
|
||||
test("usage stats --help 包含 --workspace-id 选项", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, ["usage", "stats", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(USAGE_ROUTES, ["usage", "stats", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("--workspace-id");
|
||||
});
|
||||
|
||||
@@ -3,13 +3,14 @@ import {
|
||||
isConsoleAuthFailure,
|
||||
isConsoleE2EReady,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { USAGE_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: usage token-plan", () => {
|
||||
test("usage token-plan --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(USAGE_ROUTES, [
|
||||
"usage",
|
||||
"token-plan",
|
||||
"--help",
|
||||
@@ -19,7 +20,7 @@ describe("e2e: usage token-plan", () => {
|
||||
});
|
||||
|
||||
test("usage token-plan --help 包含 --output json 示例", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(USAGE_ROUTES, [
|
||||
"usage",
|
||||
"token-plan",
|
||||
"--help",
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
isDashScopeE2EReady,
|
||||
makeE2eOutputDir,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { VIDEO_ROUTES } from "./topic-routes.ts";
|
||||
@@ -22,7 +23,11 @@ const PLACEHOLDER_TASK_ID = "00000000-0000-4000-8000-000000000001";
|
||||
|
||||
describe("e2e: video download", () => {
|
||||
test("video download --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, ["video", "download", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(VIDEO_ROUTES, [
|
||||
"video",
|
||||
"download",
|
||||
"--help",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/download|--task-id|--out/i);
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
isDashScopeE2EReady,
|
||||
makeE2eOutputDir,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { VIDEO_ROUTES } from "./topic-routes.ts";
|
||||
@@ -17,7 +18,7 @@ import { VIDEO_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: video edit", () => {
|
||||
test("video edit --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, ["video", "edit", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(VIDEO_ROUTES, ["video", "edit", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/edit|--video|--prompt|model|--async|--concurrent/i);
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
isDashScopeE2EReady,
|
||||
makeE2eOutputDir,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { VIDEO_ROUTES } from "./topic-routes.ts";
|
||||
@@ -18,7 +19,11 @@ import { VIDEO_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: video generate (i2v)", () => {
|
||||
test("video generate --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, ["video", "generate", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(VIDEO_ROUTES, [
|
||||
"video",
|
||||
"generate",
|
||||
"--help",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/generate|--prompt|--image|model/i);
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
isDashScopeE2EReady,
|
||||
makeE2eOutputDir,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { VIDEO_ROUTES } from "./topic-routes.ts";
|
||||
@@ -17,7 +18,11 @@ import { VIDEO_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: video generate (t2v)", () => {
|
||||
test("video generate --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, ["video", "generate", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(VIDEO_ROUTES, [
|
||||
"video",
|
||||
"generate",
|
||||
"--help",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/generate|--prompt|--model|download|image/i);
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
isDashScopeE2EReady,
|
||||
makeE2eOutputDir,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { VIDEO_ROUTES } from "./topic-routes.ts";
|
||||
@@ -18,7 +19,7 @@ import { VIDEO_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: video ref (r2v)", () => {
|
||||
test("video ref --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, ["video", "ref", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(VIDEO_ROUTES, ["video", "ref", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/ref|--prompt|--image|model|--async|--concurrent/i);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
isBailianE2EEnabled,
|
||||
isDashScopeE2EReady,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { VIDEO_ROUTES } from "./topic-routes.ts";
|
||||
@@ -15,7 +16,7 @@ const taskId = process.env.BAILIAN_E2E_VIDEO_TASK_ID?.trim();
|
||||
|
||||
describe("e2e: video task get", () => {
|
||||
test("video task get --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(VIDEO_ROUTES, [
|
||||
"video",
|
||||
"task",
|
||||
"get",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli-core",
|
||||
"version": "1.18.1",
|
||||
"version": "1.18.2",
|
||||
"description": "Core SDK for bailian-cli. See https://www.npmjs.com/package/bailian-cli for usage.",
|
||||
"homepage": "https://bailian.console.aliyun.com/cli",
|
||||
"bugs": {
|
||||
|
||||
@@ -37,6 +37,8 @@ const MODELS_FILE = "models.jsonl";
|
||||
const THROTTLE_MS = 12 * 60 * 60 * 1000; // 12h
|
||||
/** Tighter than the interactive default: the silent channel must not stall `bl advisor recommend` */
|
||||
const INDEX_TIMEOUT_MS = 3000;
|
||||
/** No retries on the silent channel: a failed sync simply tries again on the next recommend */
|
||||
const FETCH_ATTEMPTS = 1;
|
||||
|
||||
interface SyncState {
|
||||
lastChecked: number;
|
||||
@@ -108,7 +110,7 @@ function wikiLockNeedsBackfill(contentHash: string): boolean {
|
||||
/** Fetch skills/index.json via the shared registry client and extract the wiki skill entry; returns null on any failure */
|
||||
async function fetchIndexEntry(): Promise<SkillIndexEntry | null> {
|
||||
try {
|
||||
const index = await fetchSkillsIndex(INDEX_TIMEOUT_MS);
|
||||
const index = await fetchSkillsIndex(INDEX_TIMEOUT_MS, FETCH_ATTEMPTS);
|
||||
return index.skills[WIKI_SKILL_NAME] ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
@@ -160,6 +162,7 @@ export async function maybeSyncWikiData(): Promise<boolean> {
|
||||
entry,
|
||||
detectInstalledAgents(),
|
||||
previousLinks,
|
||||
FETCH_ATTEMPTS,
|
||||
);
|
||||
recordWikiInLock(record.lockEntry);
|
||||
} catch {
|
||||
|
||||
@@ -100,5 +100,12 @@ export function atomicSwap(tmpDir: string, destDir: string): void {
|
||||
if (existsSync(backup) && !existsSync(destDir)) renameSync(backup, destDir);
|
||||
throw err;
|
||||
}
|
||||
if (existsSync(backup)) rmSync(backup, { recursive: true, force: true });
|
||||
// Best-effort cleanup: the swap already succeeded, so a backup deletion failure
|
||||
// (permissions, host safe-delete guards on large dirs) must not fail the install;
|
||||
// leftover .old-* dirs are inert (skill status scans ignore them)
|
||||
try {
|
||||
if (existsSync(backup)) rmSync(backup, { recursive: true, force: true });
|
||||
} catch {
|
||||
/* keep the backup on disk rather than report a completed install as failed */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,12 +61,22 @@ export async function installSkillFromBuffer(
|
||||
atomicSwap(tmpDir, dest);
|
||||
return { name, path: dest, meta };
|
||||
} finally {
|
||||
if (existsSync(tmpDir)) rmSync(tmpDir, { recursive: true, force: true });
|
||||
// Best-effort cleanup: on failure paths this must not mask the original error,
|
||||
// and on success the dir is already renamed away (existsSync → false)
|
||||
try {
|
||||
if (existsSync(tmpDir)) rmSync(tmpDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
/* leave the temp dir rather than hide the real error */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Install a single skill by index entry (download + validate + write to disk) */
|
||||
export async function installSkill(name: string, entry: SkillIndexEntry): Promise<InstalledSkill> {
|
||||
export async function installSkill(
|
||||
name: string,
|
||||
entry: SkillIndexEntry,
|
||||
downloadAttempts?: number,
|
||||
): Promise<InstalledSkill> {
|
||||
if (entry.compression && entry.compression !== "tar.br") {
|
||||
throw new BailianError(
|
||||
`Skill ${name} uses unsupported compression format: ${entry.compression}`,
|
||||
@@ -74,7 +84,7 @@ export async function installSkill(name: string, entry: SkillIndexEntry): Promis
|
||||
"Upgrade bailian-cli to the latest version and retry",
|
||||
);
|
||||
}
|
||||
const buffer = await downloadSkillAsset(name, entry);
|
||||
const buffer = await downloadSkillAsset(name, entry, downloadAttempts);
|
||||
return installSkillFromBuffer(name, buffer, entry.contentHash);
|
||||
}
|
||||
|
||||
@@ -116,14 +126,17 @@ export interface SkillInstallRecord {
|
||||
* entry (batch writeSkillLock for commands, best-effort upsertSkillLockEntry for silent channels).
|
||||
* recordedLinks = the skill's previously recorded fan-out paths from the lock; lets the
|
||||
* fan-out replace copy-fallback artifacts and keeps unvisited paths reclaimable.
|
||||
* downloadAttempts = registry fetch attempts (undefined → interactive default; silent
|
||||
* background channels pass 1 to fail fast instead of stalling the host command).
|
||||
*/
|
||||
export async function installSkillWithFanout(
|
||||
name: string,
|
||||
entry: SkillIndexEntry,
|
||||
agents: AgentTarget[] = detectInstalledAgents(),
|
||||
recordedLinks: string[] = [],
|
||||
downloadAttempts?: number,
|
||||
): Promise<SkillInstallRecord> {
|
||||
await installSkill(name, entry);
|
||||
await installSkill(name, entry, downloadAttempts);
|
||||
const fanout = fanOutSkillToAgents(name, agents, recordedLinks);
|
||||
return {
|
||||
lockEntry: buildSkillLockEntry(entry, fanout.links),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BailianError } from "../errors/base.ts";
|
||||
import { ExitCode } from "../errors/codes.ts";
|
||||
import { withRetry } from "../utils/retry.ts";
|
||||
import type { SkillIndexEntry, SkillsIndex } from "./types.ts";
|
||||
|
||||
/**
|
||||
@@ -9,8 +10,10 @@ import type { SkillIndexEntry, SkillsIndex } from "./types.ts";
|
||||
*/
|
||||
const DEFAULT_REGISTRY_BASE_URL = "https://bailian-wiki.oss-cn-hangzhou.aliyuncs.com/skills";
|
||||
|
||||
const INDEX_TIMEOUT_MS = 10_000;
|
||||
const INDEX_TIMEOUT_MS = 30_000;
|
||||
const ASSET_TIMEOUT_MS = 120_000;
|
||||
/** Interactive channels retry transient failures; silent background channels pass 1 to fail fast */
|
||||
const DEFAULT_ATTEMPTS = 3;
|
||||
|
||||
export function getSkillRegistryBaseUrl(): string {
|
||||
const override = process.env.BAILIAN_SKILL_REGISTRY_URL?.trim();
|
||||
@@ -20,55 +23,64 @@ export function getSkillRegistryBaseUrl(): string {
|
||||
/**
|
||||
* Fetch the remote skill index. No local caching — the diff comparison is always
|
||||
* "live remote index vs local skill-lock.json".
|
||||
* Silent background channels (advisor sync) may pass a tighter timeout than the interactive default.
|
||||
* Silent background channels (advisor sync) may pass a tighter timeout and attempts=1
|
||||
* than the interactive defaults.
|
||||
*/
|
||||
export async function fetchSkillsIndex(timeoutMs: number = INDEX_TIMEOUT_MS): Promise<SkillsIndex> {
|
||||
const url = `${getSkillRegistryBaseUrl()}/index.json`;
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
||||
} catch (err) {
|
||||
throw new BailianError(
|
||||
`Cannot access skill registry: ${url}`,
|
||||
ExitCode.NETWORK,
|
||||
"Check network connectivity; if using a private mirror, verify BAILIAN_SKILL_REGISTRY_URL configuration",
|
||||
{ cause: err },
|
||||
);
|
||||
}
|
||||
if (!res.ok) {
|
||||
throw new BailianError(
|
||||
`Skill registry returned HTTP ${res.status}: ${url}`,
|
||||
ExitCode.NETWORK,
|
||||
res.status === 404
|
||||
? "Skill index not yet published or registry URL is incorrect; confirm the publisher has generated index.json"
|
||||
: "Remote error, retry later",
|
||||
);
|
||||
}
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = await res.json();
|
||||
} catch (err) {
|
||||
throw new BailianError(
|
||||
"Skill index index.json is not valid JSON",
|
||||
ExitCode.GENERAL,
|
||||
"Remote may be in the middle of publishing, retry later",
|
||||
{ cause: err },
|
||||
);
|
||||
}
|
||||
const index = parsed as SkillsIndex;
|
||||
if (
|
||||
typeof index !== "object" ||
|
||||
index === null ||
|
||||
typeof index.skills !== "object" ||
|
||||
index.skills === null
|
||||
) {
|
||||
throw new BailianError(
|
||||
"Skill index index.json has invalid structure",
|
||||
ExitCode.GENERAL,
|
||||
"Retry later or contact the publisher",
|
||||
);
|
||||
}
|
||||
return index;
|
||||
export async function fetchSkillsIndex(
|
||||
timeoutMs: number = INDEX_TIMEOUT_MS,
|
||||
attempts: number = DEFAULT_ATTEMPTS,
|
||||
): Promise<SkillsIndex> {
|
||||
return withRetry(
|
||||
async () => {
|
||||
const url = `${getSkillRegistryBaseUrl()}/index.json`;
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
||||
} catch (err) {
|
||||
throw new BailianError(
|
||||
`Cannot access skill registry: ${url}`,
|
||||
ExitCode.NETWORK,
|
||||
"Check network connectivity; if using a private mirror, verify BAILIAN_SKILL_REGISTRY_URL configuration",
|
||||
{ cause: err },
|
||||
);
|
||||
}
|
||||
if (!res.ok) {
|
||||
throw new BailianError(
|
||||
`Skill registry returned HTTP ${res.status}: ${url}`,
|
||||
ExitCode.NETWORK,
|
||||
res.status === 404
|
||||
? "Skill index not yet published or registry URL is incorrect; confirm the publisher has generated index.json"
|
||||
: "Remote error, retry later",
|
||||
);
|
||||
}
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = await res.json();
|
||||
} catch (err) {
|
||||
throw new BailianError(
|
||||
"Skill index index.json is not valid JSON",
|
||||
ExitCode.GENERAL,
|
||||
"Remote may be in the middle of publishing, retry later",
|
||||
{ cause: err },
|
||||
);
|
||||
}
|
||||
const index = parsed as SkillsIndex;
|
||||
if (
|
||||
typeof index !== "object" ||
|
||||
index === null ||
|
||||
typeof index.skills !== "object" ||
|
||||
index.skills === null
|
||||
) {
|
||||
throw new BailianError(
|
||||
"Skill index index.json has invalid structure",
|
||||
ExitCode.GENERAL,
|
||||
"Retry later or contact the publisher",
|
||||
);
|
||||
}
|
||||
return index;
|
||||
},
|
||||
{ attempts },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,29 +96,38 @@ export function resolveAssetFileName(entry?: SkillIndexEntry): string {
|
||||
}
|
||||
|
||||
/** Download the tar.br archive for a single skill (one skill = one GET) */
|
||||
export async function downloadSkillAsset(name: string, entry?: SkillIndexEntry): Promise<Buffer> {
|
||||
const url = `${getSkillRegistryBaseUrl()}/${name}/${resolveAssetFileName(entry)}`;
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(url, { signal: AbortSignal.timeout(ASSET_TIMEOUT_MS) });
|
||||
} catch (err) {
|
||||
throw new BailianError(
|
||||
`Failed to download skill ${name}: ${url}`,
|
||||
ExitCode.NETWORK,
|
||||
"Network error, retryable",
|
||||
{
|
||||
cause: err,
|
||||
},
|
||||
);
|
||||
}
|
||||
if (!res.ok) {
|
||||
throw new BailianError(
|
||||
`Failed to download skill ${name}: HTTP ${res.status}`,
|
||||
ExitCode.NETWORK,
|
||||
res.status === 404
|
||||
? "index.json and skill object are temporarily inconsistent (publishing in progress), retry later"
|
||||
: "Remote error, retry later",
|
||||
);
|
||||
}
|
||||
return Buffer.from(await res.arrayBuffer());
|
||||
export async function downloadSkillAsset(
|
||||
name: string,
|
||||
entry?: SkillIndexEntry,
|
||||
attempts: number = DEFAULT_ATTEMPTS,
|
||||
): Promise<Buffer> {
|
||||
return withRetry(
|
||||
async () => {
|
||||
const url = `${getSkillRegistryBaseUrl()}/${name}/${resolveAssetFileName(entry)}`;
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(url, { signal: AbortSignal.timeout(ASSET_TIMEOUT_MS) });
|
||||
} catch (err) {
|
||||
throw new BailianError(
|
||||
`Failed to download skill ${name}: ${url}`,
|
||||
ExitCode.NETWORK,
|
||||
"Network error, retryable",
|
||||
{
|
||||
cause: err,
|
||||
},
|
||||
);
|
||||
}
|
||||
if (!res.ok) {
|
||||
throw new BailianError(
|
||||
`Failed to download skill ${name}: HTTP ${res.status}`,
|
||||
ExitCode.NETWORK,
|
||||
res.status === 404
|
||||
? "index.json and skill object are temporarily inconsistent (publishing in progress), retry later"
|
||||
: "Remote error, retry later",
|
||||
);
|
||||
}
|
||||
return Buffer.from(await res.arrayBuffer());
|
||||
},
|
||||
{ attempts },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { existsSync, lstatSync, mkdtempSync, readFileSync, readdirSync, rmSync } from "fs";
|
||||
import { createHash } from "crypto";
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
import { brotliCompressSync } from "zlib";
|
||||
import { existsSync, lstatSync, mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
||||
import { createHash } from "node:crypto";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { brotliCompressSync } from "node:zlib";
|
||||
import tar from "tar-stream";
|
||||
import { afterEach, expect, test, vi } from "vite-plus/test";
|
||||
import { BailianError } from "../src/errors/base.ts";
|
||||
@@ -10,6 +10,18 @@ import type { AgentTarget } from "../src/skills/agents.ts";
|
||||
import { isSafeEntryName } from "../src/skills/extract.ts";
|
||||
import { installSkillFromBuffer, installSkillWithFanout } from "../src/skills/installer.ts";
|
||||
import { getSkillsDir } from "../src/skills/lock.ts";
|
||||
import { downloadSkillAsset, fetchSkillsIndex } from "../src/skills/registry.ts";
|
||||
|
||||
/** rmSync wrapped in a spy so tests can simulate host deletion guards (e.g. safe-delete) */
|
||||
const fsMocks = vi.hoisted(() => ({
|
||||
rmSync: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("node:fs", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:fs")>();
|
||||
fsMocks.rmSync.mockImplementation(actual.rmSync);
|
||||
return { ...actual, rmSync: fsMocks.rmSync };
|
||||
});
|
||||
|
||||
/** Run in an isolated temp config dir, restore env afterwards. */
|
||||
async function inTempConfigDir(fn: () => Promise<void>): Promise<void> {
|
||||
@@ -225,15 +237,83 @@ test("fanout install: downloads, links agents, and builds lock entry with merged
|
||||
});
|
||||
});
|
||||
|
||||
test("fanout install: download failure surfaces as BailianError and leaves no canonical dir", async () => {
|
||||
test("fanout install: download failure exhausts retries and leaves no canonical dir", async () => {
|
||||
await inTempConfigDir(async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => ({ ok: false, status: 404 })),
|
||||
);
|
||||
const fetchMock = vi.fn(async () => ({ ok: false, status: 404 }));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
await expect(
|
||||
installSkillWithFanout("demo", { contentHash: "sha256:whatever" }, []),
|
||||
).rejects.toThrow(BailianError);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||||
expect(existsSync(join(getSkillsDir(), "demo"))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ---- host deletion guards (safe-delete): backup cleanup must not fail a completed install ----
|
||||
|
||||
const GUARD_ERROR =
|
||||
'[safe-delete][SAFE_DELETE_BULK_CONFIRM_REQUIRED] {"count":1583,"threshold":500}';
|
||||
|
||||
test("installer: guard blocking backup deletion does not fail the install", async () => {
|
||||
await inTempConfigDir(async () => {
|
||||
await installSkillFromBuffer("demo", await buildTarBr({ "SKILL.md": VALID_SKILL_MD }));
|
||||
fsMocks.rmSync.mockImplementationOnce(() => {
|
||||
throw new Error(GUARD_ERROR);
|
||||
});
|
||||
const v2 = "---\nname: demo\ndescription: demo skill v2\n---\n";
|
||||
const installed = await installSkillFromBuffer("demo", await buildTarBr({ "SKILL.md": v2 }));
|
||||
expect(installed.name).toBe("demo");
|
||||
expect(readFileSync(join(getSkillsDir(), "demo", "SKILL.md"), "utf-8")).toBe(v2);
|
||||
// The blocked backup stays on disk but is inert (status scans ignore .old-*)
|
||||
const leftovers = readdirSync(getSkillsDir()).filter((entry) => entry.startsWith("demo.old-"));
|
||||
expect(leftovers).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
test("installer: temp cleanup failure does not mask the original error", async () => {
|
||||
await inTempConfigDir(async () => {
|
||||
fsMocks.rmSync.mockImplementationOnce(() => {
|
||||
throw new Error(GUARD_ERROR);
|
||||
});
|
||||
const buf = await buildTarBr({ "SKILL.md": VALID_SKILL_MD, "../evil.txt": "pwned\n" });
|
||||
await expect(installSkillFromBuffer("demo", buf)).rejects.toThrow(/unsafe tar entry/);
|
||||
});
|
||||
});
|
||||
|
||||
// ---- registry retry policy ----
|
||||
|
||||
test("registry: index fetch retries transient failures and succeeds", async () => {
|
||||
const indexPayload = { skills: { demo: { contentHash: "sha256:abc" } } };
|
||||
const fetchMock = vi.fn(async () => {
|
||||
if (fetchMock.mock.calls.length < 3) throw new Error("network down");
|
||||
return { ok: true, status: 200, json: async () => indexPayload };
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
const index = await fetchSkillsIndex(1000);
|
||||
expect(index.skills.demo.contentHash).toBe("sha256:abc");
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
test("registry: attempts=1 keeps the silent channel fail-fast", async () => {
|
||||
const fetchMock = vi.fn(async () => {
|
||||
throw new Error("network down");
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
await expect(fetchSkillsIndex(1000, 1)).rejects.toThrow(BailianError);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("registry: asset download retries transient HTTP errors", async () => {
|
||||
const fetchMock = vi.fn(async () => {
|
||||
if (fetchMock.mock.calls.length === 1) return { ok: false, status: 503 };
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer,
|
||||
};
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
const buffer = await downloadSkillAsset("demo");
|
||||
expect([...buffer]).toEqual([1, 2, 3]);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
@@ -6,8 +6,8 @@ export function deriveGroupPaths(commandPaths: string[]): string[] {
|
||||
const groups = new Set<string>();
|
||||
for (const path of commandPaths) {
|
||||
const parts = path.split(" ");
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
const prefix = parts.slice(0, i).join(" ");
|
||||
for (let partIndex = 1; partIndex < parts.length; partIndex++) {
|
||||
const prefix = parts.slice(0, partIndex).join(" ");
|
||||
const hasChildren = commandPaths.some(
|
||||
(candidate) => candidate.startsWith(`${prefix} `) && candidate !== prefix,
|
||||
);
|
||||
@@ -16,3 +16,21 @@ export function deriveGroupPaths(commandPaths: string[]): string[] {
|
||||
}
|
||||
return [...groups].sort();
|
||||
}
|
||||
|
||||
interface RegistryHelpPrinter {
|
||||
printHelp(commandPath: string[], output: NodeJS.WriteStream): void;
|
||||
}
|
||||
|
||||
export function captureRegistryHelp(printer: RegistryHelpPrinter, commandPath: string[]): string {
|
||||
let helpOutput = "";
|
||||
const output = {
|
||||
isTTY: false,
|
||||
write(chunk: string): boolean {
|
||||
helpOutput += chunk;
|
||||
return true;
|
||||
},
|
||||
} as NodeJS.WriteStream;
|
||||
|
||||
printer.printHelp(commandPath, output);
|
||||
return helpOutput;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { promisify } from "util";
|
||||
import { monorepoRoot } from "./monorepo-root.ts";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const tsxLoader = import.meta.resolve("tsx");
|
||||
|
||||
export interface RunCliResult {
|
||||
stdout: string;
|
||||
@@ -18,24 +19,28 @@ export function resolveTsxBin(): string {
|
||||
return join(root, "node_modules", ".bin", name);
|
||||
}
|
||||
|
||||
/** 子进程通过 tsx 执行指定 main.ts 入口 */
|
||||
/** 子进程通过 Node 的 tsx loader 执行指定 main.ts 入口,不启动 tsx CLI 的 IPC server。 */
|
||||
export async function runNodeMain(
|
||||
mainTs: string,
|
||||
args: string[],
|
||||
options: { cwd: string; env?: NodeJS.ProcessEnv } = { cwd: process.cwd() },
|
||||
): Promise<RunCliResult> {
|
||||
try {
|
||||
const { stdout, stderr } = await execFileAsync(resolveTsxBin(), [mainTs, ...args], {
|
||||
cwd: options.cwd,
|
||||
encoding: "utf8",
|
||||
maxBuffer: 32 * 1024 * 1024,
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_NO_WARNINGS: "1",
|
||||
DO_NOT_TRACK: "1",
|
||||
...options.env,
|
||||
const { stdout, stderr } = await execFileAsync(
|
||||
process.execPath,
|
||||
["--import", tsxLoader, mainTs, ...args],
|
||||
{
|
||||
cwd: options.cwd,
|
||||
encoding: "utf8",
|
||||
maxBuffer: 32 * 1024 * 1024,
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_NO_WARNINGS: "1",
|
||||
DO_NOT_TRACK: "1",
|
||||
...options.env,
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
return { stdout: stdout ?? "", stderr: stderr ?? "", exitCode: 0 };
|
||||
} catch (err: unknown) {
|
||||
const e = err as {
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
interface FixtureOutput {
|
||||
args: string[];
|
||||
execArgv: string[];
|
||||
marker: string | null;
|
||||
}
|
||||
|
||||
if (process.argv[2] === "--fail") {
|
||||
process.stderr.write("fixture failure\n");
|
||||
process.exitCode = 7;
|
||||
} else {
|
||||
const output: FixtureOutput = {
|
||||
args: process.argv.slice(2),
|
||||
execArgv: process.execArgv,
|
||||
marker: process.env.RUNNER_FIXTURE_MARKER ?? null,
|
||||
};
|
||||
process.stdout.write(`${JSON.stringify(output)}\n`);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { expect, test } from "vite-plus/test";
|
||||
import { captureRegistryHelp } from "../src/registry-smoke.ts";
|
||||
|
||||
test("captureRegistryHelp 捕获指定命令路径的非 TTY 输出", () => {
|
||||
const requestedPaths: string[][] = [];
|
||||
const renderer = {
|
||||
printHelp(commandPath: string[], output: NodeJS.WriteStream): void {
|
||||
requestedPaths.push(commandPath);
|
||||
output.write(`Usage: bl ${commandPath.join(" ")}`);
|
||||
},
|
||||
};
|
||||
|
||||
const output = captureRegistryHelp(renderer, ["text", "chat"]);
|
||||
|
||||
expect(output).toBe("Usage: bl text chat");
|
||||
expect(requestedPaths).toEqual([["text", "chat"]]);
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { expect, test } from "vite-plus/test";
|
||||
import { runNodeMain } from "../src/run-subprocess.ts";
|
||||
|
||||
const testsDir = dirname(fileURLToPath(import.meta.url));
|
||||
const fixtureMainTs = join(testsDir, "fixtures", "echo-argv.ts");
|
||||
|
||||
test("runNodeMain 通过 Node 的 tsx loader 执行 TypeScript 入口", async () => {
|
||||
const result = await runNodeMain(fixtureMainTs, ["alpha", "beta"], {
|
||||
cwd: testsDir,
|
||||
env: { RUNNER_FIXTURE_MARKER: "forwarded" },
|
||||
});
|
||||
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
expect(result.stderr).toBe("");
|
||||
const output = JSON.parse(result.stdout) as {
|
||||
args: string[];
|
||||
execArgv: string[];
|
||||
marker: string | null;
|
||||
};
|
||||
expect(output.args).toEqual(["alpha", "beta"]);
|
||||
expect(output.marker).toBe("forwarded");
|
||||
const importFlagIndex = output.execArgv.indexOf("--import");
|
||||
expect(importFlagIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(output.execArgv[importFlagIndex + 1]).toMatch(/tsx/);
|
||||
});
|
||||
|
||||
test("runNodeMain 返回 TypeScript 入口的 stderr 与非零退出码", async () => {
|
||||
const result = await runNodeMain(fixtureMainTs, ["--fail"], { cwd: testsDir });
|
||||
|
||||
expect(result).toEqual({
|
||||
stdout: "",
|
||||
stderr: "fixture failure\n",
|
||||
exitCode: 7,
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "knowledge-studio-cli",
|
||||
"version": "1.18.1",
|
||||
"version": "1.18.2",
|
||||
"description": "Lightweight RAG CLI for Aliyun Model Studio — focused on knowledge-base retrieval.",
|
||||
"keywords": [
|
||||
"alibaba-cloud",
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { deriveGroupPaths } from "e2e/registry-smoke";
|
||||
import { captureRegistryHelp, deriveGroupPaths } from "e2e/registry-smoke";
|
||||
import { CommandRegistry, resolve } from "bailian-cli-runtime";
|
||||
import pkg from "../../package.json" with { type: "json" };
|
||||
import { commands } from "../../src/commands.ts";
|
||||
import { runKscli } from "./helpers.ts";
|
||||
|
||||
const commandPaths = Object.keys(commands).sort();
|
||||
const groupPaths = deriveGroupPaths(commandPaths);
|
||||
const registry = new CommandRegistry(commands, "kscli");
|
||||
|
||||
describe("e2e: kscli registry smoke", () => {
|
||||
test("根帮助展示 kscli 与全局 flag", async () => {
|
||||
@@ -38,13 +40,25 @@ describe("e2e: kscli registry smoke", () => {
|
||||
expect(stderr).toMatch(/--query|Missing required/i);
|
||||
});
|
||||
|
||||
test.each(commandPaths)("已注册命令 %s --help 成功", async (path) => {
|
||||
const { stderr, exitCode } = await runKscli([...path.split(" "), "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
test.each(commandPaths)("已注册命令 %s --help 成功", (path) => {
|
||||
const commandPath = path.split(" ");
|
||||
|
||||
expect(resolve([...commandPath, "--help"], registry)).toEqual({
|
||||
kind: "help",
|
||||
path: commandPath,
|
||||
});
|
||||
expect(captureRegistryHelp(registry, commandPath)).toContain(`Usage: kscli ${path}`);
|
||||
});
|
||||
|
||||
test.each(groupPaths)("命令分组 %s --help 成功", async (path) => {
|
||||
const { stderr, exitCode } = await runKscli([...path.split(" "), "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
test.each(groupPaths)("命令分组 %s --help 成功", (path) => {
|
||||
const commandPath = path.split(" ");
|
||||
|
||||
expect(resolve([...commandPath, "--help"], registry)).toEqual({
|
||||
kind: "help",
|
||||
path: commandPath,
|
||||
});
|
||||
expect(captureRegistryHelp(registry, commandPath)).toContain(
|
||||
`Usage: kscli ${path} <command> [flags]`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli-runtime",
|
||||
"version": "1.18.1",
|
||||
"version": "1.18.2",
|
||||
"description": "Runtime framework for bailian-cli (createCli, registry, args, output, pipeline). See https://www.npmjs.com/package/bailian-cli for usage.",
|
||||
"homepage": "https://bailian.console.aliyun.com/cli",
|
||||
"bugs": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: bailian-cli
|
||||
metadata:
|
||||
version: "1.18.1"
|
||||
version: "1.18.2"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
|
||||
@@ -149,12 +149,12 @@ bl quota list --output json
|
||||
|
||||
### `bl quota update`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | -------------------------------------------------------------------- |
|
||||
| **Name** | `quota update` |
|
||||
| **Description** | Update model rate limits (QPM/TPM), or clear them with --delete |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl quota update --model <model> [--rpm <n>] [--tpm <n>] [--delete]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------------------------------------------------- |
|
||||
| **Name** | `quota update` |
|
||||
| **Description** | Update model rate limits (QPM/TPM), or clear them with --delete |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl quota update --model <model> [--rpm <n>] [--tpm <n>] [--delete] [--yes]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -164,12 +164,14 @@ bl quota list --output json
|
||||
| `--rpm <n>` | number | no | Max requests per minute (QPM) |
|
||||
| `--tpm <n>` | number | no | Max tokens per minute (TPM) |
|
||||
| `--delete` | switch | no | Clear all custom rate limits for the model |
|
||||
| `--yes` | switch | no | Skip the confirmation prompt for --delete |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
|
||||
#### Notes
|
||||
|
||||
- Fields you omit keep their current values (server-side OVERLAY merge); --delete clears all custom limits.
|
||||
- --delete requires confirmation; pass --yes to skip the prompt in scripts.
|
||||
- Setting TPM without an existing QPM limit is rejected server-side — pass --rpm first or together.
|
||||
|
||||
#### Examples
|
||||
@@ -186,6 +188,10 @@ bl quota update --model qwen3-max --tpm 500000
|
||||
bl quota update --model qwen-plus --delete
|
||||
```
|
||||
|
||||
```bash
|
||||
bl quota update --model qwen-plus --delete --yes
|
||||
```
|
||||
|
||||
```bash
|
||||
bl quota update --model qwen-plus --rpm 60 --output json
|
||||
```
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: bailian-finetune
|
||||
metadata:
|
||||
version: "1.18.1"
|
||||
version: "1.18.2"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
|
||||
@@ -19,20 +19,25 @@ Index: [index.md](index.md)
|
||||
|
||||
### `bl dataset delete`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------- |
|
||||
| **Name** | `dataset delete` |
|
||||
| **Description** | Delete a dataset file by ID |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl dataset delete --file-id <id>` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------ |
|
||||
| **Name** | `dataset delete` |
|
||||
| **Description** | Delete a dataset file by ID |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl dataset delete --file-id <id> [--yes]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------ | ------ | -------- | -------------------------- |
|
||||
| `--file-id <id>` | string | yes | Dataset file ID (required) |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------ | ------ | -------- | ---------------------------- |
|
||||
| `--file-id <id>` | string | yes | Dataset file ID (required) |
|
||||
| `--yes` | switch | no | Skip the confirmation prompt |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
|
||||
#### Notes
|
||||
|
||||
- Irreversible — the dataset file is permanently removed.
|
||||
|
||||
#### Examples
|
||||
|
||||
@@ -44,6 +49,10 @@ bl dataset delete --file-id file-id-xxx
|
||||
bl dataset delete --file-id file-id-xxx --dry-run
|
||||
```
|
||||
|
||||
```bash
|
||||
bl dataset delete --file-id file-id-xxx --yes
|
||||
```
|
||||
|
||||
### `bl dataset get`
|
||||
|
||||
| Field | Value |
|
||||
|
||||
@@ -73,12 +73,12 @@ bl deploy audio create --model-name my-cosyvoice-ft --display-name my-tts --dry-
|
||||
|
||||
### `bl deploy delete`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------------------------------- |
|
||||
| **Name** | `deploy delete` |
|
||||
| **Description** | Delete a model deployment (must be STOPPED or FAILED) |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl deploy delete --deployed-model <id> [--skip-precheck]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------------ |
|
||||
| **Name** | `deploy delete` |
|
||||
| **Description** | Delete a model deployment (must be STOPPED or FAILED) |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl deploy delete --deployed-model <id> [--skip-precheck] [--yes]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -86,9 +86,14 @@ bl deploy audio create --model-name my-cosyvoice-ft --display-name my-tts --dry-
|
||||
| ----------------------- | ------ | -------- | --------------------------------------------- |
|
||||
| `--deployed-model <id>` | string | yes | Deployed model identifier (required) |
|
||||
| `--skip-precheck` | switch | no | Skip the local STOPPED/FAILED status precheck |
|
||||
| `--yes` | switch | no | Skip the confirmation prompt |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
|
||||
#### Notes
|
||||
|
||||
- Irreversible — the deployment is permanently destroyed.
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
@@ -99,6 +104,10 @@ bl deploy delete --deployed-model dep-...
|
||||
bl deploy delete --deployed-model dep-... --dry-run
|
||||
```
|
||||
|
||||
```bash
|
||||
bl deploy delete --deployed-model dep-... --yes
|
||||
```
|
||||
|
||||
### `bl deploy get`
|
||||
|
||||
| Field | Value |
|
||||
|
||||
@@ -181,23 +181,25 @@ bl finetune checkpoints --job-id ft-xxx --output json
|
||||
|
||||
### `bl finetune delete`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------- |
|
||||
| **Name** | `finetune delete` |
|
||||
| **Description** | Delete a fine-tune job record |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl finetune delete --job-id <id>` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------ |
|
||||
| **Name** | `finetune delete` |
|
||||
| **Description** | Delete a fine-tune job record |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl finetune delete --job-id <id> [--yes]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------ | ------ | -------- | --------------------------- |
|
||||
| `--job-id <id>` | string | yes | Fine-tune job ID (required) |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------ | ------ | -------- | ---------------------------- |
|
||||
| `--job-id <id>` | string | yes | Fine-tune job ID (required) |
|
||||
| `--yes` | switch | no | Skip the confirmation prompt |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
|
||||
#### Notes
|
||||
|
||||
- Irreversible — the job record is permanently removed.
|
||||
- Cancel a RUNNING job first via `finetune cancel` — the platform refuses to delete jobs that are still in flight.
|
||||
|
||||
#### Examples
|
||||
@@ -210,6 +212,10 @@ bl finetune delete --job-id ft-xxx
|
||||
bl finetune delete --job-id ft-xxx --dry-run
|
||||
```
|
||||
|
||||
```bash
|
||||
bl finetune delete --job-id ft-xxx --yes
|
||||
```
|
||||
|
||||
### `bl finetune export`
|
||||
|
||||
| Field | Value |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: bailian-gen
|
||||
metadata:
|
||||
version: "1.18.1"
|
||||
version: "1.18.2"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: bailian-managed-agent
|
||||
metadata:
|
||||
version: "1.18.1"
|
||||
version: "1.18.2"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: bailian-protocol
|
||||
metadata:
|
||||
version: "1.18.1"
|
||||
version: "1.18.2"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: bailian-web-search
|
||||
metadata:
|
||||
version: "1.18.1"
|
||||
version: "1.18.2"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
|
||||
Reference in New Issue
Block a user