feat(config): 支持命名配置功能并隔离默认配置数据

- 新增 `--config <name>` 参数支持读取与写入命名的配置块
- 命名配置与默认配置完全隔离,互不影响
- 规范命名配置名称格式,禁止路径穿越及顶层字段冲突
- 配置文件读取写入逻辑改为维护原始完整对象,支持多配置块共存
- AuthStore 和 ConfigStore 均支持命名配置,登录登出只影响指定配置块
- CLI 命令增加对 `--config` 标志的支持,包括 config set/show/auth status 等
- 鉴权状态输出带上配置名和配置文件路径信息
- 提示和报错信息包含配置相关上下文,增强用户体验
- 完善相关单元测试覆盖命名配置行为
This commit is contained in:
lisheng.lisheng
2026-07-13 13:04:50 +08:00
parent e1532bf35c
commit 155c9dc883
16 changed files with 375 additions and 36 deletions
@@ -0,0 +1,127 @@
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { describe, expect, test } from "vite-plus/test";
import { parseStdoutJson, runCli } from "./helpers.ts";
function withTempConfigDir<T>(fn: (dir: string) => Promise<T>): Promise<T> {
const dir = mkdtempSync(join(tmpdir(), "bl-config-profile-"));
return fn(dir).finally(() => {
rmSync(dir, { recursive: true, force: true });
});
}
function writeConfig(dir: string, data: Record<string, unknown>): void {
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, "config.json"), JSON.stringify(data, null, 2) + "\n");
}
describe("e2e: named config", () => {
test("根帮助展示 --config 全局标志", async () => {
const { stderr, exitCode } = await runCli(["--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--config <name>/);
});
test("config set --config 写入命名 block 且不影响默认配置", async () => {
await withTempConfigDir(async (dir) => {
writeConfig(dir, { output: "text", api_key: "sk-default" });
const setResult = await runCli(
[
"config",
"set",
"--config",
"dev",
"--key",
"output",
"--value",
"json",
"--output",
"json",
],
{ BAILIAN_CONFIG_DIR: dir },
);
expect(setResult.exitCode, setResult.stderr).toBe(0);
const setData = parseStdoutJson<{
output?: string;
config?: string;
config_file?: string;
}>(setResult.stdout);
expect(setData.output).toBe("json");
expect(setData.config).toBe("dev");
expect(setData.config_file).toBe(join(dir, "config.json"));
const raw = JSON.parse(readFileSync(join(dir, "config.json"), "utf8")) as Record<
string,
unknown
>;
expect(raw.output).toBe("text");
expect((raw.dev as Record<string, unknown>).output).toBe("json");
});
});
test("config show --config 只展示命名 block", async () => {
await withTempConfigDir(async (dir) => {
writeConfig(dir, {
output: "text",
api_key: "sk-default",
dev: { output: "json", access_token: "tok-dev" },
});
const { stdout, stderr, exitCode } = await runCli(
["config", "show", "--config", "dev", "--output", "json"],
{ BAILIAN_CONFIG_DIR: dir },
);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<Record<string, unknown>>(stdout);
expect(data.config).toBe("dev");
expect(data.config_file).toBe(join(dir, "config.json"));
expect(data.output).toBe("json");
expect(data.access_token).toBeDefined();
expect(data.api_key).toBeUndefined();
});
});
test("auth status --config 不继承默认凭证", async () => {
await withTempConfigDir(async (dir) => {
writeConfig(dir, { api_key: "sk-default", dev: { output: "json" } });
const devStatus = await runCli(["auth", "status", "--config", "dev", "--output", "json"], {
BAILIAN_CONFIG_DIR: dir,
});
expect(devStatus.exitCode, devStatus.stderr).toBe(0);
const devData = parseStdoutJson<Record<string, unknown>>(devStatus.stdout);
expect(devData.authenticated).toBe(false);
expect(devData.config).toBe("dev");
const defaultStatus = await runCli(["auth", "status", "--output", "json"], {
BAILIAN_CONFIG_DIR: dir,
});
expect(defaultStatus.exitCode, defaultStatus.stderr).toBe(0);
const defaultData = parseStdoutJson<Record<string, unknown>>(defaultStatus.stdout);
expect(defaultData.authenticated).toBe(true);
expect(defaultData.config).toBe("default");
});
});
test("--config default 等价默认配置", async () => {
await withTempConfigDir(async (dir) => {
writeConfig(dir, { output: "json", api_key: "sk-default" });
const { stdout, stderr, exitCode } = await runCli(
["config", "show", "--config", "default", "--output", "json"],
{ BAILIAN_CONFIG_DIR: dir },
);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<Record<string, unknown>>(stdout);
expect(data.config).toBe("default");
expect(data.api_key).toBeDefined();
});
});
test("非法 --config 名称报 usage error", async () => {
const { stderr, exitCode } = await runCli(["auth", "status", "--config", "../evil"]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/Invalid config name/);
});
});
@@ -6,7 +6,6 @@ import {
BailianError,
ExitCode,
chatPath,
getConfigPath,
requestJson,
type AuthStore,
type ConfigFile,
@@ -23,6 +22,9 @@ export interface LoginDeps {
const CONSOLE_LOGIN_TIMEOUT_MS = 15 * 60 * 1000;
const MAX_AUTH_CALLBACK_BODY = 65536;
// Regex for double newline (\r\n\r\n or \n\n); built via RegExp to avoid
// literal multi-line splitting in source.
const REGEX_DOUBLE_NEWLINE = new RegExp("\r\n\r\n|\n\n");
const CONSOLE_ORIGINS: Record<string, string> = {
domestic: "https://bailian.console.aliyun.com",
@@ -76,7 +78,7 @@ function parseAccessTokenFromMultipart(raw: string, boundaryValue: string): stri
for (let i = 1; i < segments.length; i++) {
const part = segments[i]!;
if (!/name\s*=\s*["'](?:access_token|accessToken)["']/i.test(part)) continue;
const sep = part.match(/\r\n\r\n|\n\n/);
const sep = part.match(REGEX_DOUBLE_NEWLINE);
if (!sep || sep.index === undefined) continue;
let value = part.slice(sep.index + sep[0].length);
value = value
@@ -495,7 +497,7 @@ export async function runConsoleLogin(
console_switch_agent: consoleSwitchAgent ? Number(consoleSwitchAgent) : undefined,
workspace_id: workspaceId || undefined,
});
process.stderr.write(`Config saved to ${getConfigPath()}\n`);
process.stderr.write(`Config saved to ${deps.authStore.path}\n`);
}
if (apiKey) {
const testBaseUrl = baseUrl || deps.authStore.resolveBaseUrl();
+2 -2
View File
@@ -1,4 +1,4 @@
import { defineCommand, getConfigPath } from "bailian-cli-core";
import { defineCommand } from "bailian-cli-core";
import { emitBare } from "bailian-cli-runtime";
import {
resolveConsoleOrigin,
@@ -129,7 +129,7 @@ export default defineCommand({
access_key_secret: flags.accessKeySecret,
access_token: accessToken,
});
process.stderr.write(`OpenAPI credentials saved to ${getConfigPath()}\n`);
process.stderr.write(`OpenAPI credentials saved to ${store.path}\n`);
return;
}
@@ -1,4 +1,4 @@
import { defineCommand, getConfigPath } from "bailian-cli-core";
import { defineCommand } from "bailian-cli-core";
import { emitBare } from "bailian-cli-runtime";
export default defineCommand({
@@ -25,13 +25,13 @@ export default defineCommand({
if (flags.console) {
if (settings.dryRun) {
if (stored.console) emitBare("Would clear access_token from ~/.bailian/config.json");
if (stored.console) emitBare(`Would clear access_token from ${store.path}`);
else emitBare("No console access_token to clear.");
emitBare("No changes made.");
return;
}
if (await store.logout("console")) {
process.stderr.write(`Cleared access_token from ${getConfigPath()}\n`);
process.stderr.write(`Cleared access_token from ${store.path}\n`);
if (stored.apiKey) {
process.stderr.write(
"api_key is still configured and will be used for authentication.\n",
@@ -46,13 +46,13 @@ export default defineCommand({
if (flags.openApi) {
if (settings.dryRun) {
if (stored.openapi)
emitBare("Would clear access_key_id / access_key_secret from ~/.bailian/config.json");
emitBare(`Would clear access_key_id / access_key_secret from ${store.path}`);
else emitBare("No OpenAPI AK/SK credentials to clear.");
emitBare("No changes made.");
return;
}
if (await store.logout("openapi")) {
process.stderr.write(`Cleared access_key_id / access_key_secret from ${getConfigPath()}\n`);
process.stderr.write(`Cleared access_key_id / access_key_secret from ${store.path}\n`);
if (stored.apiKey || stored.console) {
process.stderr.write(
"Other credentials are still configured and will be used for authentication.\n",
@@ -69,7 +69,7 @@ export default defineCommand({
if (settings.dryRun) {
if (hasKey)
emitBare(
"Would clear api_key / access_token / access_key_id / access_key_secret from ~/.bailian/config.json",
`Would clear api_key / access_token / access_key_id / access_key_secret from ${store.path}`,
);
else emitBare("No credentials to clear.");
emitBare("No changes made.");
@@ -78,7 +78,7 @@ export default defineCommand({
if (await store.logout("all")) {
process.stderr.write(
"Cleared api_key / access_token / access_key_id / access_key_secret from ~/.bailian/config.json\n",
`Cleared api_key / access_token / access_key_id / access_key_secret from ${store.path}\n`,
);
} else {
process.stderr.write("No credentials to clear.\n");
+16 -1
View File
@@ -35,11 +35,15 @@ export default defineCommand({
: undefined;
const authenticated = !!(apiKey || consoleCred || openapi);
const configName = settings.configName ?? "default";
const configFile = ctx.authStore().path;
if (!authenticated) {
emitResult(
{
authenticated: false,
config: configName,
config_file: configFile,
message: "Not authenticated.",
hint: [
`API key (model): ${identity.binName} auth login --api-key <key> or DASHSCOPE_API_KEY`,
@@ -54,10 +58,21 @@ export default defineCommand({
}
if (format !== "text") {
emitResult({ authenticated: true, api_key: apiKey, console: consoleCred, openapi }, format);
emitResult(
{
authenticated: true,
config: configName,
config_file: configFile,
api_key: apiKey,
console: consoleCred,
openapi,
},
format,
);
return;
}
emitBare(`Config: ${configName} (${configFile})`);
emitBare("Authentication Status:");
if (apiKey) {
emitBare(` API key (model): ${apiKey.source} ${apiKey.masked}`);
+16 -2
View File
@@ -101,7 +101,14 @@ export default defineCommand({
const format = detectOutputFormat(settings.output);
if (settings.dryRun) {
emitResult({ would_set: { [resolvedKey]: value } }, format);
emitResult(
{
would_set: { [resolvedKey]: value },
config: settings.configName ?? "default",
config_file: ctx.configStore().path,
},
format,
);
return;
}
@@ -110,7 +117,14 @@ export default defineCommand({
if (!settings.quiet) {
const shown = SECRET_KEYS.has(resolvedKey) ? maskToken(String(coerced)) : coerced;
emitResult({ [resolvedKey]: shown }, format);
emitResult(
{
[resolvedKey]: shown,
config: settings.configName ?? "default",
config_file: ctx.configStore().path,
},
format,
);
}
},
});
@@ -16,6 +16,7 @@ export default defineCommand({
base_url: client.baseUrl,
output: settings.output,
timeout: settings.timeout,
config: settings.configName ?? "default",
config_file: store.path,
};
+17 -5
View File
@@ -1,6 +1,7 @@
import type { ConfigFile } from "../config/schema.ts";
import type { ResolutionSources } from "../config/loader.ts";
import { readConfigFile, writeConfigFile } from "../config/loader.ts";
import { getConfigPath } from "../config/paths.ts";
import type { AuthState } from "./types.ts";
import { describeAuthState, resolveModelBaseUrl } from "./resolver.ts";
@@ -40,13 +41,18 @@ export interface AuthStore {
login(patch: AuthPersistPatch): Promise<void>;
/** 清凭证:console/openapi 只删对应域;all 清全部登录凭证。返回是否有变更。 */
logout(scope: "console" | "openapi" | "all"): Promise<boolean>;
/** 实际写入的 config.json 路径(不受命名配置影响,一直是同一个文件)。 */
path: string;
/** 当前命名配置名(`--config <name>` 解析后);未指定或 `default` 时为 undefined。 */
configName?: string;
}
export function makeAuthStore(sources: ResolutionSources): AuthStore {
const configName = sources.configName;
return {
describe: () => describeAuthState(sources),
stored() {
const file = readConfigFile();
const file = readConfigFile(configName);
return {
apiKey: !!file.api_key,
console: !!file.access_token,
@@ -55,20 +61,26 @@ export function makeAuthStore(sources: ResolutionSources): AuthStore {
},
resolveBaseUrl: () => resolveModelBaseUrl(sources),
async login(patch) {
const existing = readConfigFile() as Record<string, unknown>;
const existing = readConfigFile(configName) as Record<string, unknown>;
for (const [key, value] of Object.entries(patch)) {
if (value !== undefined) existing[key] = value;
}
await writeConfigFile(existing);
await writeConfigFile(existing, configName);
},
async logout(scope) {
const existing = readConfigFile() as Record<string, unknown>;
const existing = readConfigFile(configName) as Record<string, unknown>;
const keys = LOGOUT_KEYS[scope];
const had = keys.some((key) => existing[key] !== undefined);
if (!had) return false;
for (const key of keys) delete existing[key];
await writeConfigFile(existing);
await writeConfigFile(existing, configName);
return true;
},
get path() {
return sources.configPath ?? getConfigPath();
},
get configName() {
return configName;
},
};
}
+2 -2
View File
@@ -1,6 +1,6 @@
export type { ConfigFile, Region, Identity, Settings } from "./schema.ts";
export { BAILIAN_HOST, DOCS_HOSTS, REGIONS, parseConfigFile } from "./schema.ts";
export { readConfigFile, writeConfigFile } from "./loader.ts";
export { BAILIAN_HOST, CONFIG_FILE_KEYS, DOCS_HOSTS, REGIONS, parseConfigFile } from "./schema.ts";
export { normalizeConfigName, readConfigFile, writeConfigFile } from "./loader.ts";
export { buildSources, buildSettings, type ResolutionSources } from "./loader.ts";
export { makeConfigStore, type ConfigStore } from "./store.ts";
export { ensureConfigDir, getConfigDir, getConfigPath, getCredentialsPath } from "./paths.ts";
+71 -7
View File
@@ -1,16 +1,45 @@
import { readFileSync, writeFileSync, renameSync, existsSync } from "fs";
import { parseConfigFile, type ConfigFile, type Settings } from "./schema.ts";
import { CONFIG_FILE_KEYS, parseConfigFile, type ConfigFile, type Settings } from "./schema.ts";
import { ensureConfigDir, getConfigPath } from "./paths.ts";
import { detectOutputFormat } from "../output/formatter.ts";
import { BailianError } from "../errors/base.ts";
import { ExitCode } from "../errors/codes.ts";
import type { SourceFlags } from "../types/command.ts";
export function readConfigFile(): ConfigFile {
const CONFIG_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
/**
* 校验并规范化 `--config <name>``undefined`/""/"default" 都视为未指定(等价顶层默认配置)。
* 合法命名只允许字母、数字、`-`/`_`,且不能与 `ConfigFile` 顶层字段同名(避免写入时与默认配置字段歧义)。
*/
export function normalizeConfigName(name?: unknown): string | undefined {
if (name === undefined || name === "" || name === "default") return undefined;
if (typeof name !== "string" || !CONFIG_NAME_PATTERN.test(name)) {
const display = typeof name === "string" ? name : JSON.stringify(name);
throw new BailianError(
`Invalid config name "${display}".`,
ExitCode.USAGE,
"Use letters, numbers, '-' or '_', starting with a letter or number.",
);
}
if ((CONFIG_FILE_KEYS as readonly string[]).includes(name)) {
throw new BailianError(
`Invalid config name "${name}". It conflicts with a config key.`,
ExitCode.USAGE,
);
}
return name;
}
/** 读完整 config.json 原始对象(不经过 `parseConfigFile` 过滤),保留其他命名配置 block。 */
function readRawConfigObject(): Record<string, unknown> {
const path = getConfigPath();
if (!existsSync(path)) return {};
try {
return parseConfigFile(JSON.parse(readFileSync(path, "utf-8")));
const raw = JSON.parse(readFileSync(path, "utf-8")) as unknown;
return raw && typeof raw === "object" && !Array.isArray(raw)
? (raw as Record<string, unknown>)
: {};
} catch (err) {
const e = err as Error;
if (e instanceof SyntaxError || e.message.includes("JSON")) {
@@ -20,11 +49,34 @@ export function readConfigFile(): ConfigFile {
}
}
export async function writeConfigFile(data: Record<string, unknown>): Promise<void> {
function readRawConfigBlock(raw: Record<string, unknown>, configName?: string): unknown {
if (!configName) return raw;
const block = raw[configName];
return block && typeof block === "object" && !Array.isArray(block) ? block : {};
}
export function readConfigFile(configName?: string): ConfigFile {
const raw = readRawConfigObject();
return parseConfigFile(readRawConfigBlock(raw, configName));
}
export async function writeConfigFile(
data: Record<string, unknown>,
configName?: string,
): Promise<void> {
const raw = readRawConfigObject();
if (configName) {
raw[configName] = data;
} else {
for (const key of Object.keys(raw)) {
if ((CONFIG_FILE_KEYS as readonly string[]).includes(key)) delete raw[key];
}
Object.assign(raw, data);
}
await ensureConfigDir();
const path = getConfigPath();
const tmp = path + ".tmp";
writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n", { mode: 0o600 });
writeFileSync(tmp, JSON.stringify(raw, null, 2) + "\n", { mode: 0o600 });
renameSync(tmp, path);
}
@@ -36,10 +88,21 @@ export interface ResolutionSources {
flags: Partial<SourceFlags>;
file: ConfigFile;
env: NodeJS.ProcessEnv;
/** 当前命名配置名(`--config <name>` 解析后);未指定或 `default` 时为 undefined。 */
configName?: string;
/** 实际 config.json 路径(不受 configName 影响,一直是同一个文件)。 */
configPath?: string;
}
export function buildSources(flags: Partial<SourceFlags>): ResolutionSources {
return { flags, file: readConfigFile(), env: process.env };
const configName = normalizeConfigName(flags.config);
return {
flags,
file: readConfigFile(configName),
env: process.env,
configName,
configPath: getConfigPath(),
};
}
/**
@@ -60,7 +123,8 @@ export function buildSettings(s: ResolutionSources): Settings {
}
return {
configPath: getConfigPath(),
configPath: s.configPath ?? getConfigPath(),
configName: s.configName,
intentDetectBaseUrl:
file.intent_detect_base_url || env.DASHSCOPE_INTENT_DETECT_BASE_URL || undefined,
output: detectOutputFormat(flags.output || env.DASHSCOPE_OUTPUT || file.output),
+24
View File
@@ -46,6 +46,29 @@ export interface ConfigFile {
telemetry?: boolean;
}
export const CONFIG_FILE_KEYS = [
"api_key",
"access_token",
"access_key_id",
"access_key_secret",
"security_token",
"base_url",
"intent_detect_base_url",
"output",
"output_dir",
"timeout",
"default_text_model",
"default_video_model",
"default_image_model",
"default_speech_model",
"default_omni_model",
"workspace_id",
"console_site",
"console_region",
"console_switch_agent",
"telemetry",
] as const satisfies readonly (keyof ConfigFile)[];
const VALID_OUTPUTS = new Set<string>(["text", "json"]);
const VALID_CONSOLE_SITES = new Set<string>(["domestic", "international"]);
@@ -136,6 +159,7 @@ export interface Identity {
*/
export interface Settings {
configPath?: string;
configName?: string;
/** Dedicated base URL for intent-detect model; falls back to the model baseUrl at call site. */
intentDetectBaseUrl?: string;
output: "text" | "json";
+10 -6
View File
@@ -13,26 +13,30 @@ export interface ConfigStore {
/** 删除指定键。 */
unset(keys: (keyof ConfigFile)[]): Promise<void>;
path: string;
configName?: string;
}
export function makeConfigStore(): ConfigStore {
export function makeConfigStore(configName?: string): ConfigStore {
return {
read: () => readConfigFile(),
read: () => readConfigFile(configName),
async write(patch) {
const existing = readConfigFile() as Record<string, unknown>;
const existing = readConfigFile(configName) as Record<string, unknown>;
for (const [key, value] of Object.entries(patch)) {
if (value === undefined) delete existing[key];
else existing[key] = value;
}
await writeConfigFile(existing);
await writeConfigFile(existing, configName);
},
async unset(keys) {
const existing = readConfigFile() as Record<string, unknown>;
const existing = readConfigFile(configName) as Record<string, unknown>;
for (const key of keys) delete existing[key];
await writeConfigFile(existing);
await writeConfigFile(existing, configName);
},
get path() {
return getConfigPath();
},
get configName() {
return configName;
},
};
}
+5
View File
@@ -71,6 +71,11 @@ export const GLOBAL_FLAGS = {
quiet: { type: "switch", description: "Suppress non-essential output" },
verbose: { type: "switch", description: "Print HTTP request/response details" },
dryRun: { type: "switch", description: "Dry run mode" },
config: {
type: "string",
valueHint: "<name>",
description: "Use named config credentials",
},
help: { type: "switch", description: "Show help" },
version: { type: "switch", description: "Print version" },
} satisfies FlagsDef;
+70
View File
@@ -4,6 +4,13 @@ import { join } from "path";
import { expect, test } from "vite-plus/test";
import { makeConfigStore } from "../src/config/store.ts";
import { makeAuthStore } from "../src/auth/store.ts";
import {
buildSources,
normalizeConfigName,
readConfigFile,
writeConfigFile,
} from "../src/config/loader.ts";
import { getConfigPath } from "../src/config/paths.ts";
/** 在隔离的临时配置目录里执行,结束后恢复环境。 */
async function inTempConfigDir(fn: () => Promise<void>): Promise<void> {
@@ -64,3 +71,66 @@ test("AuthStore:login 合并落盘,logout 按域清理并报告变更", async ()
expect(makeConfigStore().read().workspace_id).toBe("ws-1");
});
});
test("ConfigStore:命名 config 与默认配置隔离且写入保留其它 block", async () => {
await inTempConfigDir(async () => {
await writeConfigFile({ api_key: "sk-default", output: "json" });
await writeConfigFile({ api_key: "sk-prod", output: "text" }, "prod");
const dev = makeConfigStore("dev");
await dev.write({ api_key: "sk-dev", timeout: 120 });
expect(makeConfigStore().read()).toMatchObject({ api_key: "sk-default", output: "json" });
expect(dev.read()).toMatchObject({ api_key: "sk-dev", timeout: 120 });
expect(makeConfigStore("prod").read()).toMatchObject({ api_key: "sk-prod", output: "text" });
expect(readConfigFile("dev")).not.toMatchObject({ output: "json" });
expect(dev.path).toBe(getConfigPath());
});
});
test("AuthStore:login/logout 只影响当前命名 config", async () => {
await inTempConfigDir(async () => {
await writeConfigFile({ api_key: "sk-default", access_token: "tok-default" });
const sources = buildSources({ config: "dev" });
const store = makeAuthStore(sources);
await store.login({ api_key: "sk-dev", access_token: "tok-dev", workspace_id: "ws-dev" });
expect(makeConfigStore().read()).toMatchObject({
api_key: "sk-default",
access_token: "tok-default",
});
expect(makeConfigStore("dev").read()).toMatchObject({
api_key: "sk-dev",
access_token: "tok-dev",
workspace_id: "ws-dev",
});
expect(await store.logout("console")).toBe(true);
expect(makeConfigStore("dev").read().access_token).toBeUndefined();
expect(makeConfigStore().read().access_token).toBe("tok-default");
});
});
test("config name 校验拒绝路径穿越和 ConfigFile 字段冲突", () => {
expect(normalizeConfigName("dev_1")).toBe("dev_1");
expect(normalizeConfigName("default")).toBeUndefined();
expect(() => normalizeConfigName("../evil")).toThrow(/Invalid config name/);
expect(() => normalizeConfigName("api_key")).toThrow(/conflicts with a config key/);
});
test("buildSources 暴露命名 config 且 default 等价顶层", async () => {
await inTempConfigDir(async () => {
await writeConfigFile({ api_key: "sk-default", output: "json" });
await writeConfigFile({ access_token: "tok-dev" }, "dev");
const defaultSources = buildSources({ config: "default" });
expect(defaultSources.configName).toBeUndefined();
expect(defaultSources.file.api_key).toBe("sk-default");
const devSources = buildSources({ config: "dev" });
expect(devSources.configName).toBe("dev");
expect(devSources.configPath).toBe(getConfigPath());
expect(devSources.file.access_token).toBe("tok-dev");
expect(devSources.file.api_key).toBeUndefined();
});
});
+1 -1
View File
@@ -167,7 +167,7 @@ export function createCli(commands: Record<string, AnyCommand>, opts: CliOptions
flags: ownFlags,
settings,
sources,
configStore: () => makeConfigStore(),
configStore: () => makeConfigStore(sources.configName),
authStore: () => makeAuthStore(sources),
client: new Client({ identity, settings, baseUrl: resolveModelBaseUrl(sources) }),
};
+1
View File
@@ -128,6 +128,7 @@ Available on every command (in addition to command-specific flags):
| `--quiet` | switch | no | Suppress non-essential output |
| `--verbose` | switch | no | Print HTTP request/response details |
| `--dry-run` | switch | no | Dry run mode |
| `--config <name>` | string | no | Use named config credentials |
| `--help` | switch | no | Show help |
| `--version` | switch | no | Print version |