Skip to content

Commit 476dd3b

Browse files
committed
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
1 parent c35f285 commit 476dd3b

31 files changed

Lines changed: 237 additions & 249 deletions

File tree

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
>
77
> 实施(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` 无消费者未实现。
88
>
9-
> 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 命令内优先级删除。
9+
> 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 命令内优先级删除。
1010
>
1111
> 修订(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)。
1212
>
@@ -23,7 +23,7 @@
2323

2424
> **ctx 是唯一组合根。它在边界处把 flag / env / file / 默认 各源解析成 `identity / settings / credential`,交给命令。**
2525
>
26-
> 优先级链已**统一为 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 为业界标准,均非链序问题。
26+
> 优先级链已**统一为 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 为业界标准,均非链序问题。
2727
2828
三条硬规矩:
2929

@@ -70,7 +70,6 @@ export interface Settings {
7070
output: "text" | "json";
7171
outputDir?: string;
7272
timeout: number;
73-
concurrent?: number; // 命令经 getConcurrency 读 → 归 settings
7473
defaultTextModel?: string;
7574
defaultVideoModel?: string;
7675
defaultImageModel?: string;
@@ -82,11 +81,7 @@ export interface Settings {
8281
consoleSwitchAgent?: number;
8382
verbose: boolean;
8483
quiet: boolean;
85-
noColor: boolean;
86-
yes: boolean;
8784
dryRun: boolean;
88-
nonInteractive: boolean; // 0 消费者,可留可删;留着零风险
89-
async: boolean;
9085
telemetry: boolean;
9186
}
9287
```
@@ -236,11 +231,16 @@ export function describeAuth(s: ResolutionSources): AuthState; // auth status
236231

237232
```ts
238233
case "run": {
239-
// 1) 一次解析(全局+命令 flag 合并)
240-
const parsed = parseFlags(res.rest, { ...GLOBAL_FLAGS, ...res.command.flags });
234+
// 1) 一次解析(全局 + 凭证域 + 命令 flag 合并)
235+
const credDefs = credentialFlagDefs(res.command);
236+
const parsed = parseFlags(res.rest, {
237+
...GLOBAL_FLAGS,
238+
...credDefs,
239+
...res.command.flags,
240+
});
241241

242242
// 2) 分流(见下"分流规则"),validate 收收窄后的 ownFlags(与 §2 签名一致,别传 parsed)
243-
const globals = pick(parsed, Object.keys(GLOBAL_FLAGS)); // 全局 flag → sources
243+
const globals = pick(parsed, [...Object.keys(GLOBAL_FLAGS), ...Object.keys(credDefs)]); // 全局+凭证域 → sources
244244
const ownFlags = pick(parsed, Object.keys(res.command.flags ?? {})); // 命令声明的 → ctx.flags
245245
const invalid = res.command.validate?.(ownFlags);
246246
if (invalid) throw new UsageError(invalid);
@@ -262,13 +262,13 @@ case "run": {
262262
}
263263
```
264264

265-
**分流规则(重要,含"本次不改遮蔽"的妥协):**
265+
**分流规则(重要):**
266266

267-
> **全局 flag 恒进 `sources`(用 `Object.keys(GLOBAL_FLAGS)`);命令声明的 flag 进 `ctx.flags`(用 `Object.keys(command.flags)`)。同名遮蔽者两边都出现(受控重叠)**
267+
> **全局 flag + 当前命令可见的凭证域 flag 进 `sources`;命令自有 flag 进 `ctx.flags`**
268268
269-
为什么这样:约 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 不解析,无害)
269+
为什么这样:`MODEL_AUTH_FLAGS` / `CONSOLE_AUTH_FLAGS` 只按命令 `auth` 暴露,既保证 `--api-key` / `--console-region` 这类域 flag 能进入 credential/settings 解析链,也避免无关命令误收跨域 flag。历史同名遮蔽已清理,命令自有 flag 与全局/域 flag 不再受控重叠
270270

271-
**同名守卫**:registry 构建时断言 —— 命令 flag 与全局同名时,其 FlagDef `type` 必须一致。分流规则依赖"遮蔽都是同型重声明"这一假设;十行断言把口头约定变成机器约定,防止未来有人把 `async` 重声明成 value flag 后,错型值静默流进 sources
271+
**同名守卫**:registry 构建时断言 —— 命令自有 flag 与全局/凭证域 flag 同名即报错。分流规则依赖"同一 key 只归一个域"这一约束,防止未来新增 flag 时把同名值静默分到错误通道
272272

273273
`authStage`(`packages/runtime/src/middleware.ts`):
274274

@@ -292,17 +292,19 @@ const authStage = async (ctx, next) => {
292292

293293
## 6. 字段迁移对照(旧 `Config` → 去向)
294294

295-
|`Config` 字段 | 去向 |
296-
| ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
297-
| `clientName` / `clientVersion` | **Identity**(`clientName` / `version`) |
298-
| `binName` / `npmPackage` | **Identity** |
299-
| `apiKey` / `apiKeyEnv` / `fileApiKey` / `fileAccessToken` | **删除** → provider chain 从 sources 读 |
300-
| `baseUrl` | **ApiKeyCredential.baseUrl**(resolveApiKey 里解析) |
301-
| `consoleSite` / `consoleRegion` / `consoleSwitchAgent` | **Settings**(dry-run 展示)+ **ConsoleCredential**(真实调用),受控重叠 |
302-
| `workspaceId` | **Settings** |
303-
| `output` / `outputDir` / `timeout` / `default*Model` | **Settings** |
304-
| `verbose` / `quiet` / `noColor` / `dryRun` / `async` / `yes` / `telemetry` / `configPath` | **Settings** |
305-
| `concurrent`(原本仅 flags) | **Settings**(`getConcurrency` 改读 settings) |
295+
|`Config` 字段 | 去向 |
296+
| ----------------------------------------------------------- | -------------------------------------------------------------------- |
297+
| `clientName` / `clientVersion` | **Identity**(`clientName` / `version`) |
298+
| `binName` / `npmPackage` | **Identity** |
299+
| `apiKey` / `apiKeyEnv` / `fileApiKey` / `fileAccessToken` | **删除** → provider chain 从 sources 读 |
300+
| `baseUrl` | **ApiKeyCredential.baseUrl**(resolveApiKey 里解析) |
301+
| `consoleSite` / `consoleRegion` / `consoleSwitchAgent` | **Settings**(dry-run 展示)+ **ConsoleCredential**(真实调用),受控重叠 |
302+
| `workspaceId` | **Settings** |
303+
| `output` / `outputDir` / `timeout` / `default*Model` | **Settings** |
304+
| `verbose` / `quiet` / `dryRun` / `telemetry` / `configPath` | **Settings** |
305+
| `async` / `concurrent` | **命令自有 flag**(`ASYNC_FLAG` / `CONCURRENT_FLAG`) |
306+
| `yes` | **命令自有 flag**(`quota request`) |
307+
| `nonInteractive` | **删除** |
306308

307309
---
308310

@@ -314,10 +316,10 @@ const authStage = async (ctx, next) => {
314316
- `client/client.ts:38`:`apiCred?.baseUrl ?? config.baseUrl``apiCred.baseUrl`
315317
- 命令读 `config.binName`(约 6 处:`auth/status``usage/stats``mcp/list``quota/history``quota/request`)→ `identity.binName`(经 ctx)
316318
- **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)
317-
- **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 的第一优先级必须在命令里显式保住**,否则静默丢失
319+
- **workspaceId**:`usage/stats.ts``resolveWorkspaceId(config, flag)``requireWorkspaceId(ctx.settings, identity.binName)``--workspace-id` 已升入 `CONSOLE_AUTH_FLAGS`,进入 sources/settings,链为 flag > env > file。
318320
- **config/auth 命令**:`readConfigFile/writeConfigFile/resolver` 直接调用 → 走 `ctx.configStore()` / `ctx.authStore()`
319321
- **auth/login `validate`**:`!f.console && !f.apiKey`——`apiKey`/`baseUrl` 是它自己声明的 flag(`login.ts:15`),收窄后仍在 `ctx.flags`,**无需改**
320-
- **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
322+
- **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
321323
- 其余把 `Config` 当类型用的地方 → `Settings`;ctx 字段 `config``settings`(`ctx.config` 仅 2 处、解构 `const { config } = ctx` 约 46 处 + 其函数体内 `config.``settings.`)
322324

323325
**命名注意**:
@@ -344,13 +346,13 @@ const authStage = async (ctx, next) => {
344346

345347
## 9. 本次不做(已在钉钉文档记录,后续单独轮次)
346348

347-
- **flag 清理**:`nonInteractive` 删 / `async` ↔ 各命令 `--no-wait` 去重 / `yes` 收窄到命令级 / `noColor` 修一致性(registry/progress/banner 里内联 `process.stderr.isTTY` 绕过了 `config.noColor`)
349+
- ~~**flag 清理**~~ **已完成**:`nonInteractive` 删除;`async`/`concurrent` 改为命令级共享定义;`yes` 收窄为 `quota request` 自有;原颜色 CLI flag / Settings 字段删除,颜色由 `NO_COLOR` + 实际输出流 `isTTY` 共同决定,内联判断收束到 runtime helper
348350
- ~~workspaceId 的 flag 源~~ **已完成**(flag 边界轮):`--workspace-id` 升入 `CONSOLE_AUTH_FLAGS`,链为 flag > env > file;stats 删除自有声明与命令内优先级。(优先级链归一与同名遮蔽清理亦已完成:baseUrl 前置翻转见 §0,遮蔽经域化清零见 §5。)
349351
- **dry-run 输出规范统一**:各域输出现状不一致 —— model/app 域只打请求 body(不含 URL/baseUrl),console 域额外打 api 名 + region/site 路由信息。应一次定规范、跨域对齐(是否展示路由、展示哪些字段);届时若 console 域不再展示,console 三元组可从 Settings 撤出、收敛为纯 credential。**本次保持现状输出**(e2e 有断言)。
350352
- ~~全局↔命令私有 flag 同名遮蔽清理~~ **已完成**(flag 边界轮,经域化):16 处遮蔽全删,凭证 flag 按 `auth` 域可见,跨域报 Unknown flag,守卫升级为同名即抛。
351353
- **key ↔ baseUrl 强校验**(region 锁)。落点已就位:`resolveApiKey` 是唯一同时产出 `{token, baseUrl}` 的地方,校验加在它内部即可;baseUrl 不在 Settings,命令侧无法绕过绑定;`AuthStore.login` 已支持 `api_key` + `base_url` 成对落盘。
352354
- **多 profile / 多身份**(arkcli 式)。结构已留缝:单一 `ResolutionSources` 边界 + credential 封装。
353-
- **IOStreams 注入**(gh `Factory.IOStreams` / vercel `Client.stdout`);与 noColor 修复同属下一轮
355+
- **IOStreams 注入**(gh `Factory.IOStreams` / vercel `Client.stdout`);颜色 stream helper 已先行收束,完整 IOStreams 注入仍留后续轮次
354356
- `ConfigFile`(磁盘格式)不变;无用户可见 CLI 变化。
355357

356358
外部记录:钉钉文档 `https://alidocs.dingtalk.com/i/nodes/YMyQA2dXW7gYo6MzcZzzERNMWzlwrZgb`(全局 flag 对比、flag 清理项、遮蔽问题)。

packages/cli/tests/e2e/auth.e2e.test.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ describe("e2e: auth", () => {
6161
"json",
6262
"--timeout",
6363
"120",
64-
"--no-color",
6564
]);
6665
expect(exitCode, stderr).toBe(0);
6766
expect(stdout).toContain("Would validate and save API key.");
@@ -82,14 +81,8 @@ describe("e2e: auth", () => {
8281
expect(stderr).not.toContain("Cleared api_key");
8382
});
8483

85-
test("auth logout --dry-run --quiet --no-color", async () => {
86-
const { stdout, stderr, exitCode } = await runCli([
87-
"auth",
88-
"logout",
89-
"--dry-run",
90-
"--quiet",
91-
"--no-color",
92-
]);
84+
test("auth logout --dry-run --quiet", async () => {
85+
const { stdout, stderr, exitCode } = await runCli(["auth", "logout", "--dry-run", "--quiet"]);
9386
expect(exitCode, stderr).toBe(0);
9487
expect(stdout).toContain("No changes made.");
9588
});

packages/cli/tests/e2e/config.e2e.test.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,8 @@ describe("e2e: config", () => {
3838
expect(data.timeout).toBeDefined();
3939
});
4040

41-
test("config show --output text --no-color", async () => {
42-
const { stdout, stderr, exitCode } = await runCli([
43-
"config",
44-
"show",
45-
"--output",
46-
"text",
47-
"--no-color",
48-
]);
41+
test("config show --output text", async () => {
42+
const { stdout, stderr, exitCode } = await runCli(["config", "show", "--output", "text"]);
4943
expect(exitCode, stderr).toBe(0);
5044
expect(stdout).toMatch(/config_file|timeout|base_url/i);
5145
});

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

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => {
9696
});
9797

9898
test("quota list 文本输出包含英文表头", async () => {
99-
const { stdout, stderr, exitCode } = await runCli([
100-
"quota",
101-
"list",
102-
"--output",
103-
"text",
104-
"--no-color",
105-
]);
99+
const { stdout, stderr, exitCode } = await runCli(["quota", "list", "--output", "text"]);
106100
expect(exitCode, stderr).toBe(0);
107101
expect(stdout).toContain("Model");
108102
expect(stdout).toContain("Req/min");
@@ -118,7 +112,6 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => {
118112
"qwen3.6-plus",
119113
"--output",
120114
"text",
121-
"--no-color",
122115
]);
123116
expect(exitCode, stderr).toBe(0);
124117
expect(stdout).toContain("qwen3.6-plus");
@@ -254,13 +247,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => {
254247
});
255248

256249
test("quota check 文本输出包含英文表头", async () => {
257-
const { stdout, stderr, exitCode } = await runCli([
258-
"quota",
259-
"check",
260-
"--output",
261-
"text",
262-
"--no-color",
263-
]);
250+
const { stdout, stderr, exitCode } = await runCli(["quota", "check", "--output", "text"]);
264251
expect(exitCode, stderr).toBe(0);
265252
expect(stdout).toContain("Model");
266253
expect(stdout).toContain("RPM Usage/Limit");
@@ -276,7 +263,6 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => {
276263
"qwen3.6-plus",
277264
"--output",
278265
"text",
279-
"--no-color",
280266
]);
281267
expect(exitCode, stderr).toBe(0);
282268
expect(stdout).toContain("qwen3.6-plus");
@@ -291,7 +277,6 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => {
291277
"qwen3.6-plus,qwen-plus",
292278
"--output",
293279
"text",
294-
"--no-color",
295280
]);
296281
expect(exitCode, stderr).toBe(0);
297282
expect(stdout).toContain("qwen3.6-plus");
@@ -335,7 +320,6 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => {
335320
"qwen3.6-plus",
336321
"--output",
337322
"text",
338-
"--no-color",
339323
]);
340324
expect(exitCode, stderr).toBe(0);
341325
const hasStatus =

0 commit comments

Comments
 (0)