refactor(output): centralize ANSI styling and remove no-color flag

- remove --no-color from GLOBAL_FLAGS and drop Settings.noColor
- move ANSI styling decisions into runtime color helpers with NO_COLOR support
- update command text renderers to use shared color helpers instead of local ANSI codes
- refresh e2e invocations, generated reference, and agent skill guidance
This commit is contained in:
若麒
2026-07-07 00:07:21 +08:00
parent c35f2856e5
commit 476dd3b841
31 changed files with 237 additions and 249 deletions
@@ -6,7 +6,7 @@
>
> 实施(2026-07-06):**已按本方案完成**——前置 baseUrl 翻转 + 阶段 0–6 全部落地(含 flags 收窄/分流/同名守卫、console/advisor/pipeline 收口、tracker 传值、边界守卫测试 `packages/commands/tests/boundaries.test.ts`)。全量 `vp check`/单测/关键 e2e 绿;**未 commit,待 review**。实施中的偏差:advisor 匿名调网关促使 `callConsoleGateway` 收 `ConsoleGatewayTarget`(token 可选)而非整个 credential;`describeAuth` 更名 `describeAuthState`;`ConfigStore.reset` 无消费者未实现。
>
> flag 边界轮(2026-07-06):**域化完成**——flag 拆 `GLOBAL_FLAGS` + `MODEL_AUTH_FLAGS`/`CONSOLE_AUTH_FLAGS`(按命令 `auth`/`authFlags` 可见),16 处遮蔽清零,跨域传 flag 报错,help 改为 Flags(自有+域)/Global Flags(全量)三段式,`pipeline run --timeout` 更名 `--step-timeout`,login 经 `AuthStore.flagInput()` 收凭证输入。workspaceId 已升入 console 域(链 flag > env > file),stats 命令内优先级删除。
> flag 边界轮(2026-07-06):**域化完成**——flag 拆 `GLOBAL_FLAGS` + `MODEL_AUTH_FLAGS`/`CONSOLE_AUTH_FLAGS`(按命令 `auth` 可见),16 处遮蔽清零,跨域传 flag 报错,help 改为 Flags(自有+域)/Global Flags(全量)三段式,`pipeline run --timeout` 更名 `--step-timeout`,login 凭证输入由自有 flags + `AuthStore.login()` 落盘。workspaceId 已升入 console 域(链 flag > env > file),stats 命令内优先级删除。
>
> 修订(2026-07-04 评审后,均已拍板):§8 改 strangler 分阶段 + 阶段 0 行为锁定测试(已落地);§2 store 接口细化(write async / unset / AuthStore.login 揽登录落盘);§5 validate 收 ownFlags + 同名守卫 + authStage dry-run 双域容忍;§7 tracker/workspaceId 修法;§0/§9 优先级链保真口径。dry-run 决策:**保持"无需凭证"现状**,console 三元组归 Settings 服务 dry-run 展示(不引入 ConsoleTarget);dry-run 输出规范统一推后(§9)。
>
@@ -23,7 +23,7 @@
> **ctx 是唯一组合根。它在边界处把 flag / env / file / 默认 各源解析成 `identity / settings / credential`,交给命令。**
>
> 优先级链已**统一为 flag > env > file > 默认**:唯一异类 baseUrl(原 flag>file>env)已在前置独立 commit 翻转,锁定表(`packages/core/tests/config-priority.test.ts`)同步更新,`buildSettings` 逐字段对照锁定表移植。workspaceId 无全局 flag 源(见 §9);verbose/noColor 为 OR 语义、telemetry 的 DO_NOT_TRACK 为业界标准,均非链序问题。
> 优先级链已**统一为 flag > env > file > 默认**:唯一异类 baseUrl(原 flag>file>env)已在前置独立 commit 翻转,锁定表(`packages/core/tests/config-priority.test.ts`)同步更新,`buildSettings` 逐字段对照锁定表移植。workspaceId 已升入 console 域(链 flag > env > file);verbose 为 OR 语义、telemetry 的 DO_NOT_TRACK 为业界标准,均非链序问题。
三条硬规矩:
@@ -70,7 +70,6 @@ export interface Settings {
output: "text" | "json";
outputDir?: string;
timeout: number;
concurrent?: number; // 命令经 getConcurrency 读 → 归 settings
defaultTextModel?: string;
defaultVideoModel?: string;
defaultImageModel?: string;
@@ -82,11 +81,7 @@ export interface Settings {
consoleSwitchAgent?: number;
verbose: boolean;
quiet: boolean;
noColor: boolean;
yes: boolean;
dryRun: boolean;
nonInteractive: boolean; // 0 消费者,可留可删;留着零风险
async: boolean;
telemetry: boolean;
}
```
@@ -236,11 +231,16 @@ export function describeAuth(s: ResolutionSources): AuthState; // auth status
```ts
case "run": {
// 1) 一次解析(全局+命令 flag 合并)
const parsed = parseFlags(res.rest, { ...GLOBAL_FLAGS, ...res.command.flags });
// 1) 一次解析(全局 + 凭证域 + 命令 flag 合并)
const credDefs = credentialFlagDefs(res.command);
const parsed = parseFlags(res.rest, {
...GLOBAL_FLAGS,
...credDefs,
...res.command.flags,
});
// 2) 分流(见下"分流规则"),validate 收收窄后的 ownFlags(与 §2 签名一致,别传 parsed)
const globals = pick(parsed, Object.keys(GLOBAL_FLAGS)); // 全局 flag → sources
const globals = pick(parsed, [...Object.keys(GLOBAL_FLAGS), ...Object.keys(credDefs)]); // 全局+凭证域 → sources
const ownFlags = pick(parsed, Object.keys(res.command.flags ?? {})); // 命令声明的 → ctx.flags
const invalid = res.command.validate?.(ownFlags);
if (invalid) throw new UsageError(invalid);
@@ -262,13 +262,13 @@ case "run": {
}
```
**分流规则(重要,含"本次不改遮蔽"的妥协):**
**分流规则(重要):**
> **全局 flag 恒进 `sources`(用 `Object.keys(GLOBAL_FLAGS)`);命令声明的 flag 进 `ctx.flags`(用 `Object.keys(command.flags)`)。同名遮蔽者两边都出现(受控重叠)。**
> **全局 flag + 当前命令可见的凭证域 flag 进 `sources`;命令自有 flag 进 `ctx.flags`。**
为什么这样:约 15 个命令把全局 flag(`consoleSite/consoleRegion/switchAgent`、`async`)重声明成自己的,纯为 help 显示(声明不读)。若"命令声明的 key 一律归 ctx.flags 且不进 sources",这些遮蔽会让 `--console-region` 到不了 `resolveConsole`,区域覆盖静默失效。**本次不清理这些遮蔽**(已记录在钉钉文档),用"全局恒进 sources"规避,行为不变;代价是这 ~15 个 flag 在 ctx.flags 和 sources 各出现一次(auth login 的 apiKey/baseUrl 也在两边,但 auth:none 不解析,无害)。
为什么这样:`MODEL_AUTH_FLAGS` / `CONSOLE_AUTH_FLAGS` 只按命令 `auth` 暴露,既保证 `--api-key` / `--console-region` 这类域 flag 能进入 credential/settings 解析链,也避免无关命令误收跨域 flag。历史同名遮蔽已清理,命令自有 flag 与全局/域 flag 不再受控重叠。
**同名守卫**:registry 构建时断言 —— 命令 flag 与全局同名时,其 FlagDef `type` 必须一致。分流规则依赖"遮蔽都是同型重声明"这一假设;十行断言把口头约定变成机器约定,防止未来有人把 `async` 重声明成 value flag 后,错型值静默流进 sources。
**同名守卫**:registry 构建时断言 —— 命令自有 flag 与全局/凭证域 flag 同名即报错。分流规则依赖"同一 key 只归一个域"这一约束,防止未来新增 flag 时把同名值静默分到错误通道。
`authStage`(`packages/runtime/src/middleware.ts`):
@@ -292,17 +292,19 @@ const authStage = async (ctx, next) => {
## 6. 字段迁移对照(旧 `Config` → 去向)
| 旧 `Config` 字段 | 去向 |
| ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| `clientName` / `clientVersion` | **Identity**(`clientName` / `version`) |
| `binName` / `npmPackage` | **Identity** |
| `apiKey` / `apiKeyEnv` / `fileApiKey` / `fileAccessToken` | **删除** → provider chain 从 sources 读 |
| `baseUrl` | **ApiKeyCredential.baseUrl**(resolveApiKey 里解析) |
| `consoleSite` / `consoleRegion` / `consoleSwitchAgent` | **Settings**(dry-run 展示)+ **ConsoleCredential**(真实调用),受控重叠 |
| `workspaceId` | **Settings** |
| `output` / `outputDir` / `timeout` / `default*Model` | **Settings** |
| `verbose` / `quiet` / `noColor` / `dryRun` / `async` / `yes` / `telemetry` / `configPath` | **Settings** |
| `concurrent`(原本仅 flags) | **Settings**(`getConcurrency` 改读 settings) |
| 旧 `Config` 字段 | 去向 |
| ----------------------------------------------------------- | -------------------------------------------------------------------- |
| `clientName` / `clientVersion` | **Identity**(`clientName` / `version`) |
| `binName` / `npmPackage` | **Identity** |
| `apiKey` / `apiKeyEnv` / `fileApiKey` / `fileAccessToken` | **删除** → provider chain 从 sources 读 |
| `baseUrl` | **ApiKeyCredential.baseUrl**(resolveApiKey 里解析) |
| `consoleSite` / `consoleRegion` / `consoleSwitchAgent` | **Settings**(dry-run 展示)+ **ConsoleCredential**(真实调用),受控重叠 |
| `workspaceId` | **Settings** |
| `output` / `outputDir` / `timeout` / `default*Model` | **Settings** |
| `verbose` / `quiet` / `dryRun` / `telemetry` / `configPath` | **Settings** |
| `async` / `concurrent` | **命令自有 flag**(`ASYNC_FLAG` / `CONCURRENT_FLAG`) |
| `yes` | **命令自有 flag**(`quota request`) |
| `nonInteractive` | **删除** |
---
@@ -314,10 +316,10 @@ const authStage = async (ctx, next) => {
- `client/client.ts:38`:`apiCred?.baseUrl ?? config.baseUrl` → `apiCred.baseUrl`
- 命令读 `config.binName`(约 6 处:`auth/status`、`usage/stats`、`mcp/list`、`quota/history`、`quota/request`)→ `identity.binName`(经 ctx)
- **console 收口**:约 12 处 `callConsoleGateway(config, token, {api,data})`(`app/list`、`workspace/list`、`usage/*`、`mcp/list`、`quota/*`、`console/call`)→ `ctx.client.callConsole({api,data})`;dry-run 里的 `effectiveConsoleGatewayConfig(config)` → `effectiveConsoleGatewayConfig(settings)`(签名收窄,不走 client)
- **workspaceId**:`usage/stats.ts` 的 `resolveWorkspaceId(config, flag)` → `flags.workspaceId ?? requireWorkspace(ctx.settings)`(新 helper 只兜 settings,缺失时报原来的错 + `${identity.binName} workspace list` 提示)。**注意:`--workspace-id` 是命令级 flag、不在 GLOBAL_FLAGS,进不了 sources/settings,flag 的第一优先级必须在命令里显式保住**,否则静默丢失
- **workspaceId**:`usage/stats.ts` 的 `resolveWorkspaceId(config, flag)` → `requireWorkspaceId(ctx.settings, identity.binName)`。`--workspace-id` 已升入 `CONSOLE_AUTH_FLAGS`,进入 sources/settings,链为 flag > env > file。
- **config/auth 命令**:`readConfigFile/writeConfigFile/resolver` 直接调用 → 走 `ctx.configStore()` / `ctx.authStore()`
- **auth/login `validate`**:`!f.console && !f.apiKey`——`apiKey`/`baseUrl` 是它自己声明的 flag(`login.ts:15`),收窄后仍在 `ctx.flags`,**无需改**
- **pipeline**:`buildPipelineConfig`(伪造整套 GlobalFlags,`runtime/src/pipeline/bl-config.ts`)→ `buildSettings({ flags: {}, file: readConfigFile(), env })`(flags 已收 Partial,见 §4) + 强制 `output:'json'/quiet/nonInteractive`;pipeline executor 是"迷你边界",给 step 构造 settings/client
- **pipeline**:`buildPipelineConfig`(伪造整套 GlobalFlags,`runtime/src/pipeline/bl-config.ts`)→ `buildSettings({ flags: {}, file: readConfigFile(), env })`(flags 已收 Partial,见 §4) + 强制 `output:'json'/quiet`;pipeline executor 是"迷你边界",给 step 构造 settings/client
- 其余把 `Config` 当类型用的地方 → `Settings`;ctx 字段 `config` → `settings`(`ctx.config` 仅 2 处、解构 `const { config } = ctx` 约 46 处 + 其函数体内 `config.` → `settings.`)
**命名注意**:
@@ -344,13 +346,13 @@ const authStage = async (ctx, next) => {
## 9. 本次不做(已在钉钉文档记录,后续单独轮次)
- **flag 清理**:`nonInteractive` 删 / `async` ↔ 各命令 `--no-wait` 去重 / `yes` 收窄到命令级 / `noColor` 修一致性(registry/progress/banner 里内联 `process.stderr.isTTY` 绕过了 `config.noColor`)。
- ~~**flag 清理**~~ **已完成**:`nonInteractive` 删除;`async`/`concurrent` 改为命令级共享定义;`yes` 收窄为 `quota request` 自有;原颜色 CLI flag / Settings 字段删除,颜色由 `NO_COLOR` + 实际输出流 `isTTY` 共同决定,内联判断收束到 runtime helper。
- ~~workspaceId 的 flag 源~~ **已完成**(flag 边界轮):`--workspace-id` 升入 `CONSOLE_AUTH_FLAGS`,链为 flag > env > file;stats 删除自有声明与命令内优先级。(优先级链归一与同名遮蔽清理亦已完成:baseUrl 前置翻转见 §0,遮蔽经域化清零见 §5。)
- **dry-run 输出规范统一**:各域输出现状不一致 —— model/app 域只打请求 body(不含 URL/baseUrl),console 域额外打 api 名 + region/site 路由信息。应一次定规范、跨域对齐(是否展示路由、展示哪些字段);届时若 console 域不再展示,console 三元组可从 Settings 撤出、收敛为纯 credential。**本次保持现状输出**(e2e 有断言)。
- ~~全局↔命令私有 flag 同名遮蔽清理~~ **已完成**(flag 边界轮,经域化):16 处遮蔽全删,凭证 flag 按 `auth` 域可见,跨域报 Unknown flag,守卫升级为同名即抛。
- **key ↔ baseUrl 强校验**(region 锁)。落点已就位:`resolveApiKey` 是唯一同时产出 `{token, baseUrl}` 的地方,校验加在它内部即可;baseUrl 不在 Settings,命令侧无法绕过绑定;`AuthStore.login` 已支持 `api_key` + `base_url` 成对落盘。
- **多 profile / 多身份**(arkcli 式)。结构已留缝:单一 `ResolutionSources` 边界 + credential 封装。
- **IOStreams 注入**(gh `Factory.IOStreams` / vercel `Client.stdout`);与 noColor 修复同属下一轮。
- **IOStreams 注入**(gh `Factory.IOStreams` / vercel `Client.stdout`);颜色 stream helper 已先行收束,完整 IOStreams 注入仍留后续轮次。
- `ConfigFile`(磁盘格式)不变;无用户可见 CLI 变化。
外部记录:钉钉文档 `https://alidocs.dingtalk.com/i/nodes/YMyQA2dXW7gYo6MzcZzzERNMWzlwrZgb`(全局 flag 对比、flag 清理项、遮蔽问题)。
+2 -9
View File
@@ -61,7 +61,6 @@ describe("e2e: auth", () => {
"json",
"--timeout",
"120",
"--no-color",
]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toContain("Would validate and save API key.");
@@ -82,14 +81,8 @@ describe("e2e: auth", () => {
expect(stderr).not.toContain("Cleared api_key");
});
test("auth logout --dry-run --quiet --no-color", async () => {
const { stdout, stderr, exitCode } = await runCli([
"auth",
"logout",
"--dry-run",
"--quiet",
"--no-color",
]);
test("auth logout --dry-run --quiet", async () => {
const { stdout, stderr, exitCode } = await runCli(["auth", "logout", "--dry-run", "--quiet"]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toContain("No changes made.");
});
+2 -8
View File
@@ -38,14 +38,8 @@ describe("e2e: config", () => {
expect(data.timeout).toBeDefined();
});
test("config show --output text --no-color", async () => {
const { stdout, stderr, exitCode } = await runCli([
"config",
"show",
"--output",
"text",
"--no-color",
]);
test("config show --output text", async () => {
const { stdout, stderr, exitCode } = await runCli(["config", "show", "--output", "text"]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toMatch(/config_file|timeout|base_url/i);
});
+2 -18
View File
@@ -96,13 +96,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => {
});
test("quota list 文本输出包含英文表头", async () => {
const { stdout, stderr, exitCode } = await runCli([
"quota",
"list",
"--output",
"text",
"--no-color",
]);
const { stdout, stderr, exitCode } = await runCli(["quota", "list", "--output", "text"]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toContain("Model");
expect(stdout).toContain("Req/min");
@@ -118,7 +112,6 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => {
"qwen3.6-plus",
"--output",
"text",
"--no-color",
]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toContain("qwen3.6-plus");
@@ -254,13 +247,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => {
});
test("quota check 文本输出包含英文表头", async () => {
const { stdout, stderr, exitCode } = await runCli([
"quota",
"check",
"--output",
"text",
"--no-color",
]);
const { stdout, stderr, exitCode } = await runCli(["quota", "check", "--output", "text"]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toContain("Model");
expect(stdout).toContain("RPM Usage/Limit");
@@ -276,7 +263,6 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => {
"qwen3.6-plus",
"--output",
"text",
"--no-color",
]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toContain("qwen3.6-plus");
@@ -291,7 +277,6 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => {
"qwen3.6-plus,qwen-plus",
"--output",
"text",
"--no-color",
]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toContain("qwen3.6-plus");
@@ -335,7 +320,6 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => {
"qwen3.6-plus",
"--output",
"text",
"--no-color",
]);
expect(exitCode, stderr).toBe(0);
const hasStatus =
@@ -146,7 +146,6 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage free(Console)", () => {
"qwen3-max",
"--output",
"text",
"--no-color",
]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toContain("Model");
@@ -165,7 +164,6 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage free(Console)", () => {
"qwen3-max",
"--output",
"text",
"--no-color",
]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toContain("qwen3-max");
@@ -179,7 +177,6 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage free(Console)", () => {
"qwen3-max,qwen-turbo",
"--output",
"text",
"--no-color",
]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toContain("qwen3-max");
@@ -194,7 +191,6 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage free(Console)", () => {
"qwen3-max",
"--output",
"text",
"--no-color",
]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toContain("Text");
@@ -208,7 +204,6 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage free(Console)", () => {
"wan2.7-image",
"--output",
"text",
"--no-color",
]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toContain("Unsupported");
@@ -222,7 +217,6 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage free(Console)", () => {
"wan2.7-image",
"--output",
"text",
"--no-color",
]);
expect(exitCode, stderr).toBe(0);
const lines = stdout.split("\n").filter((line) => line.includes("wan2.7-image"));
@@ -239,7 +233,6 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage free(Console)", () => {
"nonexistent-model-xyz-12345",
"--output",
"text",
"--no-color",
]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toContain("nonexistent-model-xyz-12345");
@@ -253,7 +246,6 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage free(Console)", () => {
"qwen3-max",
"--output",
"text",
"--no-color",
]);
expect(exitCode, stderr).toBe(0);
const hasAutoStop =
@@ -177,7 +177,6 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage stats(Console)", () => {
wsId,
"--output",
"text",
"--no-color",
]);
expect(exitCode, stderr).toBe(0);
});
@@ -190,7 +189,6 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage stats(Console)", () => {
wsId,
"--output",
"text",
"--no-color",
]);
expect(exitCode, stderr).toBe(0);
});
@@ -205,7 +203,6 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage stats(Console)", () => {
"qwen3.6-plus",
"--output",
"text",
"--no-color",
]);
expect(exitCode, stderr).toBe(0);
});
@@ -220,7 +217,6 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage stats(Console)", () => {
"qwen3.6-plus,deepseek-v4-pro",
"--output",
"text",
"--no-color",
]);
expect(exitCode, stderr).toBe(0);
});
@@ -235,7 +231,6 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage stats(Console)", () => {
"nonexistent-model-xyz-99999",
"--output",
"text",
"--no-color",
]);
expect(exitCode, stderr).toBe(0);
});
@@ -250,7 +245,6 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage stats(Console)", () => {
"1",
"--output",
"text",
"--no-color",
]);
expect(exitCode, stderr).toBe(0);
});
@@ -265,7 +259,6 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage stats(Console)", () => {
"Vision",
"--output",
"text",
"--no-color",
]);
expect(exitCode, stderr).toBe(0);
});
@@ -14,8 +14,7 @@ import {
} from "bailian-cli-core";
import boxen from "boxen";
import chalk, { Chalk, type ChalkInstance } from "chalk";
import { emitBare, emitResult } from "bailian-cli-runtime";
import { createSpinner } from "bailian-cli-runtime";
import { createSpinner, emitBare, emitResult, supportsColor } from "bailian-cli-runtime";
function formatContextWindow(tokens: number): string {
if (tokens >= 1_000_000)
@@ -55,8 +54,13 @@ const PREFERENCE_MODE_LABELS: Record<string, string> = {
alternative: "Alternative",
};
function formatIntentSummary(intent: IntentProfile, noColor: boolean): string {
const colorize = noColor ? new Chalk({ level: 0 }) : chalk;
function chalkFor(out: NodeJS.WriteStream): ChalkInstance {
return supportsColor(out) ? chalk : new Chalk({ level: 0 });
}
function formatIntentSummary(intent: IntentProfile): string {
const colorize = chalkFor(process.stdout);
const useColor = supportsColor(process.stdout);
const lines: string[] = [];
lines.push(colorize.cyan.bold("Intent Analysis"));
@@ -121,15 +125,20 @@ function formatIntentSummary(intent: IntentProfile, noColor: boolean): string {
return boxen(lines.join("\n"), {
padding: { top: 0, bottom: 0, left: 1, right: 1 },
margin: { top: 0, bottom: 0, left: 1, right: 0 },
borderColor: "cyan",
borderColor: useColor ? "cyan" : undefined,
borderStyle: "round",
dimBorder: true,
dimBorder: useColor,
});
}
const RECOMMEND_LABELS = ["Best Pick", "Runner-Up", "Alternative"];
function renderCard(rec: RecommendedModel, index: number, colorize: ChalkInstance): string {
function renderCard(
rec: RecommendedModel,
index: number,
colorize: ChalkInstance,
useColor: boolean,
): string {
const labelColors = [colorize.green.bold, colorize.blue.bold, colorize.magenta.bold];
const colorFn = labelColors[index] ?? colorize.white.bold;
const label = RECOMMEND_LABELS[index] ?? `#${index + 1}`;
@@ -165,19 +174,21 @@ function renderCard(rec: RecommendedModel, index: number, colorize: ChalkInstanc
return boxen(lines.join("\n"), {
padding: { top: 0, bottom: 0, left: 1, right: 1 },
margin: { top: 0, bottom: 0, left: 1, right: 0 },
borderColor: "gray",
borderColor: useColor ? "gray" : undefined,
borderStyle: "round",
dimBorder: true,
dimBorder: useColor,
});
}
function formatSingleResult(results: RecommendedModel[], noColor: boolean): string {
const colorize = noColor ? new Chalk({ level: 0 }) : chalk;
return results.map((rec, idx) => renderCard(rec, idx, colorize)).join("\n");
function formatSingleResult(results: RecommendedModel[]): string {
const colorize = chalkFor(process.stdout);
const useColor = supportsColor(process.stdout);
return results.map((rec, idx) => renderCard(rec, idx, colorize, useColor)).join("\n");
}
function formatPipelineResult(summary: string, steps: PipelineStep[], noColor: boolean): string {
const colorize = noColor ? new Chalk({ level: 0 }) : chalk;
function formatPipelineResult(summary: string, steps: PipelineStep[]): string {
const colorize = chalkFor(process.stdout);
const useColor = supportsColor(process.stdout);
const lines: string[] = [];
lines.push(` ${colorize.yellow.bold("⚡ Pipeline")} ${summary}`);
@@ -192,17 +203,19 @@ function formatPipelineResult(summary: string, steps: PipelineStep[], noColor: b
}
lines.push("");
lines.push(recommendations.map((rec, idx) => renderCard(rec, idx, colorize)).join("\n"));
lines.push(
recommendations.map((rec, idx) => renderCard(rec, idx, colorize, useColor)).join("\n"),
);
}
return lines.join("\n");
}
function formatResult(result: RecommendResult, noColor: boolean): string {
function formatResult(result: RecommendResult): string {
if (result.type === "pipeline") {
return formatPipelineResult(result.summary, result.steps, noColor);
return formatPipelineResult(result.summary, result.steps);
}
return formatSingleResult(result.recommendations, noColor);
return formatSingleResult(result.recommendations);
}
function isEmptyResult(result: RecommendResult): boolean {
@@ -308,8 +321,8 @@ export default defineCommand({
return;
}
emitBare(formatIntentSummary(intent, settings.noColor));
emitBare(formatIntentSummary(intent));
emitBare("");
emitBare(formatResult(result, settings.noColor));
emitBare(formatResult(result));
},
});
+7 -7
View File
@@ -8,7 +8,7 @@ import {
type AppStreamChunk,
type AppCompletionResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { ansi, emitResult, emitBare } from "bailian-cli-runtime";
export default defineCommand({
description: "Call a Bailian application (agent or workflow)",
@@ -137,8 +137,7 @@ export default defineCommand({
let fullText = "";
let sessionId = "";
const writesStreamingStdout = format === "text";
const dim = settings.noColor ? "" : "\x1b[2m";
const reset = settings.noColor ? "" : "\x1b[0m";
const stderrColor = ansi(process.stderr);
for await (const event of parseSSE(res)) {
if (event.data === "[DONE]") break;
@@ -160,13 +159,14 @@ export default defineCommand({
// Show thoughts if available
if (chunk.output?.thoughts && flags.hasThoughts) {
for (const t of chunk.output.thoughts) {
if (t.thought) process.stderr.write(`${dim}[Thinking] ${t.thought}${reset}\n`);
if (t.thought)
process.stderr.write(`${stderrColor.dim(`[Thinking] ${t.thought}`)}\n`);
if (t.action_name)
process.stderr.write(
`${dim}[Action] ${t.action_name}: ${t.action_input || ""}${reset}\n`,
`${stderrColor.dim(`[Action] ${t.action_name}: ${t.action_input || ""}`)}\n`,
);
if (t.observation)
process.stderr.write(`${dim}[Observation] ${t.observation}${reset}\n`);
process.stderr.write(`${stderrColor.dim(`[Observation] ${t.observation}`)}\n`);
}
}
} catch {
@@ -176,7 +176,7 @@ export default defineCommand({
// Show session_id for multi-turn conversation
if (sessionId && !settings.quiet) {
process.stderr.write(`${dim}Session ID: ${sessionId}${reset}\n`);
process.stderr.write(`${stderrColor.dim(`Session ID: ${sessionId}`)}\n`);
}
if (format === "json") {
+10 -14
View File
@@ -4,7 +4,7 @@ import {
detectOutputFormat,
type Client,
} from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
import { ansi, emitResult } from "bailian-cli-runtime";
import { displayWidth, padEnd } from "bailian-cli-runtime";
const MODEL_LIST_API = "zeldaHttp.dashscopeModel./zelda/api/v1/modelCenter/listFoundationModels";
@@ -170,12 +170,8 @@ interface CheckRow {
tpmLimit: number;
}
function printTable(rows: CheckRow[], noColor: boolean): void {
const bold = noColor ? (t: string) => t : (t: string) => `\x1b[1m${t}\x1b[0m`;
const dim = noColor ? (t: string) => t : (t: string) => `\x1b[2m${t}\x1b[0m`;
const green = noColor ? (t: string) => t : (t: string) => `\x1b[32m${t}\x1b[0m`;
const yellow = noColor ? (t: string) => t : (t: string) => `\x1b[33m${t}\x1b[0m`;
const red = noColor ? (t: string) => t : (t: string) => `\x1b[31m${t}\x1b[0m`;
function printTable(rows: CheckRow[]): void {
const color = ansi(process.stdout);
const headers = ["Model", "RPM Usage/Limit", "TPM Usage/Limit", "Status"];
@@ -202,8 +198,8 @@ function printTable(rows: CheckRow[], noColor: boolean): void {
Math.max(displayWidth(label), ...tableRows.map((r) => displayWidth(r.cells[col]))),
);
const headerLine = headers.map((label, col) => bold(padEnd(label, widths[col]))).join(" ");
const separator = widths.map((w) => dim("─".repeat(w))).join("──");
const headerLine = headers.map((label, col) => color.bold(padEnd(label, widths[col]))).join(" ");
const separator = widths.map((w) => color.dim("─".repeat(w))).join("──");
process.stdout.write(headerLine + "\n");
process.stdout.write(separator + "\n");
@@ -212,16 +208,16 @@ function printTable(rows: CheckRow[], noColor: boolean): void {
for (const r of tableRows) {
const cells = r.cells.map((cell, col) => {
if (col === statusCol) {
if (cell === "Rate Limited") return red(padEnd(cell, widths[col]));
if (cell === "Near limit") return yellow(padEnd(cell, widths[col]));
if (cell === "Normal") return green(padEnd(cell, widths[col]));
if (cell === "Rate Limited") return color.red(padEnd(cell, widths[col]));
if (cell === "Near limit") return color.yellow(padEnd(cell, widths[col]));
if (cell === "Normal") return color.green(padEnd(cell, widths[col]));
}
return padEnd(cell, widths[col]);
});
process.stdout.write(cells.join(" ") + "\n");
}
process.stdout.write(dim(`\nTotal: ${rows.length} models`) + "\n");
process.stdout.write(color.dim(`\nTotal: ${rows.length} models`) + "\n");
}
export default defineCommand({
@@ -313,6 +309,6 @@ export default defineCommand({
return;
}
printTable(checkRows, settings.noColor);
printTable(checkRows);
},
});
@@ -1,5 +1,5 @@
import { defineCommand, detectOutputFormat, BailianError, ExitCode } from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
import { ansi, emitResult } from "bailian-cli-runtime";
import { displayWidth, padEnd } from "bailian-cli-runtime";
const HISTORY_API = "zeldaEasy.broadscope-platform.modelInstance.listModelLimitApplications";
@@ -53,9 +53,8 @@ function formatNumber(num: number): string {
return num.toLocaleString("en-US");
}
function printTable(records: LimitApplicationItem[], noColor: boolean, total: number): void {
const bold = noColor ? (t: string) => t : (t: string) => `\x1b[1m${t}\x1b[0m`;
const dim = noColor ? (t: string) => t : (t: string) => `\x1b[2m${t}\x1b[0m`;
function printTable(records: LimitApplicationItem[], total: number): void {
const color = ansi(process.stdout);
const headers = ["Model", "Token Limit", "Applied At"];
@@ -69,8 +68,8 @@ function printTable(records: LimitApplicationItem[], noColor: boolean, total: nu
Math.max(displayWidth(label), ...rows.map((row) => displayWidth(row[col]))),
);
const headerLine = headers.map((label, col) => bold(padEnd(label, widths[col]))).join(" ");
const separator = widths.map((w) => dim("─".repeat(w))).join("──");
const headerLine = headers.map((label, col) => color.bold(padEnd(label, widths[col]))).join(" ");
const separator = widths.map((w) => color.dim("─".repeat(w))).join("──");
process.stdout.write(headerLine + "\n");
process.stdout.write(separator + "\n");
@@ -79,7 +78,7 @@ function printTable(records: LimitApplicationItem[], noColor: boolean, total: nu
process.stdout.write(row.map((cell, col) => padEnd(cell, widths[col])).join(" ") + "\n");
}
process.stdout.write(dim(`\nTotal: ${total} records`) + "\n");
process.stdout.write(color.dim(`\nTotal: ${total} records`) + "\n");
}
export default defineCommand({
@@ -157,6 +156,6 @@ export default defineCommand({
return;
}
printTable(records, settings.noColor, modelFilter ? records.length : total);
printTable(records, modelFilter ? records.length : total);
},
});
+7 -8
View File
@@ -1,5 +1,5 @@
import { defineCommand, BailianError, detectOutputFormat, type Client } from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
import { ansi, emitResult } from "bailian-cli-runtime";
import { displayWidth, padEnd } from "bailian-cli-runtime";
const MODEL_LIST_API = "zeldaHttp.dashscopeModel./zelda/api/v1/modelCenter/listFoundationModels";
@@ -91,9 +91,8 @@ async function fetchAllModelsWithQpm(
return allModels;
}
function printTable(models: ModelWithQpm[], noColor: boolean): void {
const bold = noColor ? (t: string) => t : (t: string) => `\x1b[1m${t}\x1b[0m`;
const dim = noColor ? (t: string) => t : (t: string) => `\x1b[2m${t}\x1b[0m`;
function printTable(models: ModelWithQpm[]): void {
const color = ansi(process.stdout);
const headers = ["Model", "Req/min", "Token/min", "Max TPM"];
@@ -125,8 +124,8 @@ function printTable(models: ModelWithQpm[], noColor: boolean): void {
Math.max(displayWidth(label), ...rows.map((row) => displayWidth(row[col]))),
);
const headerLine = headers.map((label, col) => bold(padEnd(label, widths[col]))).join(" ");
const separator = widths.map((w) => dim("─".repeat(w))).join("──");
const headerLine = headers.map((label, col) => color.bold(padEnd(label, widths[col]))).join(" ");
const separator = widths.map((w) => color.dim("─".repeat(w))).join("──");
process.stdout.write(headerLine + "\n");
process.stdout.write(separator + "\n");
@@ -135,7 +134,7 @@ function printTable(models: ModelWithQpm[], noColor: boolean): void {
process.stdout.write(row.map((cell, col) => padEnd(cell, widths[col])).join(" ") + "\n");
}
process.stdout.write(dim(`\nTotal: ${models.length} models`) + "\n");
process.stdout.write(color.dim(`\nTotal: ${models.length} models`) + "\n");
}
export default defineCommand({
@@ -217,6 +216,6 @@ export default defineCommand({
return;
}
printTable(models, settings.noColor);
printTable(models);
},
});
+5 -6
View File
@@ -10,7 +10,7 @@ import {
type FlagsDef,
type ParsedFlags,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { ansi, emitResult, emitBare } from "bailian-cli-runtime";
import { readFileSync } from "fs";
const CHAT_FLAGS = {
@@ -180,12 +180,11 @@ export default defineCommand({
let textContent = "";
let inThinking = false;
const writesStreamingStdout = format === "text";
const dim = settings.noColor ? "" : "\x1b[2m";
const reset = settings.noColor ? "" : "\x1b[0m";
const isTTY = process.stdout.isTTY;
const statusOut =
format === "json" ? process.stderr : isTTY ? process.stdout : process.stderr;
const resultOut = process.stdout;
const statusColor = ansi(statusOut);
for await (const event of parseSSE(res)) {
if (event.data === "[DONE]") break;
@@ -199,7 +198,7 @@ export default defineCommand({
if (delta.reasoning_content) {
if (writesStreamingStdout && !inThinking) {
inThinking = true;
statusOut.write(`${dim}Thinking:\n`);
statusOut.write(statusColor.dim("Thinking:\n"));
}
if (writesStreamingStdout) statusOut.write(delta.reasoning_content);
}
@@ -207,7 +206,7 @@ export default defineCommand({
// Handle regular content
if (delta.content) {
if (writesStreamingStdout && inThinking) {
statusOut.write(`${reset}\n\nResponse:\n`);
statusOut.write(`${statusColor.reset}\n\nResponse:\n`);
inThinking = false;
}
textContent += delta.content;
@@ -218,7 +217,7 @@ export default defineCommand({
// Skip unparseable chunks
}
}
if (inThinking) statusOut.write(reset);
if (inThinking) statusOut.write(statusColor.reset);
if (format === "json") {
emitResult({ content: textContent }, format);
+12 -16
View File
@@ -2,7 +2,7 @@ import { execSync } from "child_process";
import { writeFileSync } from "fs";
import { join } from "path";
import { defineCommand, getConfigDir } from "bailian-cli-core";
import { fetchLatestVersion } from "bailian-cli-runtime";
import { ansi, fetchLatestVersion, type AnsiStyles } from "bailian-cli-runtime";
const SKILL_SOURCE = "modelstudioai/cli";
const SKILL_INSTALL_CMD = `npx skills add ${SKILL_SOURCE} --all -g -y`;
@@ -12,17 +12,16 @@ function detectInstallCommand(npmPackage: string): { cmd: string; label: string
return { cmd: `npm install -g ${npmPackage}@latest`, label: "npm" };
}
function updateAgentSkill(colors: { green: string; yellow: string; reset: string }): void {
const { green, yellow, reset } = colors;
function updateAgentSkill(color: AnsiStyles): void {
process.stderr.write("\nUpdating agent skill...\n");
try {
// Reinstall (not `skills update`) into ~/.agents/skills/ and sync to all agent apps.
// `--all` on `skills add` means --skill '*' --agent '*' -y (Cursor, Claude Code, etc.).
execSync(SKILL_INSTALL_CMD, { stdio: "inherit" });
process.stderr.write(`${green}\u2713 Agent skill updated.${reset}\n`);
process.stderr.write(`${color.green("\u2713 Agent skill updated.")}\n`);
} catch {
process.stderr.write(
`${yellow}Agent skill update skipped. Run manually: ${SKILL_INSTALL_CMD}${reset}\n`,
`${color.yellow(`Agent skill update skipped. Run manually: ${SKILL_INSTALL_CMD}`)}\n`,
);
}
}
@@ -36,25 +35,22 @@ export default defineCommand({
const npmPackage = identity.npmPackage;
const binName = identity.binName;
const currentVersion = identity.version;
const isTTY = process.stderr.isTTY;
const green = isTTY ? "\x1b[32m" : "";
const yellow = isTTY ? "\x1b[33m" : "";
const reset = isTTY ? "\x1b[0m" : "";
const color = ansi(process.stderr);
process.stderr.write(`Current version: ${yellow}${currentVersion}${reset}\n`);
process.stderr.write(`Current version: ${color.yellow(currentVersion)}\n`);
// Check latest version first
process.stderr.write("Checking for updates...\n");
const latest = await fetchLatestVersion(5000, npmPackage);
if (latest && latest === currentVersion) {
process.stderr.write(`${green}\u2713 Already up to date (${currentVersion}).${reset}\n`);
updateAgentSkill({ green, yellow, reset });
process.stderr.write(`${color.green(`\u2713 Already up to date (${currentVersion}).`)}\n`);
updateAgentSkill(color);
return;
}
if (latest) {
process.stderr.write(`Latest version: ${green}${latest}${reset}\n\n`);
process.stderr.write(`Latest version: ${color.green(latest)}\n\n`);
}
const { cmd, label } = detectInstallCommand(npmPackage);
@@ -68,7 +64,7 @@ export default defineCommand({
// `<bin> --version` outputs "<bin> X.Y.Z" — extract just the version number
const newVer = rawVer.replace(new RegExp(`^${binName}\\s+`), "");
process.stderr.write(
`\n${green}\u2713 Update complete: ${currentVersion} \u2192 ${newVer}${reset}\n`,
`\n${color.green(`\u2713 Update complete: ${currentVersion} \u2192 ${newVer}`)}\n`,
);
// Update the cached state so the post-run notification doesn't fire
try {
@@ -81,9 +77,9 @@ export default defineCommand({
/* ignore */
}
} catch {
process.stderr.write(`\n${green}\u2713 Update complete.${reset}\n`);
process.stderr.write(`\n${color.green("\u2713 Update complete.")}\n`);
}
updateAgentSkill({ green, yellow, reset });
updateAgentSkill(color);
} catch {
process.stderr.write("\nAutomatic update failed. Please run manually:\n");
process.stderr.write(` ${cmd}\n\n`);
+7 -12
View File
@@ -1,5 +1,5 @@
import { defineCommand, detectOutputFormat, fetchModelList, type Client } from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
import { ansi, emitResult } from "bailian-cli-runtime";
import { displayWidth, padEnd } from "bailian-cli-runtime";
const FREE_TIER_API = "zeldaEasy.broadscope-bailian.freeTrial.queryFreeTierQuota";
@@ -68,8 +68,8 @@ function printTable(
quotas: FreeTierQuota[],
stopMap: Map<string, boolean>,
typeMap: Map<string, string>,
noColor: boolean,
): void {
const color = ansi(process.stdout);
const headers = ["Model", "Type", "Remaining/Total", "Usage", "Expires", "Auto-Stop"];
const rows = quotas.map((quota) => {
@@ -97,14 +97,9 @@ function printTable(
Math.max(displayWidth(label), ...rows.map((row) => displayWidth(row[col]))),
);
const dim = noColor ? (text: string) => text : (text: string) => `\x1b[2m${text}\x1b[0m`;
const bold = noColor ? (text: string) => text : (text: string) => `\x1b[1m${text}\x1b[0m`;
const green = noColor ? (text: string) => text : (text: string) => `\x1b[32m${text}\x1b[0m`;
const yellow = noColor ? (text: string) => text : (text: string) => `\x1b[33m${text}\x1b[0m`;
const autoStopCol = headers.length - 1;
const headerLine = headers.map((label, col) => bold(padEnd(label, widths[col]))).join(" ");
const separator = widths.map((width) => dim("─".repeat(width))).join("──");
const headerLine = headers.map((label, col) => color.bold(padEnd(label, widths[col]))).join(" ");
const separator = widths.map((width) => color.dim("─".repeat(width))).join("──");
process.stdout.write(headerLine + "\n");
process.stdout.write(separator + "\n");
@@ -112,8 +107,8 @@ function printTable(
for (const row of rows) {
const cells = row.map((cell, col) => {
if (col === autoStopCol) {
if (cell === "ON") return green(padEnd(cell, widths[col]));
if (cell === "OFF") return yellow(padEnd(cell, widths[col]));
if (cell === "ON") return color.green(padEnd(cell, widths[col]));
if (cell === "OFF") return color.yellow(padEnd(cell, widths[col]));
}
return padEnd(cell, widths[col]);
});
@@ -333,6 +328,6 @@ export default defineCommand({
return;
}
printTable(quotas, stopMap, typeMap, settings.noColor);
printTable(quotas, stopMap, typeMap);
},
});
+11 -15
View File
@@ -6,7 +6,7 @@ import {
type Settings,
type Client,
} from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
import { ansi, emitResult } from "bailian-cli-runtime";
import { displayWidth, padEnd } from "bailian-cli-runtime";
const OVERVIEW_API = "zeldaEasy.bailian-telemetry.model.getModelUsageStatistic";
@@ -181,13 +181,11 @@ function printOverview(
startTime: number,
endTime: number,
days: number,
noColor: boolean,
): void {
const bold = noColor ? (text: string) => text : (text: string) => `\x1b[1m${text}\x1b[0m`;
const dim = noColor ? (text: string) => text : (text: string) => `\x1b[2m${text}\x1b[0m`;
const color = ansi(process.stdout);
process.stdout.write(
`${dim("Time Range Period:")} ${formatDate(startTime)} ~ ${formatDate(endTime)} ${dim(`(${days} days)`)}\n\n`,
`${color.dim("Time Range Period:")} ${formatDate(startTime)} ~ ${formatDate(endTime)} ${color.dim(`(${days} days)`)}\n\n`,
);
const rows: [string, string][] = [
@@ -203,7 +201,7 @@ function printOverview(
const maxLabel = Math.max(...rows.map(([label]) => displayWidth(label)));
for (const [label, value] of rows) {
process.stdout.write(`${bold(padEnd(label, maxLabel + 2))}${value}\n`);
process.stdout.write(`${color.bold(padEnd(label, maxLabel + 2))}${value}\n`);
}
}
@@ -212,13 +210,11 @@ function printModelTable(
startTime: number,
endTime: number,
days: number,
noColor: boolean,
): void {
const bold = noColor ? (text: string) => text : (text: string) => `\x1b[1m${text}\x1b[0m`;
const dim = noColor ? (text: string) => text : (text: string) => `\x1b[2m${text}\x1b[0m`;
const color = ansi(process.stdout);
process.stdout.write(
`${dim("Time Range Period:")} ${formatDate(startTime)} ~ ${formatDate(endTime)} ${dim(`(${days} days)`)}\n\n`,
`${color.dim("Time Range Period:")} ${formatDate(startTime)} ~ ${formatDate(endTime)} ${color.dim(`(${days} days)`)}\n\n`,
);
if (items.length === 0) {
@@ -263,8 +259,8 @@ function printModelTable(
Math.max(displayWidth(label), ...rows.map((row) => displayWidth(row[col]))),
);
const headerLine = headers.map((label, col) => bold(padEnd(label, widths[col]))).join(" ");
const separator = widths.map((width) => dim("─".repeat(width))).join("──");
const headerLine = headers.map((label, col) => color.bold(padEnd(label, widths[col]))).join(" ");
const separator = widths.map((width) => color.dim("─".repeat(width))).join("──");
process.stdout.write(headerLine + "\n");
process.stdout.write(separator + "\n");
@@ -274,7 +270,7 @@ function printModelTable(
process.stdout.write(cells.join(" ") + "\n");
}
process.stdout.write(dim(`\nTotal: ${items.length} models`) + "\n");
process.stdout.write(color.dim(`\nTotal: ${items.length} models`) + "\n");
}
export default defineCommand({
@@ -382,7 +378,7 @@ export default defineCommand({
return;
}
printModelTable(allItems, startTime, endTime, daysFlag, settings.noColor);
printModelTable(allItems, startTime, endTime, daysFlag);
} else {
const reqDTO: Record<string, unknown> = {
startTime,
@@ -438,7 +434,7 @@ export default defineCommand({
return;
}
printOverview(stat, startTime, endTime, daysFlag, settings.noColor);
printOverview(stat, startTime, endTime, daysFlag);
}
},
});
@@ -1,5 +1,5 @@
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
import { ansi, emitResult } from "bailian-cli-runtime";
import { displayWidth, padEnd } from "bailian-cli-runtime";
const LIST_WORKSPACES_API = "zeldaEasy.bailian-dash-workspace.space.listWorkspaces";
@@ -34,10 +34,8 @@ function extractResponseData(result: Record<string, unknown>): Record<string, un
return direct ?? data;
}
function printTable(workspaces: WorkspaceInfo[], noColor: boolean): void {
const bold = noColor ? (text: string) => text : (text: string) => `\x1b[1m${text}\x1b[0m`;
const dim = noColor ? (text: string) => text : (text: string) => `\x1b[2m${text}\x1b[0m`;
const green = noColor ? (text: string) => text : (text: string) => `\x1b[32m${text}\x1b[0m`;
function printTable(workspaces: WorkspaceInfo[]): void {
const color = ansi(process.stdout);
const headers = ["Name", "Workspace ID", "Default"];
@@ -51,21 +49,21 @@ function printTable(workspaces: WorkspaceInfo[], noColor: boolean): void {
Math.max(displayWidth(label), ...rows.map((row) => displayWidth(row[col]))),
);
const headerLine = headers.map((label, col) => bold(padEnd(label, widths[col]))).join(" ");
const separator = widths.map((width) => dim("─".repeat(width))).join("──");
const headerLine = headers.map((label, col) => color.bold(padEnd(label, widths[col]))).join(" ");
const separator = widths.map((width) => color.dim("─".repeat(width))).join("──");
process.stdout.write(headerLine + "\n");
process.stdout.write(separator + "\n");
for (const row of rows) {
const cells = row.map((cell, col) => {
if (col === 2 && cell === "Yes") return green(padEnd(cell, widths[col]));
if (col === 2 && cell === "Yes") return color.green(padEnd(cell, widths[col]));
return padEnd(cell, widths[col]);
});
process.stdout.write(cells.join(" ") + "\n");
}
process.stdout.write(dim(`\nTotal: ${workspaces.length}`) + "\n");
process.stdout.write(color.dim(`\nTotal: ${workspaces.length}`) + "\n");
}
export default defineCommand({
@@ -116,6 +114,6 @@ export default defineCommand({
return;
}
printTable(workspaces, settings.noColor);
printTable(workspaces);
},
});
-1
View File
@@ -75,7 +75,6 @@ export function buildSettings(s: ResolutionSources): Settings {
consoleSwitchAgent: flags.consoleSwitchAgent || file.console_switch_agent || undefined,
verbose: flags.verbose || env.DASHSCOPE_VERBOSE === "1",
quiet: flags.quiet || false,
noColor: flags.noColor || env.NO_COLOR !== undefined || !process.stdout.isTTY,
dryRun: flags.dryRun || false,
telemetry: env.DO_NOT_TRACK === "1" ? false : (file.telemetry ?? true),
};
-1
View File
@@ -123,7 +123,6 @@ export interface Settings {
consoleSwitchAgent?: number;
verbose: boolean;
quiet: boolean;
noColor: boolean;
dryRun: boolean;
telemetry: boolean;
}
-1
View File
@@ -10,7 +10,6 @@ const GLOBAL_FLAG_KEYS = new Set([
"quiet",
"verbose",
"timeout",
"noColor",
"dryRun",
"help",
"console",
-1
View File
@@ -70,7 +70,6 @@ export const GLOBAL_FLAGS = {
timeout: { type: "number", valueHint: "<seconds>", description: "Request timeout" },
quiet: { type: "switch", description: "Suppress non-essential output" },
verbose: { type: "switch", description: "Print HTTP request/response details" },
noColor: { type: "switch", description: "Disable ANSI colors" },
dryRun: { type: "switch", description: "Dry run mode" },
help: { type: "switch", description: "Show help" },
version: { type: "switch", description: "Print version" },
@@ -91,11 +91,6 @@ test("telemetry:DO_NOT_TRACK=1 一票否决 > file > 默认 true", () => {
expect(resolve({}).telemetry).toBe(true);
});
test("noColor:NO_COLOR 只看存在性(空串也算);非 TTY 下恒为 true", () => {
expect(resolve({ env: { NO_COLOR: "" } }).noColor).toBe(true);
if (!process.stdout.isTTY) expect(resolve({}).noColor).toBe(true);
});
test("apiKey 凭证:flag > env > file,source 字段随之;无 key 抛 AUTH", () => {
const all = src({
flags: { apiKey: "sk-flag" },
-1
View File
@@ -22,7 +22,6 @@ function testDeps(identity: Partial<Identity> = {}): { identity: Identity; setti
timeout: 30,
verbose: false,
quiet: true,
noColor: true,
dryRun: false,
telemetry: true,
},
+7
View File
@@ -30,6 +30,13 @@ export { createSpinner, createProgressBar } from "./output/progress.ts";
export { printWelcomeBanner, printQuickStart } from "./output/banner.ts";
export { maybeShowStatusBar } from "./output/status-bar.ts";
export { displayWidth, padEnd } from "./output/cjk-width.ts";
export {
ansi,
isTerminal,
supportsColor,
type AnsiStyles,
type TextStyle,
} from "./output/color.ts";
// Utility facilities consumed by commands
export { poll } from "./utils/polling.ts";
+4 -6
View File
@@ -19,6 +19,7 @@ import {
trackCommandExecution,
} from "bailian-cli-core";
import { maybeShowStatusBar } from "./output/status-bar.ts";
import { ansi } from "./output/color.ts";
import { checkForUpdate, getPendingUpdateNotification } from "./utils/update-checker.ts";
/**
@@ -116,14 +117,11 @@ export const versionCheckStage: Middleware = async (ctx, next) => {
const isUpdateCommand = ctx.path.length === 1 && ctx.path[0] === "update";
const newVersion = getPendingUpdateNotification();
if (newVersion && !ctx.settings.quiet && !isUpdateCommand) {
const isTTY = process.stderr.isTTY;
const yellow = isTTY ? "\x1b[33m" : "";
const cyan = isTTY ? "\x1b[36m" : "";
const reset = isTTY ? "\x1b[0m" : "";
const color = ansi(process.stderr);
process.stderr.write(
`\n ${yellow}Update available: ${ctx.identity.version} → ${newVersion}${reset}\n`,
`\n ${color.yellow(`Update available: ${ctx.identity.version} → ${newVersion}`)}\n`,
);
process.stderr.write(` Run ${cyan}${ctx.identity.binName} update${reset} to upgrade\n\n`);
process.stderr.write(` Run ${color.cyan(`${ctx.identity.binName} update`)} to upgrade\n\n`);
}
};
+5 -13
View File
@@ -1,4 +1,5 @@
import { API_KEY_PAGE } from "../urls.ts";
import { ansi } from "./color.ts";
const QUICK_START_TASKS = [
"Help me generate a set of Amazon e-commerce main images for baseball caps (white background + lifestyle shots + model wear shots)",
@@ -7,28 +8,19 @@ const QUICK_START_TASKS = [
"Help me analyze this video and write a Xiaohongshu-style post",
];
function colors() {
const isTTY = process.stderr.isTTY;
return {
purple: isTTY ? "\x1b[38;2;147;51;234m" : "",
dim: isTTY ? "\x1b[2m" : "",
reset: isTTY ? "\x1b[0m" : "",
};
}
export function printWelcomeBanner(cliName: string): void {
const { purple, reset } = colors();
process.stderr.write(`\n Welcome to ${purple}Bailian${reset} CLI!\n\n`);
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`);
}
export function printQuickStart(): void {
const { dim, reset } = colors();
const color = ansi(process.stderr);
process.stderr.write("\n🎯 Try these with your AI coding assistant:\n\n");
QUICK_START_TASKS.forEach((task, i) => {
process.stderr.write(`${dim}${i + 1}${reset} ${task}\n`);
process.stderr.write(`${color.dim(String(i + 1))} ${task}\n`);
});
process.stderr.write("\n");
}
+54
View File
@@ -0,0 +1,54 @@
export type TextStyle = (text: string) => string;
export interface AnsiStyles {
bold: TextStyle;
dim: TextStyle;
green: TextStyle;
yellow: TextStyle;
red: TextStyle;
cyan: TextStyle;
blue: TextStyle;
magenta: TextStyle;
white: TextStyle;
accent: TextStyle;
logo: TextStyle;
purple: TextStyle;
brandBlue: TextStyle;
keyPink: TextStyle;
reset: string;
}
const plain: TextStyle = (text) => text;
function wrap(enabled: boolean, code: string): TextStyle {
return enabled ? (text) => `\x1b[${code}m${text}\x1b[0m` : plain;
}
export function isTerminal(out: NodeJS.WriteStream): boolean {
return out.isTTY === true;
}
export function supportsColor(out: NodeJS.WriteStream): boolean {
return !("NO_COLOR" in process.env) && isTerminal(out);
}
export function ansi(out: NodeJS.WriteStream): AnsiStyles {
const enabled = supportsColor(out);
return {
bold: wrap(enabled, "1"),
dim: wrap(enabled, "2"),
green: wrap(enabled, "32"),
yellow: wrap(enabled, "33"),
red: wrap(enabled, "31"),
cyan: wrap(enabled, "36"),
blue: wrap(enabled, "34"),
magenta: wrap(enabled, "35"),
white: wrap(enabled, "37"),
accent: wrap(enabled, "38;2;59;130;246"),
logo: wrap(enabled, "38;2;97;92;237"),
purple: wrap(enabled, "38;2;147;51;234"),
brandBlue: wrap(enabled, "1;38;2;43;82;255"),
keyPink: wrap(enabled, "38;2;236;72;153"),
reset: enabled ? "\x1b[0m" : "",
};
}
+7 -11
View File
@@ -1,11 +1,6 @@
import { homedir } from "os";
import { maskToken, type Settings, type ApiKeyCredential } from "bailian-cli-core";
const reset = "\x1b[0m";
const dim = "\x1b[2m";
const bold = "\x1b[1m";
const mmBlue = "\x1b[38;2;43;82;255m";
const mmPink = "\x1b[38;2;236;72;153m";
import { ansi, isTerminal } from "./color.ts";
function tildePath(p: string): string {
return p.startsWith(homedir()) ? p.replace(homedir(), "~") : p;
@@ -16,16 +11,17 @@ export function maybeShowStatusBar(
token: string,
resolved: ApiKeyCredential,
): void {
if (settings.quiet || !process.stderr.isTTY) return;
if (settings.quiet || !isTerminal(process.stderr)) return;
const filePath = settings.configPath ? tildePath(settings.configPath) : "~/.bailian/config.json";
const authTag = `${resolved.source} · api-key`;
const maskedKey = maskToken(token);
const color = ansi(process.stderr);
process.stderr.write(
`${bold}${mmBlue}BAILIAN${reset} ` +
`${dim}${filePath}${reset} ` +
`${dim}|${reset} ` +
`${dim}Auth:${reset} ${mmPink}${maskedKey}${reset} ${dim}${authTag}${reset}\n`,
`${color.brandBlue("BAILIAN")} ` +
`${color.dim(filePath)} ` +
`${color.dim("|")} ` +
`${color.dim("Auth:")} ${color.keyPink(maskedKey)} ${color.dim(authTag)}\n`,
);
}
@@ -26,7 +26,6 @@ export function buildPipelineEnv(): PipelineEnv {
const settings: Settings = {
...buildSettings(sources),
output: "json",
noColor: true,
quiet: true,
};
const identity: Identity = {
+7 -12
View File
@@ -7,6 +7,7 @@ import {
credentialFlagDefs,
} from "bailian-cli-core";
import { camelToKebab } from "./args.ts";
import { ansi } from "./output/color.ts";
export type { Command, AnyCommand, FlagDef, FlagsDef } from "bailian-cli-core";
@@ -186,11 +187,10 @@ export class CommandRegistry {
return lines.map((l) => ` ${a(l.flag.padEnd(maxLen + 2))} ${d(l.desc)}`).join("\n");
}
// Color helpers — no-ops when output is not a TTY
private bold = (s: string, out: NodeJS.WriteStream) => (out.isTTY ? `\x1b[1m${s}\x1b[0m` : s);
private accent = (s: string, out: NodeJS.WriteStream) =>
out.isTTY ? `\x1b[38;2;59;130;246m${s}\x1b[0m` : s;
private dim = (s: string, out: NodeJS.WriteStream) => (out.isTTY ? `\x1b[2m${s}\x1b[0m` : s);
// Color helpers — no-ops when output is not a TTY.
private bold = (s: string, out: NodeJS.WriteStream) => ansi(out).bold(s);
private accent = (s: string, out: NodeJS.WriteStream) => ansi(out).accent(s);
private dim = (s: string, out: NodeJS.WriteStream) => ansi(out).dim(s);
printHelp(commandPath: string[], out: NodeJS.WriteStream = process.stdout): void {
if (commandPath.length === 0) {
@@ -254,16 +254,11 @@ ${d(` ${this.cliName} pipeline run workflow.yaml --dry-run --output json`)}
"██████╔╝██║ ██║██║███████╗██║██║ ██║██║ ╚████║",
"╚═════╝ ╚═╝ ╚═╝╚═╝╚══════╝╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝",
];
const PURPLE = "\x1b[38;2;97;92;237m";
const RESET = "\x1b[0m";
const color = ansi(out);
out.write("\n");
for (const line of LOGO) {
if (out.isTTY) {
out.write(`${PURPLE}${line}${RESET}\n`);
} else {
out.write(line + "\n");
}
out.write(`${color.logo(line)}\n`);
}
const b = (s: string) => this.bold(s, out);
+9
View File
@@ -34,6 +34,15 @@ Auto-generated from the CLI source at build time. Before running an unfamiliar c
Do not guess flags — use the reference files or `--help`.
### Color output
When an agent needs plain text without ANSI color codes (for parsing, logs, or
snapshots), run the command with `NO_COLOR=1`:
```bash
NO_COLOR=1 bl config show --output text
```
---
## When to use which command
-1
View File
@@ -92,7 +92,6 @@ Available on every command (in addition to command-specific flags):
| `--timeout <seconds>` | number | no | Request timeout |
| `--quiet` | switch | no | Suppress non-essential output |
| `--verbose` | switch | no | Print HTTP request/response details |
| `--no-color` | switch | no | Disable ANSI colors |
| `--dry-run` | switch | no | Dry run mode |
| `--help` | switch | no | Show help |
| `--version` | switch | no | Print version |