mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
feat(cli): expose high-risk confirmation guidance in help and skills
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { expect, test } from "vite-plus/test";
|
||||
|
||||
const repositoryRoot = join(dirname(fileURLToPath(import.meta.url)), "../../..");
|
||||
const skillsRoot = join(repositoryRoot, "skills");
|
||||
|
||||
test("every generated high-risk command reference requires user confirmation before --yes", () => {
|
||||
let highRiskCommandCount = 0;
|
||||
|
||||
for (const skillDirectory of readdirSync(skillsRoot, { withFileTypes: true })) {
|
||||
if (!skillDirectory.isDirectory()) continue;
|
||||
const referenceDirectory = join(skillsRoot, skillDirectory.name, "reference");
|
||||
|
||||
let referenceFiles: string[];
|
||||
try {
|
||||
referenceFiles = readdirSync(referenceDirectory).filter(
|
||||
(fileName) => fileName.endsWith(".md") && fileName !== "index.md",
|
||||
);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const referenceFile of referenceFiles) {
|
||||
const markdown = readFileSync(join(referenceDirectory, referenceFile), "utf8");
|
||||
const commandSections = markdown.split(/(?=^### `bl )/m).slice(1);
|
||||
|
||||
for (const commandSection of commandSections) {
|
||||
if (!commandSection.includes("`--yes`")) continue;
|
||||
highRiskCommandCount += 1;
|
||||
expect(commandSection).toMatch(/\|\s+\*\*Risk\*\*\s+\|\s+`high`\s+\|/);
|
||||
expect(commandSection).toMatch(/\|\s+\*\*Risk message\*\*\s+\|\s+.+\|/);
|
||||
expect(commandSection).toMatch(/type=.*requires_confirmation/);
|
||||
const agentSafetyLine = commandSection
|
||||
.split("\n")
|
||||
.find((line) => line.startsWith("> **Agent safety:**"));
|
||||
expect(agentSafetyLine).toBeDefined();
|
||||
expect(agentSafetyLine).toMatch(/never add `--yes` automatically/i);
|
||||
expect(agentSafetyLine).toMatch(/explicit user confirmation/i);
|
||||
expect(agentSafetyLine).not.toContain("`--dry-run`");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(highRiskCommandCount).toBeGreaterThan(0);
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
AnyCommand,
|
||||
AuthRequirement,
|
||||
CommandRiskLevel,
|
||||
FlagDef,
|
||||
FlagsDef,
|
||||
LocalizedText,
|
||||
@@ -41,10 +42,16 @@ const AUTH_LABELS = {
|
||||
none: { "en-US": "No Auth", "zh-CN": "无需鉴权" },
|
||||
} satisfies Record<AuthRequirement, LocalizedText>;
|
||||
|
||||
const RISK_LEVEL_LABELS = {
|
||||
high: { "en-US": "high", "zh-CN": "高风险" },
|
||||
} satisfies Record<CommandRiskLevel, LocalizedText>;
|
||||
|
||||
const HELP_TEXT = {
|
||||
usage: { "en-US": "Usage:", "zh-CN": "用法:" },
|
||||
commands: { "en-US": "Commands:", "zh-CN": "命令:" },
|
||||
authentication: { "en-US": "Authentication:", "zh-CN": "鉴权方式:" },
|
||||
risk: { "en-US": "Risk:", "zh-CN": "风险等级:" },
|
||||
riskMessage: { "en-US": "Risk message:", "zh-CN": "风险说明:" },
|
||||
flags: { "en-US": "Flags:", "zh-CN": "选项:" },
|
||||
globalFlags: { "en-US": "Global Flags:", "zh-CN": "全局选项:" },
|
||||
modelAuthFlags: { "en-US": "Model Auth Flags:", "zh-CN": "模型鉴权选项:" },
|
||||
@@ -64,6 +71,10 @@ const HELP_TEXT = {
|
||||
},
|
||||
notes: { "en-US": "Notes:", "zh-CN": "说明:" },
|
||||
examples: { "en-US": "Examples:", "zh-CN": "示例:" },
|
||||
confirmedExample: {
|
||||
"en-US": "# Only after explicit confirmation:",
|
||||
"zh-CN": "# 仅在明确确认后执行:",
|
||||
},
|
||||
minimalWorkflow: { "en-US": "Minimal workflow.yaml:", "zh-CN": "最小 workflow.yaml:" },
|
||||
tryIt: { "en-US": "Try it:", "zh-CN": "试一试:" },
|
||||
} satisfies Record<string, LocalizedText>;
|
||||
@@ -432,6 +443,12 @@ ${authFlagSections ? `${authFlagSections}\n\n` : ""}${b(this.localize(HELP_TEXT.
|
||||
out.write(
|
||||
`${b(this.localize(HELP_TEXT.authentication))} ${a(this.localize(AUTH_LABELS[cmd.auth]))}\n`,
|
||||
);
|
||||
if (cmd.risk !== undefined) {
|
||||
out.write(
|
||||
`${b(this.localize(HELP_TEXT.risk))} ${a(this.localize(RISK_LEVEL_LABELS[cmd.risk.level]))}\n`,
|
||||
);
|
||||
out.write(`${b(this.localize(HELP_TEXT.riskMessage))} ${this.localize(cmd.risk.message)}\n`);
|
||||
}
|
||||
const flagEntries = [
|
||||
...Object.entries(cmd.flags ?? {}),
|
||||
...Object.entries(confirmationFlagDefs(cmd)),
|
||||
@@ -460,6 +477,9 @@ ${authFlagSections ? `${authFlagSections}\n\n` : ""}${b(this.localize(HELP_TEXT.
|
||||
out.write(`\n${b(this.localize(HELP_TEXT.examples))}\n`);
|
||||
for (const example of cmd.exampleArgs) {
|
||||
const localizedExample = this.localize(example);
|
||||
if (cmd.risk !== undefined && /(?:^|\s)--yes(?:\s|$)/.test(localizedExample)) {
|
||||
out.write(` ${d(this.localize(HELP_TEXT.confirmedExample))}\n`);
|
||||
}
|
||||
const line = localizedExample.startsWith("#")
|
||||
? localizedExample
|
||||
: localizedExample
|
||||
|
||||
@@ -47,6 +47,13 @@ test("registry renders runtime help copy with the selected language", async () =
|
||||
},
|
||||
],
|
||||
auth: "none",
|
||||
risk: {
|
||||
level: "high",
|
||||
message: {
|
||||
"en-US": "This operation is permanent.",
|
||||
"zh-CN": "该操作无法撤销。",
|
||||
},
|
||||
},
|
||||
run: async () => {},
|
||||
});
|
||||
const registry = new CommandRegistry({ test: command }, "bl", translator);
|
||||
@@ -69,6 +76,8 @@ test("registry renders runtime help copy with the selected language", async () =
|
||||
|
||||
output = "";
|
||||
registry.printHelp(["test"], stream);
|
||||
expect(output).toContain("风险等级: 高风险");
|
||||
expect(output).toContain("风险说明: 该操作无法撤销。");
|
||||
expect(output).toContain("测试说明");
|
||||
expect(output).toContain('bl test --message "你好"');
|
||||
expect(output).toContain(" # 流式输出响应");
|
||||
|
||||
@@ -53,11 +53,12 @@ test("high risk 命令不能自行声明 runtime 保留的 yes", () => {
|
||||
expect(() => new CommandRegistry({ "x normal": normal }, "bl")).not.toThrow();
|
||||
});
|
||||
|
||||
test("命令 help 只为 high risk 展示 runtime 注入的 --yes", () => {
|
||||
test("命令 help 只为 high risk 展示风险信息和 runtime 注入的 --yes", () => {
|
||||
const high = defineCommand({
|
||||
description: "danger",
|
||||
auth: "none",
|
||||
risk: { level: "high", message: "dangerous operation" },
|
||||
exampleArgs: ["--dry-run", "--yes"],
|
||||
run: noopRun,
|
||||
});
|
||||
const normal = defineCommand({
|
||||
@@ -77,5 +78,10 @@ test("命令 help 只为 high risk 展示 runtime 注入的 --yes", () => {
|
||||
} as unknown as NodeJS.WriteStream);
|
||||
|
||||
expect(highHelp).toContain("--yes");
|
||||
expect(highHelp).toContain("Risk: high");
|
||||
expect(highHelp).toContain("Risk message: dangerous operation");
|
||||
expect(highHelp).toMatch(/# Only after explicit confirmation:\n\s+bl asset delete --yes/);
|
||||
expect(normalHelp).not.toContain("--yes");
|
||||
expect(normalHelp).not.toContain("Risk:");
|
||||
expect(normalHelp).not.toContain("Risk message:");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user