mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
feat(runtime): support colocated localized CLI help text
This commit is contained in:
@@ -11,7 +11,7 @@ export default defineCommand({
|
||||
type: "string",
|
||||
valueHint: "<key>",
|
||||
description:
|
||||
"Config key (base_url, output, output_dir, timeout, api_key, access_token, access_key_id, access_key_secret, security_token, default_*_model, workspace_id)",
|
||||
"Config key (language, base_url, output, output_dir, timeout, api_key, access_token, access_key_id, access_key_secret, security_token, default_*_model, workspace_id)",
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
@@ -22,6 +22,7 @@ export default defineCommand({
|
||||
},
|
||||
},
|
||||
exampleArgs: [
|
||||
"--key language --value zh-CN",
|
||||
"--key output --value json",
|
||||
"--key timeout --value 600",
|
||||
"--key base_url --value https://dashscope.aliyuncs.com",
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { BailianError, ExitCode, normalizeModelBaseUrl } from "bailian-cli-core";
|
||||
import {
|
||||
BailianError,
|
||||
ExitCode,
|
||||
normalizeModelBaseUrl,
|
||||
SUPPORTED_LANGUAGES,
|
||||
} from "bailian-cli-core";
|
||||
|
||||
/** Config keys that `config set` / `config ui` accept for read/write. */
|
||||
export const VALID_KEYS = [
|
||||
"language",
|
||||
"base_url",
|
||||
"output",
|
||||
"output_dir",
|
||||
@@ -47,6 +53,7 @@ export const UI_VALID_KEYS = [...VALID_KEYS, ...UI_EXTRA_KEYS] as const;
|
||||
|
||||
// Keys the UI renders as a fixed-choice dropdown instead of a free-text input.
|
||||
export const UI_ENUM_KEYS: Record<string, string[]> = {
|
||||
language: [...SUPPORTED_LANGUAGES],
|
||||
output: ["text", "json"],
|
||||
console_site: ["domestic", "international"],
|
||||
};
|
||||
@@ -144,6 +151,13 @@ export function validateAndCoerce(key: string, value: string): string | number {
|
||||
);
|
||||
}
|
||||
|
||||
if (resolvedKey === "language" && !(SUPPORTED_LANGUAGES as readonly string[]).includes(value)) {
|
||||
throw new BailianError(
|
||||
`Invalid language "${value}". Valid values: ${SUPPORTED_LANGUAGES.join(", ")}`,
|
||||
ExitCode.USAGE,
|
||||
);
|
||||
}
|
||||
|
||||
if (resolvedKey === "output" && !["text", "json"].includes(value)) {
|
||||
throw new BailianError(
|
||||
`Invalid output format "${value}". Valid values: text, json`,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineCommand, detectOutputFormat, maskToken } from "bailian-cli-core";
|
||||
import { DEFAULT_LANGUAGE, defineCommand, detectOutputFormat, maskToken } from "bailian-cli-core";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
import { SECRET_KEYS } from "./shared.ts";
|
||||
|
||||
@@ -14,6 +14,7 @@ export default defineCommand({
|
||||
|
||||
const result: Record<string, unknown> = {
|
||||
...file,
|
||||
language: file.language ?? DEFAULT_LANGUAGE,
|
||||
base_url: client.baseUrl,
|
||||
output: settings.output,
|
||||
timeout: settings.timeout,
|
||||
|
||||
@@ -189,6 +189,19 @@ describe("e2e: config", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("config set language 拒绝不支持的值", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [
|
||||
"config",
|
||||
"set",
|
||||
"--key",
|
||||
"language",
|
||||
"--value",
|
||||
"fr-FR",
|
||||
]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/Invalid language|en-US, zh-CN/i);
|
||||
});
|
||||
|
||||
test("config set 非法 key 时退出为用法错误", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [
|
||||
"config",
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
export type { ConfigFile, Region, Identity, Settings } from "./schema.ts";
|
||||
export { BAILIAN_HOST, CONFIG_FILE_KEYS, DOCS_HOSTS, REGIONS, parseConfigFile } from "./schema.ts";
|
||||
export type { ConfigFile, Language, Region, Identity, Settings } from "./schema.ts";
|
||||
export {
|
||||
BAILIAN_HOST,
|
||||
CONFIG_FILE_KEYS,
|
||||
DEFAULT_LANGUAGE,
|
||||
DOCS_HOSTS,
|
||||
REGIONS,
|
||||
SUPPORTED_LANGUAGES,
|
||||
parseConfigFile,
|
||||
} from "./schema.ts";
|
||||
export { normalizeConfigName, readConfigFile, writeConfigFile } from "./loader.ts";
|
||||
export {
|
||||
activateConfigProfile,
|
||||
|
||||
@@ -16,7 +16,12 @@ export const BAILIAN_HOST = "https://bailian.cn-beijing.aliyuncs.com";
|
||||
|
||||
export type Region = keyof typeof REGIONS;
|
||||
|
||||
export const SUPPORTED_LANGUAGES = ["en-US", "zh-CN"] as const;
|
||||
export type Language = (typeof SUPPORTED_LANGUAGES)[number];
|
||||
export const DEFAULT_LANGUAGE: Language = "en-US";
|
||||
|
||||
export interface ConfigFile {
|
||||
language?: Language;
|
||||
api_key?: string;
|
||||
/** OAuth-style token from `bl auth login --console` callback; sent as `Authorization: Bearer …` */
|
||||
access_token?: string;
|
||||
@@ -45,6 +50,7 @@ export interface ConfigFile {
|
||||
}
|
||||
|
||||
export const CONFIG_FILE_KEYS = [
|
||||
"language",
|
||||
"api_key",
|
||||
"access_token",
|
||||
"access_key_id",
|
||||
@@ -90,6 +96,11 @@ export function parseConfigFile(raw: unknown): ConfigFile {
|
||||
const obj = raw as Record<string, unknown>;
|
||||
const out: ConfigFile = {};
|
||||
|
||||
if (
|
||||
typeof obj.language === "string" &&
|
||||
(SUPPORTED_LANGUAGES as readonly string[]).includes(obj.language)
|
||||
)
|
||||
out.language = obj.language as Language;
|
||||
if (typeof obj.api_key === "string") out.api_key = obj.api_key;
|
||||
if (typeof obj.access_token === "string" && obj.access_token.length > 0)
|
||||
out.access_token = obj.access_token;
|
||||
|
||||
@@ -4,6 +4,8 @@ import type { AuthStore } from "../auth/store.ts";
|
||||
import type { Client } from "../client/client.ts";
|
||||
import type { CommandPackManager } from "./command-pack-manager.ts";
|
||||
|
||||
export type LocalizedText = string | { readonly "en-US": string; readonly "zh-CN": string };
|
||||
|
||||
// ── Flag definitions ─────────────────────────────────────────────────────────
|
||||
// Flags are keyed by camelCase name (the key IS the parsed flag name, e.g.
|
||||
// `maxTokens` ↔ `--max-tokens`). The flag's type drives both runtime parsing
|
||||
@@ -12,12 +14,12 @@ import type { CommandPackManager } from "./command-pack-manager.ts";
|
||||
/** A presence flag: `--quiet`. No value; absent → false. */
|
||||
export interface SwitchFlag {
|
||||
type: "switch";
|
||||
description: string;
|
||||
description: LocalizedText;
|
||||
}
|
||||
/** A value flag: `--prompt <text>`, `--n <count>`, `--watermark true|false`. */
|
||||
export interface ValueFlag {
|
||||
type: "string" | "number" | "boolean" | "array";
|
||||
description: string;
|
||||
description: LocalizedText;
|
||||
valueHint: string;
|
||||
required?: boolean;
|
||||
/**
|
||||
@@ -70,26 +72,44 @@ export const GLOBAL_FLAGS = {
|
||||
output: {
|
||||
type: "string",
|
||||
valueHint: "<format>",
|
||||
description: "Output format: text, json",
|
||||
description: { "en-US": "Output format: text, json", "zh-CN": "输出格式:text、json" },
|
||||
},
|
||||
timeout: {
|
||||
type: "number",
|
||||
valueHint: "<seconds>",
|
||||
description: "Request timeout",
|
||||
description: { "en-US": "Request timeout", "zh-CN": "请求超时时间" },
|
||||
},
|
||||
quiet: {
|
||||
type: "switch",
|
||||
description: { "en-US": "Suppress non-essential output", "zh-CN": "隐藏非必要输出" },
|
||||
},
|
||||
quiet: { type: "switch", description: "Suppress non-essential output" },
|
||||
verbose: {
|
||||
type: "switch",
|
||||
description: "Print HTTP request/response details",
|
||||
description: {
|
||||
"en-US": "Print HTTP request/response details",
|
||||
"zh-CN": "打印 HTTP 请求和响应详情",
|
||||
},
|
||||
},
|
||||
dryRun: {
|
||||
type: "switch",
|
||||
description: { "en-US": "Dry run mode", "zh-CN": "仅预览,不实际执行" },
|
||||
},
|
||||
dryRun: { type: "switch", description: "Dry run mode" },
|
||||
config: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: "Use a config profile for this command",
|
||||
description: {
|
||||
"en-US": "Use a config profile for this command",
|
||||
"zh-CN": "为当前命令使用指定配置 Profile",
|
||||
},
|
||||
},
|
||||
help: {
|
||||
type: "switch",
|
||||
description: { "en-US": "Show help", "zh-CN": "显示帮助信息" },
|
||||
},
|
||||
version: {
|
||||
type: "switch",
|
||||
description: { "en-US": "Print version", "zh-CN": "显示版本信息" },
|
||||
},
|
||||
help: { type: "switch", description: "Show help" },
|
||||
version: { type: "switch", description: "Print version" },
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/** Command-scoped flag for commands that support parallel API calls. */
|
||||
@@ -111,8 +131,16 @@ export const ASYNC_FLAG = {
|
||||
|
||||
/** Model 域凭证/连接 flag,`auth: "apiKey"` 命令可见。 */
|
||||
export const MODEL_AUTH_FLAGS = {
|
||||
apiKey: { type: "string", valueHint: "<key>", description: "API key" },
|
||||
baseUrl: { type: "string", valueHint: "<url>", description: "API base URL" },
|
||||
apiKey: {
|
||||
type: "string",
|
||||
valueHint: "<key>",
|
||||
description: { "en-US": "API key", "zh-CN": "API Key" },
|
||||
},
|
||||
baseUrl: {
|
||||
type: "string",
|
||||
valueHint: "<url>",
|
||||
description: { "en-US": "API base URL", "zh-CN": "API Base URL" },
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/** Console 域目标/作用域 flag,`auth: "console"` 命令可见。 */
|
||||
@@ -120,22 +148,34 @@ export const CONSOLE_AUTH_FLAGS = {
|
||||
consoleRegion: {
|
||||
type: "string",
|
||||
valueHint: "<region>",
|
||||
description: "Console gateway region (e.g. cn-beijing, ap-southeast-1)",
|
||||
description: {
|
||||
"en-US": "Console gateway region (e.g. cn-beijing, ap-southeast-1)",
|
||||
"zh-CN": "控制台网关地域(例如 cn-beijing、ap-southeast-1)",
|
||||
},
|
||||
},
|
||||
consoleSite: {
|
||||
type: "string",
|
||||
valueHint: "<site>",
|
||||
description: "Console site: domestic, international",
|
||||
description: {
|
||||
"en-US": "Console site: domestic, international",
|
||||
"zh-CN": "控制台站点:domestic、international",
|
||||
},
|
||||
},
|
||||
consoleSwitchAgent: {
|
||||
type: "number",
|
||||
valueHint: "<uid>",
|
||||
description: "Switch agent UID for delegated access",
|
||||
description: {
|
||||
"en-US": "Switch agent UID for delegated access",
|
||||
"zh-CN": "切换代理访问的 UID",
|
||||
},
|
||||
},
|
||||
workspaceId: {
|
||||
type: "string",
|
||||
valueHint: "<id>",
|
||||
description: "Workspace ID (env: BAILIAN_WORKSPACE_ID)",
|
||||
description: {
|
||||
"en-US": "Workspace ID (env: BAILIAN_WORKSPACE_ID)",
|
||||
"zh-CN": "Workspace ID(环境变量:BAILIAN_WORKSPACE_ID)",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
@@ -144,17 +184,26 @@ export const OPENAPI_AUTH_FLAGS = {
|
||||
accessKeyId: {
|
||||
type: "string",
|
||||
valueHint: "<key>",
|
||||
description: "Alibaba Cloud Access Key ID (env: ALIBABA_CLOUD_ACCESS_KEY_ID)",
|
||||
description: {
|
||||
"en-US": "Alibaba Cloud Access Key ID (env: ALIBABA_CLOUD_ACCESS_KEY_ID)",
|
||||
"zh-CN": "阿里云 Access Key ID(环境变量:ALIBABA_CLOUD_ACCESS_KEY_ID)",
|
||||
},
|
||||
},
|
||||
accessKeySecret: {
|
||||
type: "string",
|
||||
valueHint: "<key>",
|
||||
description: "Alibaba Cloud Access Key Secret (env: ALIBABA_CLOUD_ACCESS_KEY_SECRET)",
|
||||
description: {
|
||||
"en-US": "Alibaba Cloud Access Key Secret (env: ALIBABA_CLOUD_ACCESS_KEY_SECRET)",
|
||||
"zh-CN": "阿里云 Access Key Secret(环境变量:ALIBABA_CLOUD_ACCESS_KEY_SECRET)",
|
||||
},
|
||||
},
|
||||
securityToken: {
|
||||
type: "string",
|
||||
valueHint: "<token>",
|
||||
description: "Alibaba Cloud STS Security Token (env: ALIBABA_CLOUD_SECURITY_TOKEN)",
|
||||
description: {
|
||||
"en-US": "Alibaba Cloud STS Security Token (env: ALIBABA_CLOUD_SECURITY_TOKEN)",
|
||||
"zh-CN": "阿里云 STS Security Token(环境变量:ALIBABA_CLOUD_SECURITY_TOKEN)",
|
||||
},
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
@@ -204,7 +253,7 @@ export interface CommandContext<F extends FlagsDef = FlagsDef> {
|
||||
* {@link AnyCommand}; the precise typing lives at the `defineCommand` call site.
|
||||
*/
|
||||
export interface Command<F extends FlagsDef = FlagsDef> {
|
||||
description: string;
|
||||
description: LocalizedText;
|
||||
/** Credential this command requires. See {@link AuthRequirement}. */
|
||||
auth: AuthRequirement;
|
||||
/** Usage line arg portion, e.g. "--prompt <text> [flags]". Manually written. */
|
||||
|
||||
@@ -2,6 +2,7 @@ export type {
|
||||
Command,
|
||||
AnyCommand,
|
||||
CommandContext,
|
||||
LocalizedText,
|
||||
FlagDef,
|
||||
FlagsDef,
|
||||
ParsedFlags,
|
||||
|
||||
@@ -311,6 +311,11 @@ test("parseConfigFile ignores obsolete region field", () => {
|
||||
expect("region" in f).toBe(false);
|
||||
});
|
||||
|
||||
test("parseConfigFile accepts only supported languages", () => {
|
||||
expect(parseConfigFile({ language: "zh-CN" }).language).toBe("zh-CN");
|
||||
expect(parseConfigFile({ language: "fr-FR" }).language).toBeUndefined();
|
||||
});
|
||||
|
||||
test("parseConfigFile accepts only well-formed http(s) base_url", () => {
|
||||
expect(parseConfigFile({ base_url: "https://dashscope.aliyuncs.com" }).base_url).toBe(
|
||||
"https://dashscope.aliyuncs.com",
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
"bailian-cli-core": "workspace:*",
|
||||
"boxen": "catalog:",
|
||||
"chalk": "catalog:",
|
||||
"i18next": "catalog:",
|
||||
"undici": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import type { AnyCommand, FlagsDef, Identity, ParsedFlags, SourceFlags } from "bailian-cli-core";
|
||||
import {
|
||||
CONSOLE_AUTH_FLAGS,
|
||||
DEFAULT_LANGUAGE,
|
||||
GLOBAL_FLAGS,
|
||||
MODEL_AUTH_FLAGS,
|
||||
OPENAPI_AUTH_FLAGS,
|
||||
@@ -28,10 +29,11 @@ import {
|
||||
} from "bailian-cli-core";
|
||||
import { setupProxyFromEnv } from "./proxy.ts";
|
||||
import { handleError } from "./error-handler.ts";
|
||||
import { printWelcomeBanner, printQuickStart } from "./output/banner.ts";
|
||||
import { printQuickStart } from "./output/banner.ts";
|
||||
import { loadCommandPacks } from "./command-packs/load.ts";
|
||||
import { createCommandPackManager } from "./command-packs/manager.ts";
|
||||
import type { CommandPackPolicy } from "./command-packs/types.ts";
|
||||
import { createTranslator } from "./i18n.ts";
|
||||
|
||||
/** Per-product identity injected by each CLI entrypoint (bl / rag / …). */
|
||||
export interface CliOptions {
|
||||
@@ -60,6 +62,20 @@ function pick(obj: Record<string, unknown>, keys: string[]): Record<string, unkn
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Read only the existing `--config` flag so locale follows the selected Profile before dispatch. */
|
||||
function pickConfigFlag(argv: string[]): Partial<SourceFlags> {
|
||||
for (let argumentIndex = 0; argumentIndex < argv.length; argumentIndex++) {
|
||||
const argument = argv[argumentIndex];
|
||||
if (argument === "--config") {
|
||||
return { config: argv[argumentIndex + 1] };
|
||||
}
|
||||
if (argument?.startsWith("--config=")) {
|
||||
return { config: argument.slice("--config=".length) };
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* 进程级一次性设置:代理初始化、Ctrl+C、stdout EPIPE。
|
||||
* 属进程生命周期行为,装一次即可,不进 per-command 中间件。
|
||||
@@ -94,19 +110,26 @@ export function createCli(commands: Record<string, AnyCommand>, opts: CliOptions
|
||||
const identity: Identity = { binName, version, npmPackage, clientName };
|
||||
const commandPackPolicy = opts.commandPacks ?? { supported: {} };
|
||||
const commandPackManager = createCommandPackManager(identity, commandPackPolicy);
|
||||
let registryPromise: Promise<CommandRegistry> | undefined;
|
||||
let loadedCommandPacksPromise: ReturnType<typeof loadCommandPacks> | undefined;
|
||||
|
||||
installProcessHandlers(binName);
|
||||
|
||||
const runMiddleware = compose([versionCheckStage, telemetryStage, authStage, runCommandStage]);
|
||||
|
||||
function getRegistry(): Promise<CommandRegistry> {
|
||||
if (!registryPromise) {
|
||||
registryPromise = loadCommandPacks(commands, identity, commandPackPolicy).then(
|
||||
(loaded) => new CommandRegistry(loaded.commands, binName),
|
||||
);
|
||||
function getLoadedCommandPacks(): ReturnType<typeof loadCommandPacks> {
|
||||
if (!loadedCommandPacksPromise) {
|
||||
loadedCommandPacksPromise = loadCommandPacks(commands, identity, commandPackPolicy);
|
||||
}
|
||||
return registryPromise;
|
||||
return loadedCommandPacksPromise;
|
||||
}
|
||||
|
||||
async function getRegistry(argv: string[]): Promise<CommandRegistry> {
|
||||
const localeSources = buildSources(pickConfigFlag(argv));
|
||||
const [translator, loaded] = await Promise.all([
|
||||
createTranslator(localeSources.file.language ?? DEFAULT_LANGUAGE),
|
||||
getLoadedCommandPacks(),
|
||||
]);
|
||||
return new CommandRegistry(loaded.commands, binName, translator);
|
||||
}
|
||||
|
||||
/** Render help for `path`; root ([]) doubles as the onboarding / login guide. */
|
||||
@@ -133,7 +156,7 @@ export function createCli(commands: Record<string, AnyCommand>, opts: CliOptions
|
||||
if (hasKey) {
|
||||
if (opts.quickStartTasks?.length) printQuickStart(opts.quickStartTasks);
|
||||
} else {
|
||||
printWelcomeBanner(binName);
|
||||
registry.printWelcome();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +230,8 @@ export function createCli(commands: Record<string, AnyCommand>, opts: CliOptions
|
||||
|
||||
return {
|
||||
run(argv: string[] = process.argv.slice(2)) {
|
||||
return getRegistry()
|
||||
return Promise.resolve()
|
||||
.then(() => getRegistry(argv))
|
||||
.then((registry) => dispatch(registry, argv))
|
||||
.catch(
|
||||
(err) => flushTelemetry(1000).finally(() => handleError(err, binName)) as unknown as void,
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { createInstance, type ResourceLanguage, type TOptions } from "i18next";
|
||||
import {
|
||||
DEFAULT_LANGUAGE,
|
||||
SUPPORTED_LANGUAGES,
|
||||
type Language,
|
||||
type LocalizedText,
|
||||
} from "bailian-cli-core";
|
||||
|
||||
export type Translate = (key: string, options?: Record<string, unknown>) => string;
|
||||
|
||||
/** A colocated namespace that a product or feature can register with the CLI runtime. */
|
||||
export interface CliMessageBundle {
|
||||
namespace: string;
|
||||
resources: Partial<Record<Language, ResourceLanguage>>;
|
||||
}
|
||||
|
||||
/** Per-CLI translation surface. The mutable i18next instance stays private to runtime. */
|
||||
export interface Translator {
|
||||
language: Language;
|
||||
translate: Translate;
|
||||
localize(text: LocalizedText): string;
|
||||
}
|
||||
|
||||
export async function createTranslator(
|
||||
language: Language,
|
||||
bundles: readonly CliMessageBundle[] = [],
|
||||
): Promise<Translator> {
|
||||
const instance = createInstance();
|
||||
|
||||
await instance.init({
|
||||
lng: language,
|
||||
fallbackLng: DEFAULT_LANGUAGE,
|
||||
supportedLngs: [...SUPPORTED_LANGUAGES],
|
||||
initAsync: false,
|
||||
interpolation: { escapeValue: false },
|
||||
});
|
||||
|
||||
for (const bundle of bundles) {
|
||||
for (const supportedLanguage of SUPPORTED_LANGUAGES) {
|
||||
const resource = bundle.resources[supportedLanguage];
|
||||
if (resource) {
|
||||
instance.addResourceBundle(supportedLanguage, bundle.namespace, resource, true, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
language,
|
||||
localize(text) {
|
||||
return typeof text === "string" ? text : text[language];
|
||||
},
|
||||
translate(key, options = {}) {
|
||||
return String(instance.t(key, options as TOptions));
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,16 +1,38 @@
|
||||
import { API_KEY_PAGE, TOKEN_PLAN_PAGE } from "../urls.ts";
|
||||
import type { LocalizedText } from "bailian-cli-core";
|
||||
import type { Translator } from "../i18n.ts";
|
||||
import { ansi } from "./color.ts";
|
||||
|
||||
export function printWelcomeBanner(cliName: string): void {
|
||||
const WELCOME_TEXT = {
|
||||
title: { "en-US": "Welcome to Bailian CLI!", "zh-CN": "欢迎使用 Bailian CLI!" },
|
||||
getStarted: { "en-US": "Get started in 2 steps:", "zh-CN": "只需两步即可开始使用:" },
|
||||
getApiKey: { "en-US": "Get your API Key:", "zh-CN": "获取 API Key:" },
|
||||
login: { "en-US": "Login:", "zh-CN": "登录:" },
|
||||
tokenPlan: { "en-US": "Token Plan:", "zh-CN": "Token Plan:" },
|
||||
} satisfies Record<string, LocalizedText>;
|
||||
|
||||
function localize(translator: Translator | undefined, text: LocalizedText): string {
|
||||
return translator?.localize(text) ?? (typeof text === "string" ? text : text["en-US"]);
|
||||
}
|
||||
|
||||
export function printWelcomeBanner(cliName: string, translator?: Translator): void {
|
||||
const color = ansi(process.stderr);
|
||||
process.stderr.write(`\n Welcome to ${color.purple("Bailian")} CLI!\n\n`);
|
||||
process.stderr.write(" Get started in 2 steps:\n");
|
||||
process.stderr.write(` 1. Get your API Key: ${API_KEY_PAGE}\n`);
|
||||
process.stderr.write(` 2. Login: ${cliName} auth login --api-key <your-key>\n\n`);
|
||||
process.stderr.write(" Token Plan:\n");
|
||||
process.stderr.write(` 1. Get your API Key: ${TOKEN_PLAN_PAGE}\n`);
|
||||
const title = localize(translator, WELCOME_TEXT.title).replace(
|
||||
"Bailian",
|
||||
color.purple("Bailian"),
|
||||
);
|
||||
process.stderr.write(`\n ${title}\n\n`);
|
||||
process.stderr.write(` ${localize(translator, WELCOME_TEXT.getStarted)}\n`);
|
||||
process.stderr.write(` 1. ${localize(translator, WELCOME_TEXT.getApiKey)} ${API_KEY_PAGE}\n`);
|
||||
process.stderr.write(
|
||||
` 2. Login: ${cliName} auth login --config token-plan --api-key <your-key>\n\n`,
|
||||
` 2. ${localize(translator, WELCOME_TEXT.login)} ${cliName} auth login --api-key <your-key>\n\n`,
|
||||
);
|
||||
process.stderr.write(` ${localize(translator, WELCOME_TEXT.tokenPlan)}\n`);
|
||||
process.stderr.write(
|
||||
` 1. ${localize(translator, WELCOME_TEXT.getApiKey)} ${TOKEN_PLAN_PAGE}\n`,
|
||||
);
|
||||
process.stderr.write(
|
||||
` 2. ${localize(translator, WELCOME_TEXT.login)} ${cliName} auth login --config token-plan --api-key <your-key>\n\n`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import type { AnyCommand, AuthRequirement, FlagDef, FlagsDef } from "bailian-cli-core";
|
||||
import type {
|
||||
AnyCommand,
|
||||
AuthRequirement,
|
||||
FlagDef,
|
||||
FlagsDef,
|
||||
LocalizedText,
|
||||
} from "bailian-cli-core";
|
||||
import { UsageError } from "bailian-cli-core";
|
||||
import {
|
||||
CONSOLE_AUTH_FLAGS,
|
||||
@@ -8,6 +14,8 @@ import {
|
||||
credentialFlagDefs,
|
||||
} from "bailian-cli-core";
|
||||
import { camelToKebab } from "./args.ts";
|
||||
import type { Translator } from "./i18n.ts";
|
||||
import { printWelcomeBanner } from "./output/banner.ts";
|
||||
import { ansi } from "./output/color.ts";
|
||||
|
||||
export type { Command, AnyCommand, FlagDef, FlagsDef } from "bailian-cli-core";
|
||||
@@ -25,6 +33,32 @@ interface CommandNode {
|
||||
children: Map<string, CommandNode>;
|
||||
}
|
||||
|
||||
const HELP_TEXT = {
|
||||
usage: { "en-US": "Usage:", "zh-CN": "用法:" },
|
||||
commands: { "en-US": "Commands:", "zh-CN": "命令:" },
|
||||
flags: { "en-US": "Flags:", "zh-CN": "选项:" },
|
||||
globalFlags: { "en-US": "Global Flags:", "zh-CN": "全局选项:" },
|
||||
modelAuthFlags: { "en-US": "Model Auth Flags:", "zh-CN": "模型鉴权选项:" },
|
||||
modelAuthScope: { "en-US": "(model-domain commands)", "zh-CN": "(模型域命令)" },
|
||||
consoleAuthFlags: { "en-US": "Console Auth Flags:", "zh-CN": "控制台鉴权选项:" },
|
||||
consoleAuthScope: { "en-US": "(console-domain commands)", "zh-CN": "(控制台域命令)" },
|
||||
openApiAuthFlags: { "en-US": "OpenAPI Auth Flags:", "zh-CN": "OpenAPI 鉴权选项:" },
|
||||
openApiAuthScope: { "en-US": "(openapi-domain commands)", "zh-CN": "(OpenAPI 域命令)" },
|
||||
gettingHelp: { "en-US": "Getting Help:", "zh-CN": "获取帮助:" },
|
||||
gettingHelpDescription1: {
|
||||
"en-US": "Add --help after any command to see its full list of flags, defaults,",
|
||||
"zh-CN": "在任意命令后添加 --help,查看完整的选项和默认值,",
|
||||
},
|
||||
gettingHelpDescription2: {
|
||||
"en-US": "and usage examples. For example:",
|
||||
"zh-CN": "以及用法示例。例如:",
|
||||
},
|
||||
notes: { "en-US": "Notes:", "zh-CN": "说明:" },
|
||||
examples: { "en-US": "Examples:", "zh-CN": "示例:" },
|
||||
minimalWorkflow: { "en-US": "Minimal workflow.yaml:", "zh-CN": "最小 workflow.yaml:" },
|
||||
tryIt: { "en-US": "Try it:", "zh-CN": "试一试:" },
|
||||
} satisfies Record<string, LocalizedText>;
|
||||
|
||||
/**
|
||||
* What a command path resolves to in the registry. The single judgement that
|
||||
* feeds `resolve()` — no scattered `isGroupPath` + throwing `resolve`.
|
||||
@@ -42,15 +76,21 @@ export class CommandRegistry {
|
||||
private root: CommandNode = { children: new Map() };
|
||||
/** Binary name shown in usage/help/error strings (e.g. "bl", "rag"). */
|
||||
private readonly cliName: string;
|
||||
private readonly translator?: Translator;
|
||||
private readonly authRequirements = new Set<AuthRequirement>();
|
||||
|
||||
constructor(commands: Record<string, AnyCommand>, cliName: string) {
|
||||
constructor(commands: Record<string, AnyCommand>, cliName: string, translator?: Translator) {
|
||||
this.cliName = cliName;
|
||||
this.translator = translator;
|
||||
for (const [path, cmd] of Object.entries(commands)) {
|
||||
this.register(path, cmd);
|
||||
}
|
||||
}
|
||||
|
||||
private localize(text: LocalizedText): string {
|
||||
return this.translator?.localize(text) ?? (typeof text === "string" ? text : text["en-US"]);
|
||||
}
|
||||
|
||||
private register(path: string, command: AnyCommand): void {
|
||||
// 同名守卫:命令自有 flag 不得与全局或其可见凭证域 flag 同名。
|
||||
const reserved = { ...GLOBAL_FLAGS, ...credentialFlagDefs(command) };
|
||||
@@ -134,7 +174,8 @@ export class CommandRegistry {
|
||||
if (matched.length > 0 && node.children.size > 0) {
|
||||
const subcommands = Array.from(node.children.entries())
|
||||
.map(([name, n]) => {
|
||||
if (n.command) return ` ${matched.join(" ")} ${name} ${n.command.description}`;
|
||||
if (n.command)
|
||||
return ` ${matched.join(" ")} ${name} ${this.localize(n.command.description)}`;
|
||||
const subs = Array.from(n.children.keys()).join(", ");
|
||||
return ` ${matched.join(" ")} ${name} [${subs}]`;
|
||||
})
|
||||
@@ -164,7 +205,7 @@ export class CommandRegistry {
|
||||
for (const [name, child] of node.children) {
|
||||
const fullPath = prefix ? `${prefix} ${name}` : name;
|
||||
if (child.command) {
|
||||
entries.push({ path: fullPath, desc: child.command.description });
|
||||
entries.push({ path: fullPath, desc: this.localize(child.command.description) });
|
||||
}
|
||||
if (child.children.size > 0) {
|
||||
collect(child, fullPath);
|
||||
@@ -184,7 +225,7 @@ export class CommandRegistry {
|
||||
): string {
|
||||
const lines = Object.entries(defs).map(([k, def]) => ({
|
||||
flag: flagDisplay(k, def),
|
||||
desc: def.description,
|
||||
desc: this.localize(def.description),
|
||||
}));
|
||||
const maxLen = Math.max(...lines.map((l) => l.flag.length));
|
||||
return lines.map((l) => ` ${a(l.flag.padEnd(maxLen + 2))} ${d(l.desc)}`).join("\n");
|
||||
@@ -231,8 +272,10 @@ export class CommandRegistry {
|
||||
|
||||
// Group help (e.g. `bl auth --help`)
|
||||
const prefix = commandPath.join(" ");
|
||||
out.write(`\n${this.bold("Usage:", out)} ${this.cliName} ${prefix} <command> [flags]\n\n`);
|
||||
out.write(`${this.bold("Commands:", out)}\n`);
|
||||
out.write(
|
||||
`\n${this.bold(this.localize(HELP_TEXT.usage), out)} ${this.cliName} ${prefix} <command> [flags]\n\n`,
|
||||
);
|
||||
out.write(`${this.bold(this.localize(HELP_TEXT.commands), out)}\n`);
|
||||
this.printChildren(node, prefix, out);
|
||||
if (prefix === "pipeline") {
|
||||
this.printPipelineQuickStart(out);
|
||||
@@ -240,12 +283,16 @@ export class CommandRegistry {
|
||||
out.write("\n");
|
||||
}
|
||||
|
||||
printWelcome(): void {
|
||||
printWelcomeBanner(this.cliName, this.translator);
|
||||
}
|
||||
|
||||
private printPipelineQuickStart(out: NodeJS.WriteStream): void {
|
||||
const b = (s: string) => this.bold(s, out);
|
||||
const d = (s: string) => this.dim(s, out);
|
||||
|
||||
out.write(`
|
||||
${b("Minimal workflow.yaml:")}
|
||||
${b(this.localize(HELP_TEXT.minimalWorkflow))}
|
||||
${d(" version: workflow/v1")}
|
||||
${d(" steps:")}
|
||||
${d(" - id: chat")}
|
||||
@@ -254,7 +301,7 @@ ${d(" input:")}
|
||||
${d(' message: "Who are you?"')}
|
||||
${d(' system: "You are a concise assistant."')}
|
||||
|
||||
${b("Try it:")}
|
||||
${b(this.localize(HELP_TEXT.tryIt))}
|
||||
${d(` ${this.cliName} pipeline validate workflow.yaml`)}
|
||||
${d(` ${this.cliName} pipeline run workflow.yaml --dry-run --output json`)}
|
||||
`);
|
||||
@@ -286,8 +333,8 @@ ${d(` ${this.cliName} pipeline run workflow.yaml --dry-run --output json`)}
|
||||
const authFlagSections = [
|
||||
this.buildAuthFlagSection(
|
||||
"apiKey",
|
||||
"Model Auth Flags:",
|
||||
"(model-domain commands)",
|
||||
this.localize(HELP_TEXT.modelAuthFlags),
|
||||
this.localize(HELP_TEXT.modelAuthScope),
|
||||
MODEL_AUTH_FLAGS,
|
||||
b,
|
||||
a,
|
||||
@@ -295,8 +342,8 @@ ${d(` ${this.cliName} pipeline run workflow.yaml --dry-run --output json`)}
|
||||
),
|
||||
this.buildAuthFlagSection(
|
||||
"console",
|
||||
"Console Auth Flags:",
|
||||
"(console-domain commands)",
|
||||
this.localize(HELP_TEXT.consoleAuthFlags),
|
||||
this.localize(HELP_TEXT.consoleAuthScope),
|
||||
CONSOLE_AUTH_FLAGS,
|
||||
b,
|
||||
a,
|
||||
@@ -304,8 +351,8 @@ ${d(` ${this.cliName} pipeline run workflow.yaml --dry-run --output json`)}
|
||||
),
|
||||
this.buildAuthFlagSection(
|
||||
"openapi",
|
||||
"OpenAPI Auth Flags:",
|
||||
"(openapi-domain commands)",
|
||||
this.localize(HELP_TEXT.openApiAuthFlags),
|
||||
this.localize(HELP_TEXT.openApiAuthScope),
|
||||
OPENAPI_AUTH_FLAGS,
|
||||
b,
|
||||
a,
|
||||
@@ -316,17 +363,17 @@ ${d(` ${this.cliName} pipeline run workflow.yaml --dry-run --output json`)}
|
||||
.join("\n\n");
|
||||
|
||||
out.write(`
|
||||
${b("Usage:")} ${this.cliName} <resource> <command> [flags]
|
||||
${b(this.localize(HELP_TEXT.usage))} ${this.cliName} <resource> <command> [flags]
|
||||
|
||||
${b("Commands:")}
|
||||
${b(this.localize(HELP_TEXT.commands))}
|
||||
${commandLines}
|
||||
|
||||
${b("Global Flags:")}
|
||||
${b(this.localize(HELP_TEXT.globalFlags))}
|
||||
${globalFlagLines}
|
||||
|
||||
${authFlagSections ? `${authFlagSections}\n\n` : ""}${b("Getting Help:")}
|
||||
${d("Add --help after any command to see its full list of flags, defaults,")}
|
||||
${d("and usage examples. For example:")} ${this.cliName} ${this.helpExample()} --help
|
||||
${authFlagSections ? `${authFlagSections}\n\n` : ""}${b(this.localize(HELP_TEXT.gettingHelp))}
|
||||
${d(this.localize(HELP_TEXT.gettingHelpDescription1))}
|
||||
${d(this.localize(HELP_TEXT.gettingHelpDescription2))} ${this.cliName} ${this.helpExample()} --help
|
||||
`);
|
||||
}
|
||||
|
||||
@@ -339,33 +386,34 @@ ${authFlagSections ? `${authFlagSections}\n\n` : ""}${b("Getting Help:")}
|
||||
// same command shows the right invocation under any product (bl / rag / …).
|
||||
const prefix = [this.cliName, ...commandPath].join(" ");
|
||||
|
||||
out.write(`\n${cmd.description}\n`);
|
||||
out.write(`${b("Usage:")} ${prefix}${cmd.usageArgs ? ` ${cmd.usageArgs}` : ""}\n`);
|
||||
const flagEntries = [
|
||||
...Object.entries(cmd.flags ?? {}),
|
||||
...Object.entries(credentialFlagDefs(cmd)),
|
||||
] as [string, FlagDef][];
|
||||
out.write(`\n${this.localize(cmd.description)}\n`);
|
||||
out.write(
|
||||
`${b(this.localize(HELP_TEXT.usage))} ${prefix}${cmd.usageArgs ? ` ${cmd.usageArgs}` : ""}\n`,
|
||||
);
|
||||
const ownFlagEntries = Object.entries(cmd.flags ?? {}) as [string, FlagDef][];
|
||||
const credentialFlagEntries = Object.entries(credentialFlagDefs(cmd)) as [string, FlagDef][];
|
||||
const flagEntries = [...ownFlagEntries, ...credentialFlagEntries];
|
||||
if (flagEntries.length > 0) {
|
||||
const lines = flagEntries.map(([k, def]) => ({
|
||||
flag: flagDisplay(k, def),
|
||||
desc: def.description,
|
||||
const lines = flagEntries.map(([key, def]) => ({
|
||||
flag: flagDisplay(key, def),
|
||||
desc: this.localize(def.description),
|
||||
}));
|
||||
const maxLen = Math.max(...lines.map((l) => l.flag.length));
|
||||
out.write(`\n${b("Flags:")}\n`);
|
||||
out.write(`\n${b(this.localize(HELP_TEXT.flags))}\n`);
|
||||
for (const l of lines) {
|
||||
out.write(` ${a(l.flag.padEnd(maxLen + 2))} ${d(l.desc)}\n`);
|
||||
}
|
||||
}
|
||||
out.write(`\n${b("Global Flags:")}\n`);
|
||||
out.write(`\n${b(this.localize(HELP_TEXT.globalFlags))}\n`);
|
||||
out.write(this.buildFlagLines(GLOBAL_FLAGS, a, d) + "\n");
|
||||
if (cmd.notes && cmd.notes.length > 0) {
|
||||
out.write(`\n${b("Notes:")}\n`);
|
||||
out.write(`\n${b(this.localize(HELP_TEXT.notes))}\n`);
|
||||
for (const note of cmd.notes) {
|
||||
out.write(` ${note}\n`);
|
||||
}
|
||||
}
|
||||
if (cmd.exampleArgs && cmd.exampleArgs.length > 0) {
|
||||
out.write(`\n${b("Examples:")}\n`);
|
||||
out.write(`\n${b(this.localize(HELP_TEXT.examples))}\n`);
|
||||
for (const ex of cmd.exampleArgs) {
|
||||
out.write(` ${d(ex ? `${prefix} ${ex}` : prefix)}\n`);
|
||||
}
|
||||
@@ -377,7 +425,10 @@ ${authFlagSections ? `${authFlagSections}\n\n` : ""}${b("Getting Help:")}
|
||||
const collect = (n: CommandNode, p: string) => {
|
||||
for (const [name, child] of n.children) {
|
||||
if (child.command)
|
||||
entries.push({ fullName: `${p} ${name}`, description: child.command.description });
|
||||
entries.push({
|
||||
fullName: `${p} ${name}`,
|
||||
description: this.localize(child.command.description),
|
||||
});
|
||||
if (child.children.size > 0) collect(child, `${p} ${name}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { expect, test } from "vite-plus/test";
|
||||
import { defineCommand } from "bailian-cli-core";
|
||||
import { createTranslator, type CliMessageBundle } from "../src/i18n.ts";
|
||||
import { CommandRegistry } from "../src/registry.ts";
|
||||
|
||||
const messages = {
|
||||
namespace: "test",
|
||||
resources: {
|
||||
"en-US": { greeting: "Hello, {{name}}!", englishOnly: "English fallback" },
|
||||
"zh-CN": { greeting: "你好,{{name}}!" },
|
||||
},
|
||||
} satisfies CliMessageBundle;
|
||||
|
||||
test("translator uses the selected language, interpolation and English fallback", async () => {
|
||||
const translator = await createTranslator("zh-CN", [messages]);
|
||||
|
||||
expect(translator.language).toBe("zh-CN");
|
||||
expect(translator.translate("test:greeting", { name: "百炼" })).toBe("你好,百炼!");
|
||||
expect(translator.translate("test:englishOnly")).toBe("English fallback");
|
||||
});
|
||||
|
||||
test("translator resolves colocated text while preserving plain strings", async () => {
|
||||
const translator = await createTranslator("zh-CN");
|
||||
|
||||
expect(translator.localize("Already localized")).toBe("Already localized");
|
||||
expect(
|
||||
translator.localize({
|
||||
"en-US": "Show help",
|
||||
"zh-CN": "显示帮助信息",
|
||||
}),
|
||||
).toBe("显示帮助信息");
|
||||
});
|
||||
|
||||
test("registry renders runtime help copy with the selected language", async () => {
|
||||
const translator = await createTranslator("zh-CN");
|
||||
const command = defineCommand({
|
||||
description: {
|
||||
"en-US": "Test command",
|
||||
"zh-CN": "测试命令",
|
||||
},
|
||||
auth: "none",
|
||||
run: async () => {},
|
||||
});
|
||||
const registry = new CommandRegistry({ test: command }, "bl", translator);
|
||||
let output = "";
|
||||
const stream = {
|
||||
isTTY: false,
|
||||
write(chunk: string) {
|
||||
output += chunk;
|
||||
return true;
|
||||
},
|
||||
} as NodeJS.WriteStream;
|
||||
|
||||
registry.printHelp([], stream);
|
||||
|
||||
expect(output).toContain("用法: bl <resource> <command> [flags]");
|
||||
expect(output).toContain("测试命令");
|
||||
expect(output).toContain("全局选项:");
|
||||
expect(output).toContain("显示帮助信息");
|
||||
expect(output).toContain("获取帮助:");
|
||||
});
|
||||
Generated
+21
-3
@@ -24,12 +24,15 @@ catalogs:
|
||||
chalk:
|
||||
specifier: ^5.6.2
|
||||
version: 5.6.2
|
||||
tar-stream:
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.0
|
||||
i18next:
|
||||
specifier: ^26.3.6
|
||||
version: 26.3.6
|
||||
smol-toml:
|
||||
specifier: ^1.4.2
|
||||
version: 1.7.0
|
||||
tar-stream:
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.0
|
||||
tsx:
|
||||
specifier: ^4.23.0
|
||||
version: 4.23.0
|
||||
@@ -254,6 +257,9 @@ importers:
|
||||
chalk:
|
||||
specifier: 'catalog:'
|
||||
version: 5.6.2
|
||||
i18next:
|
||||
specifier: 'catalog:'
|
||||
version: 26.3.6(typescript@6.0.3)
|
||||
undici:
|
||||
specifier: 'catalog:'
|
||||
version: 6.27.0
|
||||
@@ -1200,6 +1206,14 @@ packages:
|
||||
resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
i18next@26.3.6:
|
||||
resolution: {integrity: sha512-Bu5Z2nAXgfVyM8xvW3jk9EKRIuX37PudsrBViThNFx7CR7aaYTpP01cxNB/E4c4UUzTDiAZRstEhsRfPOL/8xA==}
|
||||
peerDependencies:
|
||||
typescript: ^5 || ^6 || ^7
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
immediate@3.0.6:
|
||||
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
|
||||
|
||||
@@ -2250,6 +2264,10 @@ snapshots:
|
||||
|
||||
get-east-asian-width@1.6.0: {}
|
||||
|
||||
i18next@26.3.6(typescript@6.0.3):
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
immediate@3.0.6: {}
|
||||
|
||||
inherits@2.0.4: {}
|
||||
|
||||
@@ -9,6 +9,7 @@ catalog:
|
||||
ajv: ^8.20.0
|
||||
boxen: ^8.0.1
|
||||
chalk: ^5.6.2
|
||||
i18next: ^26.3.6
|
||||
tar-stream: ^3.2.0
|
||||
smol-toml: ^1.4.2
|
||||
tsx: ^4.23.0
|
||||
|
||||
@@ -85,13 +85,17 @@ bl config list --output json
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `--key <key>` | string | yes | Config key (base*url, output, output_dir, timeout, api_key, access_token, access_key_id, access_key_secret, security_token, default*\*\_model, workspace_id) |
|
||||
| `--value <value>` | string | yes | Value to set |
|
||||
| Flag | Type | Required | Description |
|
||||
| ----------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--key <key>` | string | yes | Config key (language, base*url, output, output_dir, timeout, api_key, access_token, access_key_id, access_key_secret, security_token, default*\*\_model, workspace_id) |
|
||||
| `--value <value>` | string | yes | Value to set |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl config set --key language --value zh-CN
|
||||
```
|
||||
|
||||
```bash
|
||||
bl config set --key output --value json
|
||||
```
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
type AnyCommand,
|
||||
type FlagDef,
|
||||
type FlagsDef,
|
||||
type LocalizedText,
|
||||
} from "../packages/core/src/index.ts";
|
||||
import { commands } from "../packages/cli/src/commands.ts";
|
||||
|
||||
@@ -62,6 +63,11 @@ function escCell(s: string): string {
|
||||
return s.replace(/\|/g, "\\|").replace(/\n/g, " ").trim();
|
||||
}
|
||||
|
||||
/** Skill references are English artifacts; localized CLI text keeps English here. */
|
||||
function referenceText(text: LocalizedText): string {
|
||||
return typeof text === "string" ? text : text["en-US"];
|
||||
}
|
||||
|
||||
function topLevel(path: string): string {
|
||||
return path.split(" ")[0]!;
|
||||
}
|
||||
@@ -88,7 +94,7 @@ function formatFlagsTable(flags: FlagsDef | undefined): string {
|
||||
if (!entries.length) return "_No command-specific flags._\n";
|
||||
const rows = entries.map(([key, def]) => {
|
||||
const req = def.type !== "switch" && def.required ? "yes" : "no";
|
||||
return `| \`${escCell(flagDisplay(key, def))}\` | ${escCell(flagType(def))} | ${req} | ${escCell(def.description)} |`;
|
||||
return `| \`${escCell(flagDisplay(key, def))}\` | ${escCell(flagType(def))} | ${req} | ${escCell(referenceText(def.description))} |`;
|
||||
});
|
||||
return [
|
||||
"| Flag | Type | Required | Description |",
|
||||
@@ -118,7 +124,7 @@ function commandSection(path: string, cmd: AnyCommand): string {
|
||||
lines.push(`### \`bl ${path}\``, "");
|
||||
lines.push(`| Field | Value |`, `| --- | --- |`);
|
||||
lines.push(`| **Name** | \`${escCell(path)}\` |`);
|
||||
lines.push(`| **Description** | ${escCell(cmd.description)} |`);
|
||||
lines.push(`| **Description** | ${escCell(referenceText(cmd.description))} |`);
|
||||
// Commands store argument-only usage; the `bl <path>` prefix is added here.
|
||||
const usage = `bl ${path}${cmd.usageArgs ? ` ${cmd.usageArgs}` : ""}`;
|
||||
lines.push(`| **Usage** | \`${escCell(usage)}\` |`);
|
||||
@@ -172,7 +178,7 @@ function buildGroupFile(group: string, groupEntries: [string, AnyCommand][]): st
|
||||
];
|
||||
|
||||
for (const [path, cmd] of groupEntries) {
|
||||
lines.push(`| \`bl ${path}\` | ${escCell(cmd.description)} |`);
|
||||
lines.push(`| \`bl ${path}\` | ${escCell(referenceText(cmd.description))} |`);
|
||||
}
|
||||
|
||||
lines.push("", "## Command details", "");
|
||||
@@ -205,7 +211,9 @@ function buildIndex(
|
||||
|
||||
for (const [path, cmd] of entries) {
|
||||
const group = topLevel(path);
|
||||
lines.push(`| \`bl ${path}\` | ${escCell(cmd.description)} | [${group}.md](${group}.md) |`);
|
||||
lines.push(
|
||||
`| \`bl ${path}\` | ${escCell(referenceText(cmd.description))} | [${group}.md](${group}.md) |`,
|
||||
);
|
||||
}
|
||||
|
||||
lines.push("", "## By group", "", "| Group | Commands | Reference |", "| --- | --- | --- |");
|
||||
|
||||
Reference in New Issue
Block a user