Skip to content

Commit 3f78de2

Browse files
authored
Merge pull request #183 from modelstudioai/feat/confirm-destructive-ops
feat: require --yes confirmation for destructive operations
2 parents e3f2996 + 38926ae commit 3f78de2

12 files changed

Lines changed: 233 additions & 45 deletions

File tree

packages/commands/src/commands/dataset/delete.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { defineCommand, deleteDataset, type FlagsDef } from "bailian-cli-core";
2-
import { emitResult, emitBare } from "bailian-cli-runtime";
2+
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
33

44
const DELETE_FLAGS = {
55
fileId: {
@@ -8,14 +8,28 @@ const DELETE_FLAGS = {
88
description: { "en-US": "Dataset file ID (required)", "zh-CN": "数据集文件 ID(必填)" },
99
required: true,
1010
},
11+
yes: {
12+
type: "switch",
13+
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
14+
},
1115
} satisfies FlagsDef;
1216

1317
export default defineCommand({
1418
description: { "en-US": "Delete a dataset file by ID", "zh-CN": "通过 ID 删除数据集文件" },
1519
auth: "apiKey",
16-
usageArgs: "--file-id <id>",
20+
usageArgs: "--file-id <id> [--yes]",
1721
flags: DELETE_FLAGS,
18-
exampleArgs: ["--file-id file-id-xxx", "--file-id file-id-xxx --dry-run"],
22+
exampleArgs: [
23+
"--file-id file-id-xxx",
24+
"--file-id file-id-xxx --dry-run",
25+
"--file-id file-id-xxx --yes",
26+
],
27+
notes: [
28+
{
29+
"en-US": "Irreversible — the dataset file is permanently removed.",
30+
"zh-CN": "该操作不可撤销——数据集文件将被永久删除。",
31+
},
32+
],
1933
async run(ctx) {
2034
const { settings, flags } = ctx;
2135
const fileId = flags.fileId;
@@ -25,6 +39,11 @@ export default defineCommand({
2539
return;
2640
}
2741

42+
await confirmDangerousAction(
43+
`Delete dataset file ${fileId}.\nThe file is permanently removed. This cannot be undone.`,
44+
flags.yes ?? false,
45+
);
46+
2847
const response = await deleteDataset(ctx.client, fileId);
2948

3049
if (settings.quiet) {

packages/commands/src/commands/deploy/delete.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
ExitCode,
77
type FlagsDef,
88
} from "bailian-cli-core";
9-
import { emitResult, emitBare } from "bailian-cli-runtime";
9+
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
1010

1111
const DELETE_FLAGS = {
1212
deployedModel: {
@@ -25,6 +25,10 @@ const DELETE_FLAGS = {
2525
"zh-CN": "跳过本地 STOPPED/FAILED 状态预检查",
2626
},
2727
},
28+
yes: {
29+
type: "switch",
30+
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
31+
},
2832
} satisfies FlagsDef;
2933

3034
/**
@@ -40,9 +44,19 @@ export default defineCommand({
4044
"zh-CN": "删除模型部署(状态必须为 STOPPED 或 FAILED)",
4145
},
4246
auth: "apiKey",
43-
usageArgs: "--deployed-model <id> [--skip-precheck]",
47+
usageArgs: "--deployed-model <id> [--skip-precheck] [--yes]",
4448
flags: DELETE_FLAGS,
45-
exampleArgs: ["--deployed-model dep-...", "--deployed-model dep-... --dry-run"],
49+
notes: [
50+
{
51+
"en-US": "Irreversible — the deployment is permanently destroyed.",
52+
"zh-CN": "该操作不可撤销——部署将被永久销毁。",
53+
},
54+
],
55+
exampleArgs: [
56+
"--deployed-model dep-...",
57+
"--deployed-model dep-... --dry-run",
58+
"--deployed-model dep-... --yes",
59+
],
4660
async run(ctx) {
4761
const { settings, flags } = ctx;
4862
const deployedModel = flags.deployedModel;
@@ -73,6 +87,11 @@ export default defineCommand({
7387
}
7488
}
7589

90+
await confirmDangerousAction(
91+
`Delete deployment ${deployedModel}.\nThe deployment is permanently destroyed. This cannot be undone.`,
92+
flags.yes ?? false,
93+
);
94+
7695
const response = await deleteDeployment(ctx.client, deployedModel);
7796

7897
if (settings.quiet) {

packages/commands/src/commands/finetune/delete.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { defineCommand, deleteFineTune, type FlagsDef } from "bailian-cli-core";
2-
import { emitResult, emitBare } from "bailian-cli-runtime";
2+
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
33

44
const DELETE_FLAGS = {
55
jobId: {
@@ -8,15 +8,23 @@ const DELETE_FLAGS = {
88
description: { "en-US": "Fine-tune job ID (required)", "zh-CN": "微调任务 ID(必填)" },
99
required: true,
1010
},
11+
yes: {
12+
type: "switch",
13+
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
14+
},
1115
} satisfies FlagsDef;
1216

1317
export default defineCommand({
1418
description: { "en-US": "Delete a fine-tune job record", "zh-CN": "删除微调任务记录" },
1519
auth: "apiKey",
16-
usageArgs: "--job-id <id>",
20+
usageArgs: "--job-id <id> [--yes]",
1721
flags: DELETE_FLAGS,
18-
exampleArgs: ["--job-id ft-xxx", "--job-id ft-xxx --dry-run"],
22+
exampleArgs: ["--job-id ft-xxx", "--job-id ft-xxx --dry-run", "--job-id ft-xxx --yes"],
1923
notes: [
24+
{
25+
"en-US": "Irreversible — the job record is permanently removed.",
26+
"zh-CN": "该操作不可撤销——任务记录将被永久删除。",
27+
},
2028
{
2129
"en-US":
2230
"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({
3240
return;
3341
}
3442

43+
await confirmDangerousAction(
44+
`Delete fine-tune job record ${jobId}.\nThe job record is permanently removed. This cannot be undone.`,
45+
flags.yes ?? false,
46+
);
47+
3548
const response = await deleteFineTune(ctx.client, jobId);
3649

3750
if (settings.quiet) {

packages/commands/src/commands/quota/update.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { defineCommand, detectOutputFormat, modelsLimitsPath } from "bailian-cli-core";
2-
import { emitResult } from "bailian-cli-runtime";
2+
import { emitResult, confirmDangerousAction } from "bailian-cli-runtime";
33
import { formatNumber } from "../shared/format.ts";
44

55
const MINUTE_SECONDS = 60;
@@ -10,7 +10,7 @@ export default defineCommand({
1010
"zh-CN": "更新模型限流配置(QPM/TPM),或使用 --delete 清除配置",
1111
},
1212
auth: "apiKey",
13-
usageArgs: "--model <model> [--rpm <n>] [--tpm <n>] [--delete]",
13+
usageArgs: "--model <model> [--rpm <n>] [--tpm <n>] [--delete] [--yes]",
1414
flags: {
1515
model: {
1616
type: "string",
@@ -41,11 +41,19 @@ export default defineCommand({
4141
"zh-CN": "清除该模型的所有自定义限流配置",
4242
},
4343
},
44+
yes: {
45+
type: "switch",
46+
description: {
47+
"en-US": "Skip the confirmation prompt for --delete",
48+
"zh-CN": "使用 --delete 时跳过确认提示",
49+
},
50+
},
4451
},
4552
exampleArgs: [
4653
"--model qwen-plus --rpm 60 --tpm 100000",
4754
"--model qwen3-max --tpm 500000",
4855
"--model qwen-plus --delete",
56+
"--model qwen-plus --delete --yes",
4957
"--model qwen-plus --rpm 60 --output json",
5058
],
5159
notes: [
@@ -55,6 +63,10 @@ export default defineCommand({
5563
"zh-CN":
5664
"未指定的字段将保留当前值(服务端 OVERLAY 合并);--delete 会清除所有自定义限流配置。",
5765
},
66+
{
67+
"en-US": "--delete requires confirmation; pass --yes to skip the prompt in scripts.",
68+
"zh-CN": "--delete 需要确认;脚本中可加 --yes 跳过交互提示。",
69+
},
5870
{
5971
"en-US":
6072
"Setting TPM without an existing QPM limit is rejected server-side — pass --rpm first or together.",
@@ -98,6 +110,13 @@ export default defineCommand({
98110
return;
99111
}
100112

113+
if (flags.delete) {
114+
await confirmDangerousAction(
115+
`Clear all custom rate limits for model ${modelName}.\nYour custom QPM/TPM configuration will be removed.`,
116+
flags.yes ?? false,
117+
);
118+
}
119+
101120
const result = await ctx.client.requestJson<{ request_id?: string }>({
102121
path: modelsLimitsPath(),
103122
method: "POST",

packages/commands/tests/e2e/dataset.e2e.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,42 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (offline)", () => {
292292
expect(exitCode, stdout + stderr).not.toBe(0);
293293
expect(`${stdout}\n${stderr}`).toMatch(/--schema video is not supported/);
294294
});
295+
296+
test("dataset delete --help 展示 --yes", async () => {
297+
const { stderr, exitCode } = await runCommandE2e(DATASET_ROUTES, [
298+
"dataset",
299+
"delete",
300+
"--help",
301+
]);
302+
expect(exitCode, stderr).toBe(0);
303+
expect(stderr).toMatch(/--yes/i);
304+
});
305+
306+
test("dataset delete --dry-run 发出结构化动作", async () => {
307+
const { stdout, stderr, exitCode } = await runCommandE2e(DATASET_ROUTES, [
308+
"dataset",
309+
"delete",
310+
"--file-id",
311+
"file-id-xxx",
312+
"--dry-run",
313+
"--output",
314+
"json",
315+
]);
316+
expect(exitCode, stderr).toBe(0);
317+
const data = parseStdoutJson<{ action: string }>(stdout);
318+
expect(data.action).toBe("dataset.delete");
319+
});
320+
321+
test("dataset delete 非 TTY 无 --yes 报 USAGE (2)", async () => {
322+
const { stderr, exitCode } = await runCommandE2e(DATASET_ROUTES, [
323+
"dataset",
324+
"delete",
325+
"--file-id",
326+
"file-id-xxx",
327+
]);
328+
expect(exitCode).toBe(2);
329+
expect(stderr).toMatch(/--yes/);
330+
});
295331
});
296332

297333
describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (DashScope)", () => {

packages/commands/tests/e2e/deploy.e2e.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,25 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: deploy (offline)", () => {
201201
const data = parseStdoutJson<{ action: string }>(stdout);
202202
expect(data.action).toBe(`deploy.${sub}`);
203203
});
204+
205+
test("deploy delete --help 展示 --yes", async () => {
206+
const { stderr, exitCode } = await runCommandE2e(DEPLOY_ROUTES, ["deploy", "delete", "--help"]);
207+
expect(exitCode, stderr).toBe(0);
208+
expect(stderr).toMatch(/--yes/i);
209+
});
210+
211+
test("deploy delete 非 TTY 无 --yes 报 USAGE (2)", async () => {
212+
// --skip-precheck 保证确认门在发任何网络请求前触发
213+
const { stderr, exitCode } = await runCommandE2e(DEPLOY_ROUTES, [
214+
"deploy",
215+
"delete",
216+
"--deployed-model",
217+
"dep-xxx",
218+
"--skip-precheck",
219+
]);
220+
expect(exitCode).toBe(2);
221+
expect(stderr).toMatch(/--yes/);
222+
});
204223
});
205224

206225
describe.skipIf(!isDashScopeE2EReady())("e2e: deploy (DashScope)", () => {

packages/commands/tests/e2e/finetune.e2e.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,27 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => {
294294
expect(data.action).toBe(`finetune.${sub}`);
295295
});
296296

297+
test("finetune delete --help 展示 --yes", async () => {
298+
const { stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [
299+
"finetune",
300+
"delete",
301+
"--help",
302+
]);
303+
expect(exitCode, stderr).toBe(0);
304+
expect(stderr).toMatch(/--yes/i);
305+
});
306+
307+
test("finetune delete 非 TTY 无 --yes 报 USAGE (2)", async () => {
308+
const { stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [
309+
"finetune",
310+
"delete",
311+
"--job-id",
312+
"ft-xxx",
313+
]);
314+
expect(exitCode).toBe(2);
315+
expect(stderr).toMatch(/--yes/);
316+
});
317+
297318
test("finetune create --dry-run 解析多 datasets 中的空白", async () => {
298319
const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [
299320
"finetune",

packages/commands/tests/e2e/quota.e2e.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ describe("e2e: quota", () => {
3232
expect(stderr).toContain("--rpm");
3333
expect(stderr).toContain("--tpm");
3434
expect(stderr).toContain("--delete");
35+
expect(stderr).toContain("--yes");
3536
});
3637

3738
test("quota request 作为 quota update 的兼容别名可用", async () => {
@@ -92,6 +93,17 @@ describe("e2e: quota", () => {
9293
expect(stderr).toContain("cannot be combined");
9394
});
9495

96+
test("quota update --delete 非 TTY 无 --yes 报 USAGE (2)", async () => {
97+
// 注入假 key 让 apiKey 鉴权通过;确认门在发任何网络请求前触发
98+
const { stderr, exitCode } = await runCommandE2e(
99+
QUOTA_ROUTES,
100+
["quota", "update", "--model", "qwen-plus", "--delete"],
101+
{ DASHSCOPE_API_KEY: "sk-e2e-quota-delete" },
102+
);
103+
expect(exitCode).toBe(2);
104+
expect(stderr).toMatch(/--yes/);
105+
});
106+
95107
test("quota update --rpm 负数报错", async () => {
96108
const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, [
97109
"quota",

skills/bailian-cli/reference/quota.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -149,12 +149,12 @@ bl quota list --output json
149149

150150
### `bl quota update`
151151

152-
| Field | Value |
153-
| ------------------ | -------------------------------------------------------------------- |
154-
| **Name** | `quota update` |
155-
| **Description** | Update model rate limits (QPM/TPM), or clear them with --delete |
156-
| **Authentication** | API Key |
157-
| **Usage** | `bl quota update --model <model> [--rpm <n>] [--tpm <n>] [--delete]` |
152+
| Field | Value |
153+
| ------------------ | ---------------------------------------------------------------------------- |
154+
| **Name** | `quota update` |
155+
| **Description** | Update model rate limits (QPM/TPM), or clear them with --delete |
156+
| **Authentication** | API Key |
157+
| **Usage** | `bl quota update --model <model> [--rpm <n>] [--tpm <n>] [--delete] [--yes]` |
158158

159159
#### Flags
160160

@@ -164,12 +164,14 @@ bl quota list --output json
164164
| `--rpm <n>` | number | no | Max requests per minute (QPM) |
165165
| `--tpm <n>` | number | no | Max tokens per minute (TPM) |
166166
| `--delete` | switch | no | Clear all custom rate limits for the model |
167+
| `--yes` | switch | no | Skip the confirmation prompt for --delete |
167168
| `--api-key <key>` | string | no | API key |
168169
| `--base-url <url>` | string | no | API base URL |
169170

170171
#### Notes
171172

172173
- Fields you omit keep their current values (server-side OVERLAY merge); --delete clears all custom limits.
174+
- --delete requires confirmation; pass --yes to skip the prompt in scripts.
173175
- Setting TPM without an existing QPM limit is rejected server-side — pass --rpm first or together.
174176

175177
#### Examples
@@ -186,6 +188,10 @@ bl quota update --model qwen3-max --tpm 500000
186188
bl quota update --model qwen-plus --delete
187189
```
188190

191+
```bash
192+
bl quota update --model qwen-plus --delete --yes
193+
```
194+
189195
```bash
190196
bl quota update --model qwen-plus --rpm 60 --output json
191197
```

0 commit comments

Comments
 (0)