diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 743b073ff..392257e5d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,9 +9,13 @@ concurrency: group: ci-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} +permissions: + contents: read + actions: read + jobs: - check: - name: lint + typecheck + test + quality: + name: lint + typecheck + build runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 @@ -23,7 +27,32 @@ jobs: node-version: "22" cache: pnpm - # Keep native-fetch and compiled Binary tests on the release Bun version. + - run: pnpm install --frozen-lockfile + + - run: pnpm run sync:skill-assets + + - run: pnpm -r --filter "./packages/*" build + + - run: pnpm run check + + test: + name: test shard ${{ matrix.shard }}/4 + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + shard: [1, 2, 3, 4] + steps: + - uses: actions/checkout@v6 + + - uses: pnpm/action-setup@v6 + + - uses: actions/setup-node@v6 + with: + node-version: "22" + cache: pnpm + + # Every shard keeps the real Node/Bun/compiled-binary coverage. - uses: oven-sh/setup-bun@v2 with: bun-version: "1.3.14" @@ -34,6 +63,74 @@ jobs: - run: pnpm -r --filter "./packages/*" build - - run: pnpm run check + - name: Run test shard + run: >- + pnpm exec vp test --shard=${{ matrix.shard }}/4 + --reporter=default --reporter=blob --reporter=json + --outputFile.blob=.ci-test-results/blob/shard-${{ matrix.shard }}.json + --outputFile.json=.ci-test-results/json/shard-${{ matrix.shard }}.json - - run: pnpm test + - name: Upload shard reports + if: ${{ always() }} + uses: actions/upload-artifact@v7 + with: + name: test-shard-${{ matrix.shard }} + path: .ci-test-results/ + include-hidden-files: true + if-no-files-found: error + retention-days: 3 + + # Keep the existing required-check identity. Upstream failure/skip/cancellation + # must produce a failing check, rather than silently skipping the gate. + check: + name: lint + typecheck + test + if: ${{ always() }} + needs: [quality, test] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: pnpm/action-setup@v6 + + - uses: actions/setup-node@v6 + with: + node-version: "22" + cache: pnpm + + - run: pnpm install --frozen-lockfile + + - name: Download shard reports + id: download + uses: actions/download-artifact@v8 + with: + pattern: test-shard-* + path: .ci-test-results + merge-multiple: true + + # list --filesOnly discovers the full inventory, independently of sharding. + - name: Discover complete test inventory + run: pnpm exec vp test list --filesOnly --json=.ci-test-inventory.json + + - name: Verify complete shard coverage + if: ${{ always() }} + env: + QUALITY_RESULT: ${{ needs.quality.result }} + TEST_RESULT: ${{ needs.test.result }} + run: >- + node tools/ci/verify-shard-reports.mjs + --reports .ci-test-results --inventory .ci-test-inventory.json --shards 4 + + - name: Merge test reports + if: ${{ always() && steps.download.outcome == 'success' }} + run: >- + pnpm exec vp test --mergeReports=.ci-test-results/blob + --reporter=default --reporter=json --outputFile.json=.ci-test-summary.json + + - name: Upload merged report + if: ${{ always() }} + uses: actions/upload-artifact@v7 + with: + name: test-summary + path: .ci-test-summary.json + include-hidden-files: true + retention-days: 3 diff --git a/.gitignore b/.gitignore index 6d6b92634..5a1bad4d6 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,9 @@ tools/generated # Test & scene outputs /test/ +/.ci-test-results/ +/.ci-test-inventory.json +/.ci-test-summary.json packages/cli/scene/**/outputs/ # Environment variables (sensitive data) diff --git a/AGENTS.md b/AGENTS.md index e49c4da09..61f6b6519 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -61,8 +61,9 @@ Skill / 命令手册随 `skills/bailian-*/` 经 `bl skill init` 安装(装齐 | 场景 | 何时进入 | 详见 | | ----------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------- | | 命令增删改 | 增加 / 删除 / 重命名 `bl xxx` 或入口命令路径 | [docs/agents/command-add-remove.md](docs/agents/command-add-remove.md) | -| E2E 测试维护 | 新增/改命令或 e2e 用例、补 help/缺参/dry-run | [docs/agents/cli-e2e-tests.md](docs/agents/cli-e2e-tests.md) | +| 测试维护与性能 | 新增/修改任意单测、输出契约测试或 E2E;选择 runner、fixture、计时方式 | [docs/agents/cli-e2e-tests.md](docs/agents/cli-e2e-tests.md) | | 测试隔离 | 单测/E2E 受本地 HOME、语言、系统目录干扰;统一隔离基建 | [docs/agents/test-isolation.md](docs/agents/test-isolation.md) | +| 输出契约 | 新增输出、quiet/verbose、JSON 错误或流式输出 | [docs/agents/output-contract.md](docs/agents/output-contract.md) | | 批量压测 | 改/跑多能力并发压测、`test:stress`、fixtures | [docs/agents/stress-batch-tests.md](docs/agents/stress-batch-tests.md) | | 选项变更 | 给已有命令加 `--flag` 或改默认值 | [docs/agents/command-flag-change.md](docs/agents/command-flag-change.md) | | 模型上下架 | 增加新模型 / 改默认模型 / 废弃旧模型 | [docs/agents/model-add-remove.md](docs/agents/model-add-remove.md) | @@ -136,6 +137,8 @@ CLI 只为「自己能权威解释的错误」发出语义化信号,服务端的 ## 完成改动后的快速验证 +新增或修改任何 `**/tests/**` 用例(含 `tests/output`)前,必须读取[测试维护规范](docs/agents/cli-e2e-tests.md)。测试通过之外,还必须检查执行成本:按测试目标选择执行层,说明必要的子进程覆盖,并对新增/扩展套件提供耗时记录;性能优化必须提供同条件前后对比,不能靠删除必要覆盖或增加跳过项提速。具体执行方式、隔离与验收清单统一在该文档维护。 + ```sh vp check # format + lint + type check vp test # unit + e2e (真实集成需 API key / console token) diff --git a/README.md b/README.md index 315dc32af..0a1c88c12 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ Keep the returned IDs and recovery record. Closing the CLI or a later failure do ### RAG audio and video -`bl knowledge doc import --index-id --doc-id ` imports an existing file into a knowledge base; `doc list --details` shows per-file processing settings. Media support includes parser options, streaming uploads, and media references in search/chunk output. Chat JSON separates the final answer from phases, tools, docs, usage, and raw events; quiet mode emits only the final answer. See the [command guide](docs/knowledge/knowledge-cli-guide.md). +`bl knowledge doc import --index-id --doc-id ` imports an existing file into a knowledge base; `doc list --details` shows per-file processing settings. Media support includes parser options, streaming uploads, and media references in search/chunk output. Chat JSON separates the final answer from phases, tools, docs, usage, and raw events; quiet mode hides progress and informational messages while preserving the final result, including every JSON field. See the [command guide](docs/knowledge/knowledge-cli-guide.md). ## ParseX diff --git a/README.zh.md b/README.zh.md index 0d8bb0c89..a07c3898c 100644 --- a/README.zh.md +++ b/README.zh.md @@ -140,7 +140,7 @@ bl knowledge init --workspace-id --dry-run ### RAG 音视频接入 -`bl knowledge doc import --index-id --doc-id ` 将历史文件导入已有库;`doc list --details` 查看文件级处理参数。支持音视频 parser、流式上传,以及检索/切片中的媒体来源展示。问答 JSON 将最终 answer 与 phases/tools/docs/usage/events 分开保存,quiet 仅输出最终回答。参阅 [命令指南](docs/knowledge/knowledge-cli-guide.md)。 +`bl knowledge doc import --index-id --doc-id ` 将历史文件导入已有库;`doc list --details` 查看文件级处理参数。支持音视频 parser、流式上传,以及检索/切片中的媒体来源展示。问答 JSON 将最终 answer 与 phases/tools/docs/usage/events 分开保存,quiet 只隐藏进度和过程提示,保留最终结果及完整 JSON 字段。参阅 [命令指南](docs/knowledge/knowledge-cli-guide.md)。 ## ParseX diff --git a/docs/agents/cli-e2e-tests.md b/docs/agents/cli-e2e-tests.md index 10cb07f70..20a15155f 100644 --- a/docs/agents/cli-e2e-tests.md +++ b/docs/agents/cli-e2e-tests.md @@ -1,4 +1,4 @@ -# CLI E2E 测试规范 +# 测试维护与 CLI E2E 规范 ## 架构分层 @@ -15,6 +15,7 @@ ## 触发条件 +- 新增/修改任意包的 `**/tests/**`,包括单测、`tests/output`、runtime 和测试 helper;均须执行下方「执行方式与性能必查清单」,命令 E2E 另遵循本文其余章节 - 新增/修改 `packages/commands/src/commands` 下的 command 实现 - 新增/修改 `packages/cli/src/commands.ts` 的 `bl` 命令路径 map - 新建或扩展 `packages/commands/tests/e2e/.e2e.test.ts` @@ -30,7 +31,7 @@ - 路径:`packages/commands/tests/e2e/.e2e.test.ts`;knowledge 领域集中在 `packages/commands/tests/e2e/knowledge/` 子目录(新增 knowledge 命令测试放这里) - help:`runCommandHelp(routes, [...path, "--help"])` from `./helpers.ts`(当前 Vitest worker 内用真实 command + `CommandRegistry` 渲染,不启动子进程) -- 离线校验 / dry-run:确认用例不依赖进程初始化、真实请求或持久化后,可显式使用 `runCommandInProcess(routes, args, envOverrides)`;走真实 `createCli`、middleware 和 command,捕获 stdout / stderr / 退出码,执行后恢复进程环境。worker 内串行捕获,禁止与其他操作全局状态的并发用例混用 +- 离线 CLI 契约(校验 / dry-run / 本地模拟 HTTP):不依赖进程初始化或跨进程持久化的用例必须优先使用 `runCommandInProcess(routes, args, envOverrides, { cwd? })`;走真实 `createCli`、middleware 和 command,捕获 stdout / stderr / 退出码,执行后恢复进程环境。本地模拟 HTTP 仍走真实 SDK / client 和回环服务,不能仅因存在 HTTP 就逐用例启动子进程。worker 内串行捕获,禁止与其他操作全局状态的并发用例混用 - 子进程:`runCommandE2e(routes, args)` 保留给 live、实际写盘、进程隔离及 shell / stdio / env 契约;每个 topic 保留代表性子进程用例。`NODE_OPTIONS` / HTTP(S) 代理配置存在时,`runCommandInProcess` 自动回退子进程 - 路由加载:harness 按 command 库公开导出名加载 topic 所需模块,不整体求值命令 barrel;新增导出形式需扩展 loader 并保留与公开导出对象一致性的测试 - 更新检查:未发布的 harness 包 `latest` 查询固定返回本地 404,避免重复访问 npm;只拦截这个精确 URL,业务 fetch 和生产 updater 测试仍走原逻辑 @@ -52,7 +53,55 @@ - 解析 JSON stdout:`parseStdoutJson`;输出目录:`makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url))` - 长任务:`cliTimeoutPrefix()`;视频用例加 `test(..., 3_600_000)` 等显式超时 -## 双层 describe(固定结构) +## 执行方式与性能必查清单 + +### A. 按测试目标选择执行层 + +| 测试目标 | 必须采用的方式 | +| ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| 纯函数、独立模块行为 | 直接调用被测模块,不为单测启动完整 CLI | +| 全量 help / 命令路径发现 | registry + `runCommandHelp`;产品身份和代表性入口另保留子进程 | +| CLI 参数、输出格式矩阵、普通 dry-run、本地模拟 HTTP 契约 | 使用已有隔离的进程内 runner;commands 用 `runCommandInProcess`,runtime 用 `e2e/in-process` + 真实入口逻辑 | +| live、实际持久化及跨进程恢复、启动时 env / preload / proxy、TTY / 管道 / 信号 / 进程退出 | 保留真实子进程,并在用例或 helper 附近说明所验证的进程边界 | + +- [ ] 每个 topic 保留与其风险对应的代表性子进程覆盖;不得把格式/参数矩阵逐项乘上 CLI 启动成本。迁移 runner 时,对代表性成功、错误或确认分支核对 stdout、stderr 和退出码;确定性 fixture 可直接逐字节比较 +- [ ] CLI 进程内契约测试必须复用 `e2e/isolation` 和 `e2e/in-process`,不得为此自写 stdout/stderr、env、cwd 捕获器。涉及项目或相对文件路径时显式传 `{ cwd: fixtureDirectory }`;修改 helper 时验证直接执行和自动子进程回退均保留 cwd,调用后恢复环境 +- [ ] 直接调用模块的单测无需套用 CLI runner,可以注入 reporter/输出流并监测意外的全局写入;需要 HOME/env 隔离时按 [test-isolation.md](test-isolation.md) 分层选择。core 单测使用自身的 `tests/helpers/isolation.ts`,不得反向依赖 `e2e` +- [ ] 本地模拟 HTTP 继续断言实际请求的方法、路径、查询、鉴权头、body 和响应;静态命令的确认前分支与静态 dry-run 证明零请求/零写入;带 `prepare` 的命令在确认前或 dry-run 中可先鉴权并读取远端计划,须断言仅执行计划所需的只读请求,且零远端/本地写入。进程内执行不能替换掉这些协议断言 +- [ ] 离线 fixture 显式固定会影响结果的环境变量(如输出开关、凭证、代理、preload),不继承本机状态;启动环境专项测试仍显式注入并走子进程,不能通过清空变量绕过被测行为。完整隔离要求见 [test-isolation.md](test-isolation.md) + +### B. 控制等待与 fixture 成本 + +- [ ] 轮询、重试、debounce 的普通逻辑测试使用受控时间推进,覆盖等待前后边界、请求次数、终态和必要清理;不得在矩阵内累计真实 sleep。不能把被测轮询函数整体 mock 掉;真实超时中止、信号和退出场景另保留真实计时/子进程覆盖 +- [ ] 仅为需要的用例创建 SDK 项目、build、版本和文件树等重型 fixture;输出展示或参数校验不得无条件执行无关初始化 +- [ ] 可共享不可变声明,不能为提速跨用例共享可写目录、State、服务端请求记录或未恢复的全局状态;服务器、连接、定时器和监听器必须清理 +- [ ] 性能优化保留必要的成功、失败、权限/确认、边界和副作用断言;不得通过放宽断言、提高超时、改成 gated/skip 或删除必要覆盖获得提速。确需合并重复用例时必须说明覆盖映射,不以用例总数替代审查 + +### C. 提交前提供耗时证据 + +- [ ] 新增/扩展套件至少运行一次并记录通过/跳过数量、套件耗时、总墙钟、Node 版本与 worker 数;修改既有套件须有同条件基线。结果写入 PR/交付说明,不新增依赖本机速度的断言 +- [ ] 性能优化或发现明显耗时增长时,前后各连续运行三轮,报告原始值和中位数;同机器、Node、worker、门控及 fixture 条件,不与其他重型任务并跑。并行套件耗时之和不能冒充总墙钟 +- [ ] 耗时明显增长时,定位新增的子进程、真实等待和重型初始化;说明必要成本,消除无关成本后再交付。修改共享 helper 时回归现有调用方及其回退路径 +- [ ] 保留/新增哪些覆盖、哪些仍用子进程、哪些因环境未执行,必须在交付中说明;缺凭证跳过和初始化失败不能当作通过 + +定向计时示例(报告路径使用仓库外目录;该命令本身不会自动比较基线或阻止性能回退): + +```sh +pnpm exec vp test packages/commands/tests/e2e/managed-agent-webhook.e2e.test.ts \ + --maxWorkers=2 --reporter=json --outputFile=/tmp/managed-webhook-before-1.json +``` + +需要三轮对比时为每轮分别命名 `before-1/2/3` 和 `after-1/2/3`,从报告读取套件耗时与总墙钟;worker 数可按环境选择,但前后必须一致。 + +### D. CI 分片与汇总门槛 + +- [ ] `.github/workflows/ci.yml` 使用 4 个独立 runner 的原生 `--shard=N/4`,保留完整测试范围、默认文件隔离、既有门控及每片相同的 Node/Bun 版本;不能用新增 skip、文件过滤或同一 checkout 多进程共享全局状态替代分片 +- [ ] 质量检查与测试矩阵并行;矩阵 `fail-fast: false`,失败时仍上传已有报告。每片输出独立命名的 blob + JSON 报告,保留 3 天 +- [ ] `jobs.check` 和名称 `lint + typecheck + test` 保持现有 required check 身份;汇总 job 用 `always()` 执行,并显式要求质量检查和整个测试矩阵均成功,不能让失败、取消或整个 job 跳过变成绿灯 +- [ ] `tools/ci/verify-shard-reports.mjs` 在合并前核对 4 片报告完整,动态发现的全部测试文件恰好出现一次,报告及用例没有失败;缺报告、漏跑、重复或元数据不一致必须使门槛失败。不要硬编码文件数或用例数 +- [ ] 用 `--mergeReports` 汇总 blob 报告;即使校验失败仍尽可能合并已有报告以便诊断,合并成功不能覆盖前面的失败结果。调整该链路时验证成功路径与失败/取消/跳过、缺报告、漏跑和重复路径 + +## 离线与真实集成分组 ```ts // 1) 不 skip:进程内 --help,无密钥、无真实 API(分组 help 由 bl registry.smoke 覆盖) @@ -63,10 +112,14 @@ describe("e2e: ", () => { }); }); -// 2) skipIf:缺参 / dry-run / 真实集成 -describe.skipIf()("e2e: (DashScope …)", () => { +// 2) 不 skip:无密钥的离线校验、dry-run、本地模拟 HTTP 契约 +describe("e2e: offline contracts", () => { test("缺少 -- 时退出为用法错误 (2)", ...); test(" --dry-run ...", ...); // 若适用 +}); + +// 3) skipIf:需要真实凭证/服务的集成 +describe.skipIf()("e2e: (DashScope …)", () => { test("【model】真实流程", ..., LONG_TIMEOUT); }); ``` @@ -129,9 +182,10 @@ describe.skipIf()("e2e: (DashScope …)", () => { - [ ] `packages/commands/tests/e2e/.e2e.test.ts`(新建或扩展) - [ ] 若改了 `usageArgs` / `flags` / `exampleArgs`,跑 `pnpm --filter bailian-cli run generate:reference` 更新各 `skills//reference/` 并提交 - [ ] 子命令 `--help` 使用 `runCommandHelp`(分组 help 由 bl `registry.smoke` 覆盖) -- [ ] skip 块:每个 required flag 缺参;可 dry-run 则加一条 +- [ ] 离线块:每个 required flag 缺参;可 dry-run 则加一条;仅需要真实服务的用例放进 skip 块 - [ ] 至少一条真实集成(或说明为何仅 smoke);不破坏已有集成用例顺序 - [ ] `vp test packages/commands/tests/e2e/` 通过 +- [ ] 完成「执行方式与性能必查清单」并提供覆盖与耗时记录 ## 调试命令 @@ -145,16 +199,17 @@ pnpm --filter bailian-cli-runtime exec vp test packages/runtime/tests/proxy.e2e. ## 示例片段 ```ts +import { parseStdoutJson, runCommandInProcess } from "./helpers.ts"; import { FOO_ROUTES } from "./topic-routes.ts"; test("foo bar 缺少 --prompt 时退出为用法错误 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e(FOO_ROUTES, ["foo", "bar", "--quiet"]); + const { stderr, exitCode } = await runCommandInProcess(FOO_ROUTES, ["foo", "bar", "--quiet"]); expect(exitCode).toBe(2); expect(stderr).toMatch(/--prompt|Missing required argument/i); }); test("foo bar --dry-run 仅输出计划", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(FOO_ROUTES, [ + const { stdout, stderr, exitCode } = await runCommandInProcess(FOO_ROUTES, [ "foo", "bar", "--dry-run", diff --git a/docs/agents/command-pack.md b/docs/agents/command-pack.md index 57cb2b271..f5c2ac8a9 100644 --- a/docs/agents/command-pack.md +++ b/docs/agents/command-pack.md @@ -39,6 +39,14 @@ - [ ] 单包失败必须 fail-open:保留内置命令和其他合法 pack。 - [ ] 破坏协议前优先在适配层兼容;确实无法兼容时才提升 `apiVersion`。 +## 输出边界 + +- 管理命令通过本次 `ctx.output.result` 提交完整结果;npm 日志由显式注入的 `CommandOutput` 分帧,普通日志走 info,npm warning/error 行走 warning,JSON stderr 不直接写入子进程原始文本。 +- API 1 继续支持 `result/line/write`。适配层仅在当前 invocation 内收集输出:显式 result 的数据保持原样,line/write 是其 text 呈现;只调用 line/write 的命令在 JSON 下返回收集后的字符串,不增加 envelope。 +- 多次提交 result 是包的协议错误(GENERAL),不输出部分文档;一次显式提交的报告在后续异常时仍先输出,再由 runtime 处理原始异常。 +- text 仍按原顺序呈现片段;统一 result writer 会为末尾没有换行的文本补一个 LF。包直接调用 process/console 的输出不属于 host API,不做进程级拦截。 +- Pipeline 的普通 run/validate 输出一次最终报告,失败报告后抛错。显式 `--output text --events jsonl` 保留原始生命周期事件协议;有效 JSON 输出(含 env/Profile)与 `--events jsonl` 冲突时,在读取工作流文件及执行前拒绝。 + ## 测试与文档 - [ ] `packages/runtime/tests/command-packs.test.ts` 覆盖产品 policy、安装目录隔离、协议版本、前缀和导出契约。 diff --git a/docs/agents/config-add.md b/docs/agents/config-add.md index c8d688705..1600b33d7 100644 --- a/docs/agents/config-add.md +++ b/docs/agents/config-add.md @@ -36,6 +36,7 @@ config 文件 ─┘ - [ ] `packages/core/src/config/loader.ts`: - `buildSources()` 如需新增来源,把 flag/file/env 纳入 sources - `buildSettings()` 加新字段的合并逻辑(`flags.x ?? process.env.XXX ?? file.x ?? default`) + - 开启型 switch(如 `quiet`)未传时解析为 `false`,环境变量回退用 `flags.quiet || env.DASHSCOPE_QUIET === "1"`,不要用 `??` 将默认 `false` 当作显式关闭 - 校验(数值范围、枚举合法性等) - 校验失败抛 `BailianError(USAGE)` diff --git a/docs/agents/config-profile-change.md b/docs/agents/config-profile-change.md index 836ad584a..b7c661fd5 100644 --- a/docs/agents/config-profile-change.md +++ b/docs/agents/config-profile-change.md @@ -50,7 +50,7 @@ - `config list` 标识所有 Profile 与当前激活项。 - `config show`、`auth status` 只输出本次最终选择的 `config` 和 `config_file`,不重复携带激活状态。 - `config show` 额外展示 `DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL`(有值才输出,key 脱敏);JSON 字段值保持干净,另用 `active: { api_key?, base_url? }` 标生效源;text 模式才在生效层值后追加 `(active)` / `(active)`。`base_url` 展示文件层或内置默认,不再被 client 生效值覆盖来源语义。 -- 模型域 `api_key` 与显式 `base_url` 来自不同优先级层级时,仅 `auth: "apiKey"` 命令在 `authStage` 向 stderr 告警一次(`--quiet` 抑制;`--output json` 为 `API_KEY_BASE_URL_SOURCE_MISMATCH`);同源或仅一端显式配置不告。排查建议指向 `{binName} config show`。 +- 模型域 `api_key` 与显式 `base_url` 来自不同优先级层级时,仅 `auth: "apiKey"` 命令在 `authStage` 向 stderr 告警一次(`--quiet` 保留;`--output json` 为单行结构化 `API_KEY_BASE_URL_SOURCE_MISMATCH` warning);同源或仅一端显式配置不告。排查建议指向 `{binName} config show`。 - `config ui` 从持久化元数据读取激活项,提供显式激活操作,并在删除激活项后刷新为 `default`。 - `config ui` 展示并可编辑完整 `ConfigFile`(含 `console_*`、`telemetry`),保存时按类型(数字/布尔/枚举)归一化写回;`config set` 仍只暴露较窄的 `VALID_KEYS`。UI 未管理的顶层元数据(如 `active_config`)不进入 Profile block,仍由写盘逻辑单独保留。 - `config ui` 只读展示本地 agent 生态:Skills 跨全部 agent skill 目录(`~/.agents/skills` 及各 agent 的 `skills/`,含软链接)按 id 聚合并标注安装来源;MCP、Agents 从各 agent 本地配置读取。 @@ -73,7 +73,7 @@ - Console token 自动刷新不从其他 Profile 借用 AK/SK,也不把新 token 写入其他 Profile。 - Console/OpenAPI/none 命令不参与 API Key capability fallback;fallback 后的 Client 特殊端点行为必须跟随最终解析的 `base_url`,不能根据原 `settings.configName` 推断端点类型。 - Fallback 反馈只描述 CLI 能权威确认的本地行为:当前 Profile 不支持空格分隔的用户可见叶子命令,本次将从 `default` 读取 API Key 配置;不得声称整个 Profile 已切换,也不得展示 capability ID 或 `` 等内部值。存在 `--api-key` / `--base-url` 或 `DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` 任一显式覆盖时必须跳过 fallback 且不输出反馈。 -- Fallback 反馈写 stderr,`--quiet` 抑制;text 模式输出本地化句子,`--output json` 输出两空格缩进的多行 `warning` 对象。dry-run 和后续鉴权失败仍保留反馈;JSON 模式下多个 diagnostics 以空行分隔,任何模式都不得输出凭证值。 +- Fallback 作为 warning 写 stderr,`--quiet` 下仍保留;text 模式输出本地化警告,`--output json` 每条输出一个单行 `warning` 对象(JSONL),记录之间不插入空行。dry-run 和后续鉴权失败仍保留反馈;stderr 与 stdout 的最终业务结果分开解析,任何模式都不得输出凭证值。 - 跨源告警覆盖:env key + file base_url、flag/env 与另一层组合;同源与「仅 key + 默认 base_url」不告;quiet / json 形态与 fallback 一致。 - `config list/show/use/ui`、`auth status` 和依赖默认模型的消费命令覆盖对应 E2E;`config show` 覆盖 env 脱敏与 active 标记。 - `config ui` 覆盖保存时保留顶层元数据(如 `active_config`),继续允许空值清除字段,并覆盖 `console_*`/`telemetry` 的类型归一化与枚举校验。 diff --git a/docs/agents/error-hint-change.md b/docs/agents/error-hint-change.md index 31c138f31..dfd423eb2 100644 --- a/docs/agents/error-hint-change.md +++ b/docs/agents/error-hint-change.md @@ -3,7 +3,7 @@ ## 触发条件 - 修改 `BailianError` 的 message 或 hint -- 调整 runtime 的 hint 增强逻辑(`enhanceHint`) +- 调整 runtime 的 hint 增强逻辑(`errorHint`) - 改 auth stage / resolver 的鉴权失败文案 - 改任何抛错位置的分类(exitCode) @@ -39,14 +39,15 @@ ``` core 抛出 BailianError(message, exitCode, hint, cause?) ↓ 沿调用栈冒泡 -runtime/create-cli.ts: dispatch().catch(handleError) +runtime/create-cli.ts: 每次调用创建 reporter,统一捕获初始化/解析/业务异常 ↓ runtime/error-handler.ts: - 服务端错误(BailianError(GENERAL)) → text 直接打 message - - 内部 AUTH/USAGE/NETWORK/TIMEOUT → 走 enhanceHint(只 AUTH 还有增强) + - 内部 AUTH/USAGE/NETWORK/TIMEOUT → 走 errorHint(只 AUTH 还有增强) - TypeError("fetch failed") → 读 err.cause.code 翻成 NETWORK - Node fs errno → 翻成 GENERAL + errno hint - - `--output json` → stderr 输出两空格缩进的多行 error JSON;前面已有结构化 warning 时,两个 diagnostics 以空行分隔 + - reporter:JSON 错误、warning、debug 各占 stderr 一行,无空行;stdout 仅保留最终业务结果 + - 早期错误使用只读 CLI/env/Profile 格式来源,不在渲染时重新初始化配置 - 其它 Error → 默认走 cause 链 ↓ process.exit(err.exitCode) @@ -72,11 +73,11 @@ process.exit(err.exitCode) - ❌ 新增/改动时不写控制台 URL 或 region - ❌ 新增/改动时不写渠道追踪参数(`source_channel=xxx`) - ✅ 只描述抽象做法(如 `"Set DASHSCOPE_API_KEY environment variable, or pass --api-key."`) -- 当前遗留:`packages/core/src/auth/resolver.ts` 仍含 `bl auth login` hint;触碰鉴权错误时迁到 runtime `enhanceHint` +- 当前遗留:`packages/core/src/auth/resolver.ts` 仍含 `bl auth login` hint;触碰鉴权错误时迁到 runtime `errorHint` ### 4. runtime / 产品层可以使用入口名 + URL -- `packages/runtime/src/error-handler.ts` 通过 `binName` 渲染 `bl` / `kscli` 等入口名,不要硬编码 +- `packages/runtime/src/error-handler.ts` 通过本次调用的 `binName` 渲染 `bl` / `kscli` 等入口名,不要硬编码 - 产品入口 / README / E2E 可以写具体入口命令 - shared command 实现不写 `bl` / `kscli` 前缀;`usageArgs` / `exampleArgs` 只写参数片段 - URL 必须从 `packages/runtime/src/urls.ts` import,不能硬编码 @@ -89,9 +90,9 @@ process.exit(err.exitCode) - [ ] `packages/core/src/auth/resolver.ts` 新增/改 throw 语句时:hint 不含 cli 关切;已有 `bl auth login` 遗留点被触碰时要收敛 - [ ] 任何 core 文件新增/改 BailianError:同上 -### B. runtime 增强(`enhanceHint`) +### B. runtime 增强(`errorHint`) -- [ ] `packages/runtime/src/error-handler.ts:enhanceHint`:**当前只为 internal AUTH 增强**(因为 resolver / authStage 等内部位置会发 AUTH) +- [ ] `packages/runtime/src/error-handler.ts:errorHint`:**当前只为 internal AUTH 增强**(因为 resolver / authStage 等内部位置会发 AUTH) - [ ] 命令名使用 `binName`,不要硬编码 `bl` - [ ] URL 必须是 `import { API_KEY_PAGE } from "./urls.ts"` diff --git a/docs/agents/output-contract.md b/docs/agents/output-contract.md new file mode 100644 index 000000000..aab563b4a --- /dev/null +++ b/docs/agents/output-contract.md @@ -0,0 +1,108 @@ +# 输出契约 + +## 触发条件 + +- 新增结果、进度、提示、警告或诊断输出 +- 修改 output / quiet / verbose 行为 +- 修改错误出口、流式或二进制输出 + +## 输出约定 + +普通业务命令及其 SDK、传输、轮询、更新和 Command Pack 调用链统一使用本次调用的 `ctx.output`。不再使用全局 `emitResult/emitBare`,不自行创建 reporter。原始流和原生交互仅限下面列出的边界。 + +```ts +ctx.output.result(data, { text }); // 最终业务结果;text 可选,只控制文本呈现 +ctx.output.progress(message); // 进度 +ctx.output.info(message); // 过程提示 +ctx.output.warning(warning); // 需要关注的问题 +ctx.output.debug(details); // verbose 诊断 +``` + +终止失败直接抛出,由 runtime 统一渲染;`CommandOutput` 不提供 `error()`。包含失败项的最终报告先 result,再按原有契约抛错。 + +| 信息 | text 默认 | text + quiet | text + verbose | text + quiet + verbose | +| ------------------- | --------------- | ------------ | --------------- | ---------------------- | +| result | stdout 文本结果 | 相同结果 | 相同结果 | 相同结果 | +| progress / info | stderr 文本 | 隐藏 | stderr 文本 | 隐藏 | +| warning | stderr 文本警告 | 保留 | 保留 | 保留 | +| debug | 隐藏 | 隐藏 | stderr 文本诊断 | stderr 文本诊断 | +| 终止错误(runtime) | stderr 文本错误 | 保留 | 保留 | 保留 | + +| 信息 | JSON 默认 | JSON + quiet | JSON + verbose | JSON + quiet + verbose | +| ------------------- | --------------------- | ------------ | ---------------- | ---------------------- | +| result | stdout 完整 JSON 文档 | 相同 JSON | 相同 JSON | 相同 JSON | +| progress / info | 隐藏 | 隐藏 | 隐藏 | 隐藏 | +| warning | stderr 单行 JSON | 保留 | 保留 | 保留 | +| debug | 隐藏 | 隐藏 | stderr 单行 JSON | stderr 单行 JSON | +| 终止错误(runtime) | stderr 单行 JSON | 保留 | 保留 | 保留 | + +stdout 与 stderr 分别解析:stdout 是一个 JSON 文档;stderr 的每个非空行是一条独立 JSON 记录(JSONL)。不要把两个通道拼成一个 JSON 文档。quiet 只减少过程提示,不选择数据字段、不隐藏警告或错误、不关闭 verbose。 + +### 配置与默认值 + +- 沿用现有 output / verbose 配置;quiet 增加 `DASHSCOPE_QUIET=1`,不增加持久化字段或反向 switch。未传 `--quiet` 的解析值 false 不是显式关闭;按开启型 switch 与环境变量共同决定。 +- 命令 `defaultOutput` 仅在 flag / 环境变量 / Profile 都未选择格式时生效;既有 telemetry 的格式强制规则由 `defaultOutputOverridesProfile` 单独声明。 +- 原生 TTY 专用默认值通过 `defaultInteractiveOutput` 声明;已有 `--trace` 等诊断开关通过 `verboseFlags` 在同一个 reporter 创建前解析,不能另建 reporter。 + +### 明确边界 + +| 入口 | 保留的行为 | JSON 约束 | +| ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| text chat / app call / text omni / knowledge chat / Managed Agent 会话 | text 回答流可逐段写 stdout;最终仍提交一次 result;过程提示受 quiet 控制 | 收集并提交一次完整结果,不写文本 token;会话原有事件脱敏投影保留 | +| speech synthesize | text 且没有 `--out` 时允许原始音频 stdout | 流式 JSON 要求 `--out`,副作用前校验;dry-run 保留预览 | +| pipeline run `--events jsonl` | 显式 `--output text` 保留既有生命周期 JSONL 流 | 与有效 JSON 格式冲突时,在读取/执行工作流前报用法错误 | +| 浏览器登录 / 终端问答 / 本地 UI / Workbench | text 原生交互;登录 URL、问题和 Workbench 子进程使用其原生通道;Workbench ready/reuse URL 是最终结果 | 无法满足普通结果契约的原生模式在浏览器、服务器、子进程或远程副作用前拒绝,提示 text 或既有非交互方式;dry-run 保留结构预览 | +| help / version / introspect | runtime 负责的产品入口及协议输出 | 沿用各入口专属协议,不当成普通业务结果 | +| Command Pack API 1 | 兼容已有 result/line/write 表面;按调用缓存并最终提交一次 result | 显式 result 保留数据;只有 raw write 时返回累计字符串;重复 result 报协议错误。用户扩展自行调用 process/console 不属于宿主 API 保证 | + +OpenAgentPack SDK 没有 logger 注入接口:仅在 SDK 调用上下文内,以 AsyncLocalStorage 路由 transport 与 console;并发和延迟回调仍使用各自 output,外部 console 调用保持原行为。非结构化 SDK 日志仅保留安全摘要,禁止复制 `/files` 响应体或凭证;类型化 feedback 按 info/warning 输出。诊断写入失败不能改变 SDK 业务成功或失败。 + +## 必查清单 + +### A. 新接口 + +- [ ] 用 ctx.output.result(data, { text? }) 提交一次最终结果,text 只控制文本呈现;JSON 始终序列化 data,false/0/null/空列表都合法 +- [ ] progress 与 info 写 stderr,仅 text 且非 quiet 时显示;progress 当前在非 TTY 下仍输出普通进度文本,不创建动画定时器;仅动画要求 TTY +- [ ] 并发请求提示通过 runConcurrent 的 output.info 输出,数量、请求顺序和失败传播不受 quiet/verbose 影响 +- [ ] 下载进度沿用 TTY 原地刷新,非 TTY 不输出;quiet/JSON 时关闭,不把逐数据块更新转换为追加日志 +- [ ] warning 始终保留;debug 仅 verbose 开启时显示;quiet 不关闭 verbose 明确请求的诊断 +- [ ] 可升级通知使用 UPDATE_AVAILABLE warning,JSON/quiet 下保留;details 提供 current_version、latest_version、upgrade_command,业务结果和退出码不变,联网检查仍按既有间隔执行,命中版本缓存时仍生成可升级通知;自动更新阈值不变 +- [ ] 消息支持 LocalizedText;warning/debug 可携带 code 与 details,details 的凭证键统一脱敏、循环/深层引用受控 +- [ ] 结果自身的凭证与敏感字段继续由命令在提交前按现有业务规则脱敏;不要把真实凭证拼进诊断 message + +### B. 错误与边界 + +- [ ] 业务不调用 error 或 process.exit;终止失败抛异常,由 runtime 统一处理 +- [ ] report 包含失败项时,先保留业务报告;若契约要求失败退出,再抛一次失败摘要。查询 FAILED 资源不自动等价于查询失败 +- [ ] text 错误保留 HTTP 状态、API 错误码、Request ID、cause 链及退出码;本地网络、超时、文件错误与旧出口共用分类,服务端错误原样透传 +- [ ] 普通异常的调用栈通过 ERROR_STACK debug 记录输出,仅 verbose 开启时保留;quiet 不隐藏,JSON 模式仍逐条单行,终止 error 不重复输出 +- [ ] reporter.error 是 runtime 内部渲染能力,只返回退出码;handleError 接收本次 reporter 并负责退出,不把 error 添加到 CommandOutput +- [ ] core 通过可选 debug 回调上报传输诊断,无回调时保持安静;runtime 注入 ctx.output.debug。请求体、认证头和查询参数值不直接打印 +- [ ] 错误 cause 中的结构化对象复用安全序列化;服务端 message、api_code、request_id 不改写 +- [ ] JSON 诊断每条一行;stdout 为单个业务 JSON 文档,stderr 单独解析 +- [ ] speech synthesize 的原始音频仅在 text 流式模式下直接写 stdout;JSON 流式需 --out 文件并返回元数据,未指定文件时在请求和文件操作前报用法错误;dry-run 仍允许 JSON 预览。--list-voices 的 JSON 返回音色条目与已有文档链接,内置列表未知时 voices/total 为 null +- [ ] speech recognize 的同步 JSON 返回完整服务响应;异步完成后返回原始任务响应并附加 transcriptions 数组,保留失败子任务和 request_id。text 保留转写、说话人和批量分隔;--out 仍保存原有转写文件结构,下载及写盘失败不提前输出最终结果;部分子任务失败不改变原有退出码 +- [ ] usage freetier 的启用 JSON 保留原始响应数组;停用 JSON 返回 `{ processed: [{ model, result }], skipped: [model] }`,跳过项仍不发起停用请求。失败与超时用 warning 提示,保留原有退出策略,不把部分失败丢出结果。 +- [ ] Token Plan 创建密钥的完整响应和一次性明文仍属于业务结果,quiet 不裁剪;不得通过 warning/debug 泄露明文。 +- [ ] 原始流与原生交互仅限上表;Command Pack API 1 由宿主缓存并提交一次结果,不再直接转发 stdout +- [ ] 每个调用使用独立 reporter,不共享结果状态、不替换全局 stdout/stderr;SDK console 桥接只能读取调用上下文,不缓存上一个 host +- [ ] 进程共用 SIGINT / stdout error 监听器,调用结束清理输出上下文;并发不触发 MaxListenersExceededWarning,单次中断只输出一条终止错误,EPIPE 保护覆盖 run 返回后的异步写入 + +### C. 新增与变更命令 + +- [ ] 按命令完整调用链迁移:业务、middleware、传输日志、轮询/下载、错误出口必须一起核对 +- [ ] 移除 quiet 裁剪/吞结果分支;同步检查测试(含 live 门控用例)、脚本及示例中依赖 quiet 提取 ID/path 的调用方,改为显式 JSON 并读取对应字段 +- [ ] 核对原始写入边界;普通命令不得恢复全局 emitter 或自行打印诊断。新增原始流例外需同时说明适用格式、通道和副作用前校验 + +### D. 回归测试隔离 + +- [ ] 本地假服务的子进程测试显式覆盖 HTTP_PROXY / HTTPS_PROXY / ALL_PROXY / NO_PROXY 及小写变量,关闭 NODE_USE_ENV_PROXY;仅代理专项测试按场景注入代理,不继承开发机的代理设置 + +## 完成后自查 + +```sh +pnpm exec vp test packages/runtime/tests/output-reporter.test.ts packages/runtime/tests/output-reporter.e2e.test.ts +pnpm exec vp check +``` + +本清单适用于仓内普通业务命令及其完整调用链;上表明确列出的协议与原生交互按各自边界验证。 diff --git a/docs/agents/stress-batch-tests.md b/docs/agents/stress-batch-tests.md index 5da7419b0..fd6ef0ab8 100644 --- a/docs/agents/stress-batch-tests.md +++ b/docs/agents/stress-batch-tests.md @@ -122,7 +122,7 @@ pnpm run test:stress -- video-edit --reuse-fixtures -- --count 3 - 除 `speech recognize` 外,压测子进程宜带 `--output json`(语音识别以 `--out` 文件为准 stdout 可能为纯文本) - 异步类命令带 `--timeout`、对应 `--poll-interval` -**禁止**对子进程加 `--quiet`(与 `--output json` 并存时可能丢 `urls` / `video_url`)。 +`--quiet` 仅隐藏过程提示;与 `--output json` 并用仍须返回完整结果(包括 `urls` / `video_url`),警告和错误保留。不要依赖 quiet 选择结果字段。 **禁止**对视频相关子进程加 `--async`;须阻塞到任务完成(及下载路径正确时落盘)。 diff --git a/docs/knowledge/chunk.md b/docs/knowledge/chunk.md index e5e8cfb51..7e89e351b 100644 --- a/docs/knowledge/chunk.md +++ b/docs/knowledge/chunk.md @@ -48,7 +48,7 @@ chunk created (pipeline: idx-xxx) List chunks to find the new chunk id. ``` -quiet 模式:无输出(成功退出码 0)。 +quiet 模式:仅隐藏过程提示,保留完整结果(成功退出码 0)。 json 模式:返回 API 原始响应(不含 chunk ID)。 @@ -109,7 +109,7 @@ total: 1 > 如果 chunk 被排除检索,行尾会显示 `[excluded from retrieval]`。 -quiet 模式:每行一个 `metadata._id`(chunk ID),用于管道传给 update/delete。 +quiet 模式:仅隐藏过程提示,保留完整结果。提取 chunk ID 时使用 `--output json`,读取 `data.nodes[].metadata._id`,再传给 update/delete。 json 模式:返回 API 原始响应,`data.nodes[]` 含完整 chunk 数据。 @@ -172,7 +172,7 @@ text 模式: updated: chunk-xxx ``` -quiet 模式:无输出。 +quiet 模式:仅隐藏过程提示,保留完整结果。 json 模式:返回 API 原始响应。 @@ -223,7 +223,7 @@ text 模式: deleted: 2 chunk(s) in 1 batch(es) ``` -quiet 模式:无输出。 +quiet 模式:仅隐藏过程提示,保留完整结果。 json 模式:返回 `{ deleted_count, batches }`。 diff --git a/docs/knowledge/collection-category.md b/docs/knowledge/collection-category.md index 6fa785046..e26796ea7 100644 --- a/docs/knowledge/collection-category.md +++ b/docs/knowledge/collection-category.md @@ -40,7 +40,7 @@ text 模式: created: conn-xxx (my-collection, PLATFORM) ``` -quiet 模式:输出数据集 ID。 +quiet 模式:仅隐藏过程提示,保留完整最终结果;程序解析请使用 `--output json`。 json 模式:返回 API 原始响应。 @@ -95,7 +95,7 @@ name: my-collection description: team docs ``` -quiet 模式:输出数据集 ID。 +quiet 模式:仅隐藏过程提示,保留完整最终结果;程序解析请使用 `--output json`。 json 模式:返回 API 原始响应。 @@ -147,7 +147,7 @@ next: --next-token eyJ... > 标记 `[default]` 的是文件未指定分类时的默认归属。 -quiet 模式:每行一个 `categoryId`。 +quiet 模式:仅隐藏过程提示,保留完整最终结果;程序解析请使用 `--output json`。 json 模式:返回 API 原始响应。 @@ -200,7 +200,7 @@ text 模式: created: cate-xxx (product-docs) ``` -quiet 模式:输出分类 ID。 +quiet 模式:仅隐藏过程提示,保留完整最终结果;程序解析请使用 `--output json`。 json 模式:返回 API 原始响应。 @@ -245,7 +245,7 @@ text 模式: deleted: cate-xxx ``` -quiet 模式:无输出。 +quiet 模式:仅隐藏过程提示,保留完整最终结果;程序解析请使用 `--output json`。 json 模式:返回 API 原始响应。 diff --git a/docs/knowledge/doc.md b/docs/knowledge/doc.md index db48bb482..c331b28c0 100644 --- a/docs/knowledge/doc.md +++ b/docs/knowledge/doc.md @@ -58,7 +58,7 @@ bl knowledge doc sync --dir ./docs --index-id idx-xxx --delete --yes 执行失败后保留状态文件;下一次运行先恢复未完成操作,恢复完成后重新预览剩余变更。不要删除状态或换名称来绕过未知写请求。确认之后若本地内容、状态版本或远端清单变化,执行停止,需重新预览。 -JSON 输出包含 `summary`、`actions`、`skipped`、`unmanaged`、`orphans`、`notices`、`stateFile`;失败时 stderr 的 `KNOWLEDGE_SYNC_INCOMPLETE` 提供最新可读 checkpoint,原错误继续输出并返回非零退出码。中途已完成的项目以 checkpoint 为准,不将部分失败报告为全部成功。 +JSON 输出包含 `summary`、`actions`、`skipped`、`unmanaged`、`orphans`、`notices`、`stateFile`;失败时 stderr 单行 JSON 的 `warning.code = KNOWLEDGE_SYNC_INCOMPLETE` 提供恢复信息,`warning.details` 包含 `stateFile` 和最新可读 `checkpoint`,原错误继续输出并返回非零退出码。中途已完成的项目以 checkpoint 为准,不将部分失败报告为全部成功。 --- @@ -93,7 +93,7 @@ doc-xxx COMPLETED intro.md md 1024 total: 1 ``` -quiet 模式:每行一个 `doc_id`。 +quiet 模式:仅隐藏过程提示,保留完整结果。提取 ID 时使用 `--output json`,读取 `data.rows[].doc_id`。 json 模式:返回 API 原始响应。 @@ -144,7 +144,7 @@ status: COMPLETED doc-xxx COMPLETED intro.md ``` -quiet 模式:输出任务状态(`PENDING`/`RUNNING`/`COMPLETED`)。 +quiet 模式:仅隐藏过程提示,保留完整结果。提取任务状态时使用 `--output json`,读取 `data.ingestion_status`。 json 模式:返回 API 原始响应,`data.rows[]` 包含每个文档的状态。 @@ -204,9 +204,9 @@ status: COMPLETED Uploaded 1 file. ``` -quiet 模式:每行一个 `fileId`。 +quiet 模式:仅隐藏过程提示,保留完整结果。提取文件 ID 时显式使用 `--output json`。 -json 模式:返回自定义结构,包含 `files`(路径和 fileId)、`skipped`、`index_id`、`ingestion_id`、`final_status`。 +json 模式:返回自定义结构,包含 `files`(路径和 fileId)、`skipped`,以及实际产生的 `index_id`、`ingestion_id`、`final_status`;文件 ID 从 `files[].fileId` 提取。大小建议警告通过 stderr 输出,JSON 下为单行 JSON,quiet 下保留。 **注意事项** @@ -262,14 +262,14 @@ deleted: 2 document(s) doc-b ``` -quiet 模式:每行一个已删除的 `doc_id`。 +quiet 模式:仅隐藏过程提示,保留完整结果和删除数量不符的警告。 json 模式:返回 API 原始响应,`data.deleted[]` 为实际删除的 ID 列表。 **注意事项** - 只从知识库索引中移除文档,数据中心源文件不受影响(用 `file delete` 删除源文件)。 -- `doc_id` 应从 `knowledge doc list --quiet` 获取,而非 `doc upload` 返回的 `fileId`。 +- `doc_id` 应从 `knowledge doc list --output json` 的 `data.rows[].doc_id` 获取,而非 `doc upload` 返回的 `fileId`。 - 删除是异步的:服务端立即返回 Success,但 `doc list` 中可能仍显示该文档(约 30 秒后传播完成)。 - 输出的是服务端实际删除的 ID 列表,可能与请求的数量不一致(会在 stderr 警告)。 @@ -319,7 +319,7 @@ text 模式: tagged: 2 file(s) with [project-a, draft] ``` -quiet 模式:无输出。 +quiet 模式:仅隐藏过程提示,保留完整结果。 json 模式:返回 API 原始响应。 @@ -375,7 +375,7 @@ imported: 2 file(s) file-b SUCCESS docs/b.docx ``` -quiet 模式:每行一个 `fileId`。 +quiet 模式:仅隐藏过程提示,保留完整结果。提取文件 ID 时显式使用 `--output json`。 json 模式:返回 API 原始响应,`data.addFileResultList[]` 包含每个文件的 fileId、status 和 ossKey。 @@ -430,7 +430,7 @@ bl knowledge doc list --index-id idx-xxx --details --page-number 2 --page-size 1 | `--enable-headers` | boolean | 显式传 true 或 false,控制标题提取。 | | `--dry-run` | switch | 预览请求,不执行导入。 | -导入复用已有 fileId,不重新上传文件或创建知识库。任务返回 ingestionId;需要实际完成状态时使用 --wait。已有文件的类型和导入数量由服务端校验,不能用分类个数代替文件数。文档切片参数不是音视频按秒切片参数。 +导入复用已有 fileId,不重新上传文件或创建知识库。quiet 仅隐藏过程提示,保留完整结果。JSON 保留原始响应,任务 ID 从 `data.ingestionId` 提取;`--wait` 成功时另含完整 `status` 响应。需要实际完成状态时使用 --wait。已有文件的类型和导入数量由服务端校验,不能用分类个数代替文件数。文档切片参数不是音视频按秒切片参数。 ### 不同场景怎么处理 diff --git a/docs/knowledge/file.md b/docs/knowledge/file.md index b38908b4a..425c0f146 100644 --- a/docs/knowledge/file.md +++ b/docs/knowledge/file.md @@ -35,7 +35,7 @@ file-xxx SUCCESS intro.md 1024 next: --next-token eyJ... ``` -quiet 模式:每行一个 `fileId`。 +quiet 模式:仅隐藏过程提示,保留完整最终结果;程序解析请使用 `--output json`。 json 模式:返回 API 原始响应。 @@ -91,7 +91,7 @@ uploaded: 2026-01-01T00:00:00Z tags: project-a, draft ``` -quiet 模式:输出 JSON 格式。 +quiet 模式:仅隐藏过程提示,保留完整最终结果;程序解析请使用 `--output json`。 json 模式:返回 API 原始响应。 @@ -133,7 +133,7 @@ text 模式: deleted: file-xxx ``` -quiet 模式:无输出。 +quiet 模式:仅隐藏过程提示,保留完整最终结果;程序解析请使用 `--output json`。 json 模式:返回 API 原始响应。 diff --git a/docs/knowledge/kb.md b/docs/knowledge/kb.md index 1f268672c..a6172a935 100644 --- a/docs/knowledge/kb.md +++ b/docs/knowledge/kb.md @@ -38,7 +38,7 @@ idx-xxx my-kb text-embedding-v4 600 product docs total: 1 ``` -quiet 模式:每行一个知识库 ID。 +quiet 模式:仅隐藏过程提示,保留完整最终结果;程序解析请使用 `--output json`。 json 模式:返回 API 原始响应,`data.rows[]` 包含完整知识库信息。 @@ -104,7 +104,7 @@ Data: connectorId: ... ``` -quiet 模式:输出知识库 ID。 +quiet 模式:仅隐藏过程提示,保留完整最终结果;程序解析请使用 `--output json`。 json 模式:返回知识库完整配置 JSON。 @@ -165,7 +165,7 @@ status: COMPLETED Next: check the import job status, then search against this knowledge base. ``` -quiet 模式:stdout 只输出知识库 ID,stderr 仍保留计费提示和已创建资源诊断。 +quiet 模式:仅隐藏过程提示,保留完整最终结果;程序解析请使用 `--output json`。 json 模式:保留 API 原始响应,`data` 包含 `pipelineId`(知识库 ID)和 `ingestionId`(导入任务 ID),并追加: @@ -236,7 +236,7 @@ text 模式: updated: idx-xxx ``` -quiet 模式:无输出。 +quiet 模式:仅隐藏过程提示,保留完整最终结果;程序解析请使用 `--output json`。 json 模式:返回 API 原始响应。 @@ -281,7 +281,7 @@ text 模式: deleted: idx-xxx ``` -quiet 模式:无输出。 +quiet 模式:仅隐藏过程提示,保留完整最终结果;程序解析请使用 `--output json`。 json 模式:返回 API 原始响应。 @@ -332,7 +332,7 @@ peak qps: 5 qps windows: 24 data point(s) ``` -quiet 模式:输出 json 格式。 +quiet 模式:仅隐藏过程提示,保留完整最终结果;程序解析请使用 `--output json`。 json 模式:返回 API 原始响应,包含 `storageMonitorData` 和 `qpsMonitorData`。 diff --git a/docs/knowledge/knowledge-cli-guide.md b/docs/knowledge/knowledge-cli-guide.md index 851af2523..ae2afc236 100644 --- a/docs/knowledge/knowledge-cli-guide.md +++ b/docs/knowledge/knowledge-cli-guide.md @@ -124,8 +124,8 @@ | `--api-key ` | string | DashScope API Key | | `--base-url ` | string | API 基地址(一般不需要指定) | | `--timeout ` | number | 请求超时秒数 | -| `--quiet` | switch | 静默模式,只输出关键结果(如 ID 列表) | -| `--verbose` | switch | 详细模式,打印 HTTP 请求/响应详情到 stderr | +| `--quiet` | switch | 隐藏过程提示;已迁移命令保留完整结果 | +| `--verbose` | switch | 详细模式,向 stderr 输出诊断摘要 | | `--dry-run` | switch | 预览请求或执行计划,不写入;带只读预检的命令可能读取远端资源 | | `--introspect` | switch | 输出当前命令范围的结构化能力说明,无需凭证 | | `--config ` | string | 使用指定配置 profile 执行命令 | @@ -136,7 +136,7 @@ - **text 模式**(默认):人类友好的表格/结构化文本,适合终端查看。不同命令的输出格式见各命令的「输出」部分。 - **json 模式**(`--output json`):返回 API 原始 JSON 响应,适合程序化处理和 agent 解析。 -- **quiet 模式**(`--quiet`):只输出最精简的结果(通常只有 ID),适合管道串联。 +- **quiet 模式**(`--quiet`):已迁移的知识库管理、类目、数据中心文件、数据集合、Service、文档 list/status/upload/delete/tag/import/import-oss 和全部 chunk 命令仅隐藏过程提示,保留完整最终结果。需要 ID 时使用 `--output json` 并读取对应字段;其余及新增命令尚在迁移,行为见各自手册。 ### 危险操作确认 @@ -762,7 +762,7 @@ bl config set workspace_id ws-xxx **原因**:通过 `knowledge create --doc-id` 导入的文档,`doc_id` 等于 `fileId`;但通过 `doc upload --index-id` 导入的,`doc_id` 可能含 workspace 后缀。 -**解决**:始终用 `doc list --quiet` 获取 `doc_id`。 +**解决**:使用 `doc list --output json` 的 `data.rows[].doc_id` 获取 `doc_id`。 ### OSS 导入权限错误 diff --git a/docs/knowledge/search-chat.md b/docs/knowledge/search-chat.md index ce4ab3b79..34b426df6 100644 --- a/docs/knowledge/search-chat.md +++ b/docs/knowledge/search-chat.md @@ -177,13 +177,16 @@ bl knowledge chat --agent-id aid-xxx --messages-file ./messages.json --output js chat 仍使用 SSE 接口,各输出模式共享事件聚合: -| 模式 | stdout | -| ------------------------ | ----------------------------------------------------------------------------- | -| JSON | answer 只含最终回答,request_id 保留;新增 phases、tools、docs、usage、events | -| text + TTY | 按规划/工具/回答显示过程,末尾可读来源摘要 | -| text + 管道 | 仅最终回答 | -| quiet,包括 JSON + quiet | 仅裸最终回答;TTY 同样遵守 | -| verbose | 主结果不变,事件诊断写 stderr | +| 模式 | stdout | +| ------------ | ----------------------------------------------------------------------------- | +| JSON | answer 只含最终回答,request_id 保留;新增 phases、tools、docs、usage、events | +| text + TTY | 按规划/工具/回答显示过程,末尾可读来源摘要 | +| text + 管道 | 仅最终回答 | +| JSON + quiet | 与 JSON 相同的完整结果,不裁剪为裸回答 | +| text + quiet | 隐藏阶段过程;TTY 保留最终回答与来源摘要,管道保留最终回答 | +| verbose | 主结果不变,事件诊断写 stderr | + +warning 始终写 stderr,quiet 不隐藏;JSON 模式的 warning/debug/error 每条为单行 JSON,与 stdout 分别解析。 `events` 顺序保存原始 SSE event/data,未知字段不会因聚合而丢失;不是 HTTP 字节流快照。`usage` 取最终生成结算帧,不能将累计帧相加;工具阶段用量可在原始事件中查看。无完整最终回答或流中断会非零退出,不把规划或工具摘要当作成功回答。相比旧版,answer 不再混入过程文本,依赖旧混合内容的脚本需要改读 phases/tools/events。 diff --git a/docs/knowledge/service.md b/docs/knowledge/service.md index 822d12c24..168478b12 100644 --- a/docs/knowledge/service.md +++ b/docs/knowledge/service.md @@ -46,7 +46,7 @@ Use an agent_id above with the knowledge chat command. > 最后一行根据 scene 自动提示用 `search` 还是 `chat` 命令消费。 -quiet 模式:每行一个 `agent_id`。 +quiet 模式:仅隐藏过程提示,保留所选格式的完整最终结果;程序解析请使用 `--output json` 并读取对应字段。 json 模式:返回 API 原始响应。 @@ -105,7 +105,7 @@ Version 1: ... ``` -quiet 模式:输出 JSON 格式。 +quiet 模式:仅隐藏过程提示,保留所选格式的完整最终结果;程序解析请使用 `--output json` 并读取对应字段。 json 模式:返回 API 原始响应。 @@ -160,7 +160,7 @@ created: aid-xxx (status: draft, version: beta) Test the draft with --agent-version beta on search/chat, then deploy it to publish. ``` -quiet 模式:输出 agent ID。 +quiet 模式:仅隐藏过程提示,保留所选格式的完整最终结果;程序解析请使用 `--output json` 并读取对应字段。 json 模式:返回 API 原始响应。 @@ -232,7 +232,7 @@ updated: aid-xxx Draft config changed — verify with --agent-version beta, then deploy. ``` -quiet 模式:无输出。 +quiet 模式:仅隐藏过程提示,保留所选格式的完整最终结果;程序解析请使用 `--output json` 并读取对应字段。 json 模式:返回 API 原始响应。 @@ -284,7 +284,7 @@ text 模式: deployed: aid-xxx version 2 ``` -quiet 模式:输出新版本号。 +quiet 模式:仅隐藏过程提示,保留所选格式的完整最终结果;程序解析请使用 `--output json` 并读取对应字段。 json 模式:返回 API 原始响应。 @@ -332,7 +332,7 @@ text 模式: deleted: aid-xxx (status: deleted) ``` -quiet 模式:无输出。 +quiet 模式:仅隐藏过程提示,保留所选格式的完整最终结果;程序解析请使用 `--output json` 并读取对应字段。 json 模式:返回 API 原始响应。 @@ -380,7 +380,7 @@ new agent_id: aid-new (name: copy_my-qa, status: draft) Test the draft with --agent-version beta on search/chat, then deploy it to publish. ``` -quiet 模式:输出新 agent ID。 +quiet 模式:仅隐藏过程提示,保留所选格式的完整最终结果;程序解析请使用 `--output json` 并读取对应字段。 json 模式:返回 API 原始响应。 diff --git a/docs/memory/memory-cli-guide.md b/docs/memory/memory-cli-guide.md index c4e42daf5..ad756146f 100644 --- a/docs/memory/memory-cli-guide.md +++ b/docs/memory/memory-cli-guide.md @@ -80,11 +80,11 @@ API Key可通过 `--api-key`、环境变量或配置提供。Workspace解析顺 | `--workspace-id ` | 指定Memory所在工作空间 | | `--config ` | 使用指定配置profile | | `--timeout ` | 请求超时;与add的抽取等待预算wait不同 | -| `--quiet` | Memory命令通常沿用文本输出分支,并非统一“只返回ID” | +| `--quiet` | 仅隐藏过程提示,保留所选格式的完整最终结果 | | `--verbose` | 输出诊断信息;保存日志时注意内容和凭证保护 | | `--dry-run` | 预览endpoint、method和适用的request,不发送Memory业务请求 | -text输出便于终端查看,格式按命令不同。json通常保留服务响应;add等待完成时输出提交request_id、event_id与轮询events的组合对象。不要把 `--quiet` 与 `--output json` 混用,quiet会优先进入文本输出分支。 +text输出便于终端查看,格式按命令不同。json通常保留服务响应;add等待完成时输出提交request_id、event_id与轮询events的组合对象。`--quiet` 可与 `--output json` 组合使用,不会改变 JSON 结构或裁剪最终结果。 JSON参数接收参数文本,不支持 `@file`:messages、attributes、attributes-operations为对象数组;meta-data为对象。数组型flag(例如skill-tags)通过重复flag传递。示例以POSIX shell为准,参数中的尖括号ID必须替换。 diff --git a/docs/memory/node.md b/docs/memory/node.md index 3917a75e0..d5683c267 100644 --- a/docs/memory/node.md +++ b/docs/memory/node.md @@ -31,7 +31,7 @@ bl memory list --user-id [flags] **输出** -json返回 `memory_nodes` / `total` 等字段;text / quiet显示类型、节点ID、正文、元数据和总数(如有),空集合显示提示。 +json返回 `memory_nodes` / `total` 等字段;text显示类型、节点ID、正文、元数据和总数(如有),空集合显示提示。 **注意事项** @@ -68,7 +68,7 @@ bl memory node show --node-id [flags] **输出** -json返回 `memory_node`;text / quiet显示正文、类型、状态、规则、时间戳和元数据等存在的字段。 +json返回 `memory_node`;text显示正文、类型、状态、规则、时间戳和元数据等存在的字段。 **注意事项** @@ -111,7 +111,7 @@ bl memory update --node-id --content [flags] **输出** -json保留服务响应;text / quiet输出节点更新确认信息。 +json保留服务响应;text输出节点更新确认信息。 **注意事项** @@ -150,7 +150,7 @@ bl memory delete --node-id [flags] **输出** -json保留服务响应;text / quiet输出节点删除确认信息。 +json保留服务响应;text输出节点删除确认信息。 **注意事项** @@ -163,3 +163,5 @@ bl memory delete --node-id '' --dry-run --output json # 确认节点ID后执行 bl memory delete --node-id '' --yes --output json ``` + +`--quiet` 仅隐藏过程提示,不改变最终结果;与 `--output json` 组合时仍返回完整 JSON,与 text 组合时保留完整文本结果。 diff --git a/docs/memory/profile.md b/docs/memory/profile.md index 85159b6a4..5f480f989 100644 --- a/docs/memory/profile.md +++ b/docs/memory/profile.md @@ -33,7 +33,7 @@ bl memory profile create --name --attributes [flags] **输出** -json保留 `profile_schema_id` 等响应字段;text / quiet输出模板创建确认和ID。 +json保留 `profile_schema_id` 等响应字段;text输出模板创建确认和ID。 **注意事项** @@ -71,7 +71,7 @@ bl memory profile list [flags] **输出** -json返回 `profile_schemas` / `total`;text / quiet显示模板ID、名称、档位、描述及总数(如有)。 +json返回 `profile_schemas` / `total`;text显示模板ID、名称、档位、描述及总数(如有)。 **注意事项** @@ -108,7 +108,7 @@ bl memory profile show --schema-id [flags] **输出** -json保留模板详情;text / quiet显示模板名称、描述和属性定义(包括attribute_id、描述、默认值等)。 +json保留模板详情;text显示模板名称、描述和属性定义(包括attribute_id、描述、默认值等)。 **注意事项** @@ -152,7 +152,7 @@ bl memory profile update --schema-id [flags] **输出** -json保留服务响应;text / quiet输出模板更新确认。 +json保留服务响应;text输出模板更新确认。 **注意事项** @@ -193,7 +193,7 @@ bl memory profile delete --schema-id [flags] **输出** -json保留服务响应;text / quiet输出模板删除确认。 +json保留服务响应;text输出模板删除确认。 **注意事项** @@ -233,7 +233,7 @@ bl memory profile get --schema-id --user-id [flags] **输出** -json中的 `profile.attributes` 包含用户属性,服务返回的value/status等字段保留;text / quiet显示属性及值,无画像数据时显示提示。字段可能缺省,不能假定每个属性都有value。 +json中的 `profile.attributes` 包含用户属性,服务返回的value/status等字段保留;text显示属性及值,无画像数据时显示提示。字段可能缺省,不能假定每个属性都有value。 **注意事项** @@ -245,3 +245,5 @@ json中的 `profile.attributes` 包含用户属性,服务返回的value/status bl memory add --user-id demo_user --profile-schema '' --extract-mode profile_only --messages '[{"role":"user","content":"我常住杭州,喜欢骑行。"}]' --output json bl memory profile get --schema-id '' --user-id demo_user --output json ``` + +`--quiet` 仅隐藏过程提示,不改变最终结果;与 `--output json` 组合时仍返回完整 JSON,与 text 组合时保留完整文本结果。 diff --git a/docs/memory/skill.md b/docs/memory/skill.md index ba3a87639..dccd2fa42 100644 --- a/docs/memory/skill.md +++ b/docs/memory/skill.md @@ -27,7 +27,7 @@ bl memory skill export --node-id [flags] **输出** -text / quiet输出服务导出的正文,不含frontmatter;json保留 `memory_node` 下的正文和技能元数据。命令不直接写文件,可用shell重定向。 +text输出服务导出的正文,不含frontmatter;json保留 `memory_node` 下的正文和技能元数据。命令不直接写文件,可用shell重定向。 **注意事项** @@ -60,3 +60,5 @@ bl memory update --node-id '' --content '按议题、决策和待 ``` 完整参数见[写入手册](add.md)、[检索手册](search.md)、[节点管理手册](node.md)。默认检索只包含observation,查询skill必须显式指定类型。 + +`--quiet` 仅隐藏过程提示,不改变最终结果;与 `--output json` 组合时仍返回完整 JSON,与 text 组合时保留完整文本结果。 diff --git a/docs/parsex.md b/docs/parsex.md index 1d66838de..55132d55b 100644 --- a/docs/parsex.md +++ b/docs/parsex.md @@ -122,8 +122,8 @@ The individual result parameter tables take precedence over the general OSS page | `--poll-timeout` | Total deadline including in-flight HTTP requests, 1–86400 seconds; default 600 | | `--timeout` | Existing per-request HTTP timeout | | Exit codes with `--follow` | Success 0; failed task/service error 1; invalid arguments 2; client deadline 5; Ctrl-C 130 | -| `--output json` | One final response on stdout; progress on stderr | -| `--quiet` | Submit prints only biz_id; result prints only status | +| `--output json` | One final response on stdout; structured diagnostics on stderr, without progress | +| `--quiet` | Hides progress only; submit/result retain the complete response in the selected format | | `--dry-run` | Validates local input and prints a redacted request; no business requests or business file writes | Ctrl-C or timeout stops local polling without cancelling the task. Keep `biz_id` and use `result` to resume. A lost submit response does not prove that no task was created; check before resubmitting. Framework telemetry/update checks follow their existing settings. diff --git a/packages/cli/README.md b/packages/cli/README.md index 315dc32af..0a1c88c12 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -141,7 +141,7 @@ Keep the returned IDs and recovery record. Closing the CLI or a later failure do ### RAG audio and video -`bl knowledge doc import --index-id --doc-id ` imports an existing file into a knowledge base; `doc list --details` shows per-file processing settings. Media support includes parser options, streaming uploads, and media references in search/chunk output. Chat JSON separates the final answer from phases, tools, docs, usage, and raw events; quiet mode emits only the final answer. See the [command guide](docs/knowledge/knowledge-cli-guide.md). +`bl knowledge doc import --index-id --doc-id ` imports an existing file into a knowledge base; `doc list --details` shows per-file processing settings. Media support includes parser options, streaming uploads, and media references in search/chunk output. Chat JSON separates the final answer from phases, tools, docs, usage, and raw events; quiet mode hides progress and informational messages while preserving the final result, including every JSON field. See the [command guide](docs/knowledge/knowledge-cli-guide.md). ## ParseX diff --git a/packages/cli/README.zh.md b/packages/cli/README.zh.md index 0d8bb0c89..a07c3898c 100644 --- a/packages/cli/README.zh.md +++ b/packages/cli/README.zh.md @@ -140,7 +140,7 @@ bl knowledge init --workspace-id --dry-run ### RAG 音视频接入 -`bl knowledge doc import --index-id --doc-id ` 将历史文件导入已有库;`doc list --details` 查看文件级处理参数。支持音视频 parser、流式上传,以及检索/切片中的媒体来源展示。问答 JSON 将最终 answer 与 phases/tools/docs/usage/events 分开保存,quiet 仅输出最终回答。参阅 [命令指南](docs/knowledge/knowledge-cli-guide.md)。 +`bl knowledge doc import --index-id --doc-id ` 将历史文件导入已有库;`doc list --details` 查看文件级处理参数。支持音视频 parser、流式上传,以及检索/切片中的媒体来源展示。问答 JSON 将最终 answer 与 phases/tools/docs/usage/events 分开保存,quiet 只隐藏进度和过程提示,保留最终结果及完整 JSON 字段。参阅 [命令指南](docs/knowledge/knowledge-cli-guide.md)。 ## ParseX diff --git a/packages/cli/tests/e2e/command-packs.e2e.test.ts b/packages/cli/tests/e2e/command-packs.e2e.test.ts index e709ea2b9..d445ba131 100644 --- a/packages/cli/tests/e2e/command-packs.e2e.test.ts +++ b/packages/cli/tests/e2e/command-packs.e2e.test.ts @@ -14,19 +14,26 @@ function env(): NodeJS.ProcessEnv { DO_NOT_TRACK: "1", DASHSCOPE_API_KEY: "", DASHSCOPE_BASE_URL: "", + DASHSCOPE_OUTPUT: "", + DASHSCOPE_QUIET: "0", + DASHSCOPE_VERBOSE: "0", }; } function parseStderrJsonDiagnostics(stderr: string): unknown[] { return stderr .trim() - .split(/\n\s*\n/) + .split("\n") .map((diagnostic) => JSON.parse(diagnostic) as unknown); } describe("e2e: Command Pack", () => { beforeAll(async () => { configDir = await mkdtemp(join(tmpdir(), "bl-command-pack-e2e-")); + await writeFile( + join(configDir, "update-state.json"), + JSON.stringify({ lastChecked: Date.now(), latestVersion: "0.0.0" }), + ); }); afterAll(async () => { @@ -102,18 +109,20 @@ describe("e2e: Command Pack", () => { ); const fallbackFailure = await runCli(["agent", "credential", "--output", "json"], env()); expect(fallbackFailure.exitCode).toBe(3); - expect(fallbackFailure.stderr).toMatch(/^\{\n {2}"warning": \{/); - expect(fallbackFailure.stderr).toContain("\n\n{\n"); + expect(fallbackFailure.stderr.trim().split("\n")).toHaveLength(2); + expect(fallbackFailure.stderr.endsWith("\n")).toBe(true); expect(parseStderrJsonDiagnostics(fallbackFailure.stderr)).toEqual([ { warning: { code: "PROFILE_API_KEY_FALLBACK", message: 'Profile "company-plan" does not support command "agent credential"; API Key settings will be read from Profile "default" for this run.', - profile: "company-plan", - command_path: ["agent", "credential"], - fallback_profile: "default", - credential_fields: ["api_key", "base_url"], + details: { + profile: "company-plan", + command_path: ["agent", "credential"], + fallback_profile: "default", + credential_fields: ["api_key", "base_url"], + }, }, }, { diff --git a/packages/cli/tests/e2e/update-notice.e2e.test.ts b/packages/cli/tests/e2e/update-notice.e2e.test.ts new file mode 100644 index 000000000..9ce8d8bca --- /dev/null +++ b/packages/cli/tests/e2e/update-notice.e2e.test.ts @@ -0,0 +1,79 @@ +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vite-plus/test"; +import pkg from "../../package.json" with { type: "json" }; +import { runCli } from "./helpers.ts"; + +const [majorVersion, minorVersion] = pkg.version + .split(".") + .map((part) => Number.parseInt(part, 10)); +const latestVersion = `${majorVersion}.${minorVersion! + 1}.0`; + +test.each(["text", "json"] as const)( + "config list retains cached update warnings in %s, including the next process with quiet", + async (format) => { + const configDir = mkdtempSync(join(tmpdir(), "bl-update-notice-")); + try { + writeFileSync( + join(configDir, "config.json"), + JSON.stringify({ language: "en-US", telemetry: false }), + ); + writeFileSync( + join(configDir, "update-state.json"), + JSON.stringify({ + lastChecked: Date.now(), + latestVersion, + }), + ); + for (const quiet of [false, true]) { + const result = await runCli( + ["config", "list", "--output", format, ...(quiet ? ["--quiet"] : [])], + { + BAILIAN_CONFIG_DIR: configDir, + DO_NOT_TRACK: "1", + DASHSCOPE_OUTPUT: "", + DASHSCOPE_QUIET: "0", + DASHSCOPE_VERBOSE: "0", + // Do not permit real installations even if update thresholds change in the future. + BAILIAN_INSTALL_METHOD: "brew", + HTTP_PROXY: "", + HTTPS_PROXY: "", + ALL_PROXY: "", + }, + ); + expect(result.exitCode, result.stderr).toBe(0); + if (format === "json") { + expect(JSON.parse(result.stdout)).toMatchObject({ + active_config: "default", + profiles: ["default"], + }); + const records = result.stderr + .trim() + .split("\n") + .map((line) => JSON.parse(line)); + expect(records).toEqual([ + { + warning: { + code: "UPDATE_AVAILABLE", + message: expect.stringContaining("Update available"), + details: { + current_version: pkg.version, + latest_version: latestVersion, + upgrade_command: "bl update", + }, + }, + }, + ]); + } else { + expect(result.stdout).toContain("default"); + expect(result.stderr).toContain("Update available"); + expect(result.stderr).toContain(`${pkg.version} → ${latestVersion}`); + expect(result.stderr).toContain("bl update"); + } + } + } finally { + rmSync(configDir, { recursive: true, force: true }); + } + }, +); diff --git a/packages/cli/tests/fixtures/command-pack/commands.mjs b/packages/cli/tests/fixtures/command-pack/commands.mjs index 19a9fd650..afe730cda 100644 --- a/packages/cli/tests/fixtures/command-pack/commands.mjs +++ b/packages/cli/tests/fixtures/command-pack/commands.mjs @@ -15,7 +15,7 @@ const ping = { usageArgs: "--message ", exampleArgs: ['--message "hello"'], async run(ctx) { - process.stdout.write(`command-pack:${ctx.flags.message}\n`); + ctx.output.line(`command-pack:${ctx.flags.message}`); }, }; @@ -44,9 +44,7 @@ const credential = { auth: "apiKey", async run(ctx) { const apiKey = ctx.credentials.apiKey(); - process.stdout.write( - `credential-source:${apiKey.source} credential-base-url:${apiKey.baseUrl}\n`, - ); + ctx.output.line(`credential-source:${apiKey.source} credential-base-url:${apiKey.baseUrl}`); }, }; diff --git a/packages/commands/src/commands/advisor/recommend.ts b/packages/commands/src/commands/advisor/recommend.ts index 69ac79bb3..b49c352e6 100644 --- a/packages/commands/src/commands/advisor/recommend.ts +++ b/packages/commands/src/commands/advisor/recommend.ts @@ -5,10 +5,10 @@ import { createReadlineInteraction, DEFAULT_LLM_MODEL, defineCommand, - detectOutputFormat, ExitCode, type EngineRecommendResult, type Degradation, + type CommandOutput, LLMError, loadCatalog, loadFamilies, @@ -16,7 +16,6 @@ import { recommendEngine, type RecommendedModel, } from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; import { adaptCommandOutput } from "./recommend-adapter.ts"; const STAGE_LABEL: Record = { @@ -38,9 +37,13 @@ function describeDegradation(entry: Degradation): string { return `${STAGE_LABEL[entry.stage] ?? entry.stage} failed (${tag}); ${effect}. ${entry.message}`; } -function emitDegradations(degradations: Degradation[]): void { +function emitDegradations(degradations: Degradation[], output: CommandOutput): void { for (const entry of degradations) { - process.stderr.write(`Warning: ${describeDegradation(entry)}\n`); + output.warning({ + message: describeDegradation(entry), + code: "ADVISOR_DEGRADED", + details: { stage: entry.stage, kind: entry.kind, fallback: entry.fallback }, + }); } } @@ -146,6 +149,9 @@ export default defineCommand({ "zh-CN": "为你的使用场景推荐最佳模型(意图 → 路由 → 召回 → 打分 → 排序,支持 agent 澄清交互)", }, auth: "apiKey", + defaultOutput: "json", + defaultInteractiveOutput: "text", + verboseFlags: ["trace"], usageArgs: "--message [flags]", flags, exampleArgs: [ @@ -170,6 +176,16 @@ export default defineCommand({ const { settings, flags: runtimeFlags } = ctx; const message = runtimeFlags.message; const top = runtimeFlags.top ?? 3; + const useAgent = !runtimeFlags.noAgent && process.stdin.isTTY === true; + if (useAgent && settings.output === "json") + throw new BailianError( + ctx.localize({ + "en-US": + "Interactive advisor clarification requires --output text. Use --no-agent for JSON output.", + "zh-CN": "交互式模型推荐澄清需要 --output text。请使用 --no-agent 获取 JSON 输出。", + }), + ExitCode.USAGE, + ); // Keep the local wiki catalog fresh: throttled (12h) version check against // the remote manifest, silently replaces data when a newer version exists. @@ -196,7 +212,7 @@ export default defineCommand({ get(target, prop, receiver) { if (prop === "push") { return (...items: string[]) => { - for (const item of items) process.stderr.write(` ${item}\n`); + for (const item of items) ctx.output.info(` ${item}`); return target.push(...items); }; } @@ -218,13 +234,21 @@ export default defineCommand({ catalog: () => loadCatalog(), families: () => loadFamilies(), verbose: runtimeFlags.trace ?? false, + onTrace: ( + trace: Parameters[1]["onTrace"]>>[0], + ) => { + const { input: _input, ...details } = trace; + ctx.output.debug({ + message: { "en-US": "Recommendation trace", "zh-CN": "模型推荐追踪" }, + code: "ADVISOR_TRACE", + details, + }); + }, + onDiagnostic: (message: string) => ctx.output.debug({ message, code: "ADVISOR_AGENT" }), decisionLog: liveDecisionLog, topN: top, }; - const isTTY = process.stdin.isTTY === true; - const useAgent = !runtimeFlags.noAgent && isTTY; - let result: EngineRecommendResult | undefined; let finalText = ""; let agentDegradations: Degradation[] = []; @@ -234,7 +258,7 @@ export default defineCommand({ ...deps, interaction: createReadlineInteraction(), // Real-time tool progress to stderr (always on, independent of --think) - onProgress: (event) => process.stderr.write(`[agent] ${event}\n`), + onProgress: (event) => ctx.output.progress(`[agent] ${event}`), }); result = agentResult.recommendResult; finalText = agentResult.finalText; @@ -256,21 +280,19 @@ export default defineCommand({ ...agentDegradations, ]; - // Default to JSON for structured output; text rendering only when the - // user explicitly asked for text output. - const format = settings.outputExplicit ? detectOutputFormat(settings.output) : "json"; - - // Degradations go to stderr so stdout stays a clean result - emitDegradations(degradations); + emitDegradations(degradations, ctx.output); // Non-interactive clarify: auto-apply defaultIfSkip so a single non-TTY run // still produces recommendations (advisor's single-shot contract), while // surfacing the question+options on stderr for visibility. if (result?.type === "need_clarify" && !useAgent) { - process.stderr.write( - `Clarification (auto-answered "${result.defaultIfSkip}"): ${result.question}\n`, + ctx.output.info( + ctx.localize({ + "en-US": `Clarification (auto-answered "${result.defaultIfSkip}"): ${result.question}`, + "zh-CN": `澄清(自动回答“${result.defaultIfSkip}”):${result.question}`, + }), ); - for (const opt of result.options) process.stderr.write(` - ${opt}\n`); + for (const option of result.options) ctx.output.info(` - ${option}`); const retryResult = await recommendEngine( `${message}\n- ${result.question} → ${result.defaultIfSkip}`, deps, @@ -278,9 +300,13 @@ export default defineCommand({ if (retryResult.type === "need_clarify") { // Still ambiguous after one auto-answer: give up on clarification and // surface the second question; fall through to a bare result. - process.stderr.write( - `Warning: request still ambiguous after auto-clarification. / 自动澄清后仍存在歧义。\n`, - ); + ctx.output.warning({ + message: { + "en-US": "Request still ambiguous after auto-clarification.", + "zh-CN": "自动澄清后仍存在歧义。", + }, + code: "ADVISOR_AMBIGUOUS", + }); result = undefined; } else { result = retryResult; @@ -291,50 +317,47 @@ export default defineCommand({ // directly without running the recommend engine. finalText is the answer; // there is no recommendation result and that is a valid terminal state. if (finalText && !result) { - if (format === "text") { - emitBare(finalText.trim()); - } else { - emitResult({ final_text: finalText.trim() }, format); - } + ctx.output.result({ final_text: finalText.trim() }, { text: finalText.trim() }); return; } - if (!result || result.type === "need_clarify") { - // TTY agent loop ran out of clarification budget, or double-clarify above. if (result?.type === "need_clarify") { - emitBare(`Clarification needed: ${result.question}`); - for (const opt of result.options) emitBare(` - ${opt}`); - emitBare( - `(If unanswered, defaults to "${result.defaultIfSkip}". Re-run with a more specific description of your need.)`, - ); + ctx.output.result(result, { + text: [ + ctx.localize({ + "en-US": `Clarification needed: ${result.question}`, + "zh-CN": `需要澄清:${result.question}`, + }), + ...result.options.map((option) => ` - ${option}`), + ctx.localize({ + "en-US": `(If unanswered, defaults to "${result.defaultIfSkip}". Re-run with a more specific description of your need.)`, + "zh-CN": `(未回答时默认使用“${result.defaultIfSkip}”。请提供更具体的需求描述后重试。)`, + }), + ].join("\n"), + }); return; } - emitBare("No recommendation produced."); + ctx.output.result( + { recommendations: [] }, + { + text: ctx.localize({ + "en-US": "No recommendation produced.", + "zh-CN": "未生成推荐结果。", + }), + }, + ); return; } - const adapted = adaptCommandOutput(result); - - if (format !== "text") { - emitResult(adapted, format); - return; - } - - if (finalText) { - emitBare(finalText.trim()); - return; - } - - if (adapted.result.type === "pipeline") { - emitBare( - formatPipelineResult( - adapted.result.summary as string, - adapted.result.steps as PipelineStepLocal[], - ), - ); - } else { - emitBare(formatSingleResult(adapted.result.recommendations as RecommendedModel[])); - } + const text = finalText + ? finalText.trim() + : adapted.result.type === "pipeline" + ? formatPipelineResult( + adapted.result.summary as string, + adapted.result.steps as PipelineStepLocal[], + ) + : formatSingleResult(adapted.result.recommendations as RecommendedModel[]); + ctx.output.result(adapted, { text }); }, }); diff --git a/packages/commands/src/commands/alert/create.ts b/packages/commands/src/commands/alert/create.ts index a9f2c9e4f..40c6a057a 100644 --- a/packages/commands/src/commands/alert/create.ts +++ b/packages/commands/src/commands/alert/create.ts @@ -1,6 +1,5 @@ -import { detectOutputFormat, unwrapResponse } from "bailian-cli-core"; +import { unwrapResponse } from "bailian-cli-core"; import { defineTelemetryCommand, telemetryGatewayPreview } from "../shared/telemetry.ts"; -import { emitResult } from "bailian-cli-runtime"; import { ALERT_RULE_WRITE_FLAGS, buildAlertRuleReqDTO, @@ -35,7 +34,7 @@ export default defineTelemetryCommand({ validate: (flags) => validateAlertRuleFlags(flags), async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const format = settings.output; const reqDTO = { ...(settings.workspaceId ? { workspaceId: settings.workspaceId } : {}), @@ -43,14 +42,11 @@ export default defineTelemetryCommand({ }; if (settings.dryRun) { - emitResult( - { - api: CREATE_RULE_API, - data: { reqDTO }, - ...telemetryGatewayPreview(settings), - }, - format, - ); + ctx.output.result({ + api: CREATE_RULE_API, + data: { reqDTO }, + ...telemetryGatewayPreview(settings), + }); return; } @@ -60,11 +56,16 @@ export default defineTelemetryCommand({ const resp = unwrapResponse(raw as Record); const ruleId = (resp.data ?? resp.ruleId ?? resp) as string | undefined; - if (format === "json") { - emitResult({ created: true, ruleId }, format); - return; - } - - process.stdout.write(`Alert rule created${ruleId ? `: ${ruleId}` : "."}\n`); + ctx.output.result( + { created: true, ruleId }, + format === "text" + ? { + text: ctx.localize({ + "en-US": `Alert rule created${ruleId ? `: ${ruleId}` : "."}`, + "zh-CN": `告警规则已创建${ruleId ? `: ${ruleId}` : "。"}`, + }), + } + : undefined, + ); }, }); diff --git a/packages/commands/src/commands/alert/delete.ts b/packages/commands/src/commands/alert/delete.ts index 7241df604..6922807fb 100644 --- a/packages/commands/src/commands/alert/delete.ts +++ b/packages/commands/src/commands/alert/delete.ts @@ -1,6 +1,4 @@ -import { detectOutputFormat } from "bailian-cli-core"; import { defineTelemetryCommand, telemetryGatewayPreview } from "../shared/telemetry.ts"; -import { emitResult } from "bailian-cli-runtime"; import { parseCommaList } from "../shared/params.ts"; import { ensureAlertReady } from "./shared.ts"; @@ -40,19 +38,16 @@ export default defineTelemetryCommand({ ], async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const format = settings.output; const reqDTO = { ruleIds: parseCommaList(flags.ruleId) }; if (settings.dryRun) { - emitResult( - { - api: DELETE_RULES_API, - data: { reqDTO }, - ...telemetryGatewayPreview(settings), - }, - format, - ); + ctx.output.result({ + api: DELETE_RULES_API, + data: { reqDTO }, + ...telemetryGatewayPreview(settings), + }); return; } @@ -60,11 +55,16 @@ export default defineTelemetryCommand({ await ctx.client.console(DELETE_RULES_API, { reqDTO }); - if (format === "json") { - emitResult({ deleted: reqDTO.ruleIds }, format); - return; - } - - process.stdout.write(`Deleted ${reqDTO.ruleIds.length} alert rule(s).\n`); + ctx.output.result( + { deleted: reqDTO.ruleIds }, + format === "text" + ? { + text: ctx.localize({ + "en-US": `Deleted ${reqDTO.ruleIds.length} alert rule(s).`, + "zh-CN": `已删除 ${reqDTO.ruleIds.length} 个告警规则。`, + }), + } + : undefined, + ); }, }); diff --git a/packages/commands/src/commands/alert/disable.ts b/packages/commands/src/commands/alert/disable.ts index 52d542faa..9863d02fd 100644 --- a/packages/commands/src/commands/alert/disable.ts +++ b/packages/commands/src/commands/alert/disable.ts @@ -1,6 +1,4 @@ -import { detectOutputFormat } from "bailian-cli-core"; import { defineTelemetryCommand, telemetryGatewayPreview } from "../shared/telemetry.ts"; -import { emitResult } from "bailian-cli-runtime"; import { ensureAlertReady } from "./shared.ts"; const DISABLE_RULE_API = "zeldaEasy.bailian-telemetry.alertRule.disableAlertRule"; @@ -26,7 +24,7 @@ export default defineTelemetryCommand({ exampleArgs: ["--rule-id 789", "--rule-id 789 --dry-run"], async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const format = settings.output; const reqDTO = { workspaceId: settings.workspaceId, @@ -35,14 +33,11 @@ export default defineTelemetryCommand({ }; if (settings.dryRun) { - emitResult( - { - api: DISABLE_RULE_API, - data: { reqDTO }, - ...telemetryGatewayPreview(settings), - }, - format, - ); + ctx.output.result({ + api: DISABLE_RULE_API, + data: { reqDTO }, + ...telemetryGatewayPreview(settings), + }); return; } @@ -50,11 +45,16 @@ export default defineTelemetryCommand({ await ctx.client.console(DISABLE_RULE_API, { reqDTO }); - if (format === "json") { - emitResult({ enabled: false, ruleId: flags.ruleId }, format); - return; - } - - process.stdout.write(`Alert rule disabled: ${flags.ruleId}\n`); + ctx.output.result( + { enabled: false, ruleId: flags.ruleId }, + format === "text" + ? { + text: ctx.localize({ + "en-US": `Alert rule disabled: ${flags.ruleId}`, + "zh-CN": `告警规则已停用: ${flags.ruleId}`, + }), + } + : undefined, + ); }, }); diff --git a/packages/commands/src/commands/alert/enable.ts b/packages/commands/src/commands/alert/enable.ts index 3ccf60479..f57ff2e39 100644 --- a/packages/commands/src/commands/alert/enable.ts +++ b/packages/commands/src/commands/alert/enable.ts @@ -1,6 +1,4 @@ -import { detectOutputFormat } from "bailian-cli-core"; import { defineTelemetryCommand, telemetryGatewayPreview } from "../shared/telemetry.ts"; -import { emitResult } from "bailian-cli-runtime"; import { ensureAlertReady } from "./shared.ts"; export default defineTelemetryCommand({ @@ -24,7 +22,7 @@ export default defineTelemetryCommand({ exampleArgs: ["--rule-id 789", "--rule-id 789 --dry-run"], async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const format = settings.output; const reqDTO = { workspaceId: settings.workspaceId, @@ -33,14 +31,11 @@ export default defineTelemetryCommand({ }; if (settings.dryRun) { - emitResult( - { - api: "zeldaEasy.bailian-telemetry.alertRule.enableAlertRule", - data: { reqDTO }, - ...telemetryGatewayPreview(settings), - }, - format, - ); + ctx.output.result({ + api: "zeldaEasy.bailian-telemetry.alertRule.enableAlertRule", + data: { reqDTO }, + ...telemetryGatewayPreview(settings), + }); return; } @@ -48,11 +43,16 @@ export default defineTelemetryCommand({ await ctx.client.console("zeldaEasy.bailian-telemetry.alertRule.enableAlertRule", { reqDTO }); - if (format === "json") { - emitResult({ enabled: true, ruleId: flags.ruleId }, format); - return; - } - - process.stdout.write(`Alert rule enabled: ${flags.ruleId}\n`); + ctx.output.result( + { enabled: true, ruleId: flags.ruleId }, + format === "text" + ? { + text: ctx.localize({ + "en-US": `Alert rule enabled: ${flags.ruleId}`, + "zh-CN": `告警规则已启用: ${flags.ruleId}`, + }), + } + : undefined, + ); }, }); diff --git a/packages/commands/src/commands/alert/history.ts b/packages/commands/src/commands/alert/history.ts index 799e32a3c..cb8480a22 100644 --- a/packages/commands/src/commands/alert/history.ts +++ b/packages/commands/src/commands/alert/history.ts @@ -1,6 +1,5 @@ -import { detectOutputFormat } from "bailian-cli-core"; import { defineTelemetryCommand, telemetryGatewayPreview } from "../shared/telemetry.ts"; -import { ansi, emitResult, renderBoxTable } from "bailian-cli-runtime"; +import { ansi, renderBoxTable } from "bailian-cli-runtime"; import { formatDateTime } from "../shared/format.ts"; import { TELEMETRY_TIME_FLAGS, resolveTimeRange } from "../shared/telemetry.ts"; import { ALERT_LEVELS, ensureAlertReady, extractAlertPage } from "./shared.ts"; @@ -105,7 +104,7 @@ export default defineTelemetryCommand({ }, async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const format = settings.output; const { startTime, endTime } = resolveTimeRange(flags); const reqDTO = { @@ -123,14 +122,11 @@ export default defineTelemetryCommand({ }; if (settings.dryRun) { - emitResult( - { - api: LIST_HISTORIES_API, - data: { reqDTO }, - ...telemetryGatewayPreview(settings), - }, - format, - ); + ctx.output.result({ + api: LIST_HISTORIES_API, + data: { reqDTO }, + ...telemetryGatewayPreview(settings), + }); return; } @@ -139,38 +135,49 @@ export default defineTelemetryCommand({ const raw = await ctx.client.console(LIST_HISTORIES_API, { reqDTO }); const page = extractAlertPage(raw); - if (format === "json") { - emitResult(page, format); - return; - } - - if (page.list.length === 0) { - process.stdout.write("No alert history found in this range.\n"); - return; - } - - const color = ansi(process.stdout); - const lines = renderBoxTable({ - headers: ["Start Time", "Rule", "Status", "Max Level", "Count", "Instance"], - rows: page.list.map((item) => [ - item.startTime ? formatDateTime(item.startTime) : "-", - item.ruleDisplayName ?? item.alertRuleId, - item.status ?? "-", - item.maxLevel ?? "-", - String(item.count ?? 0), - instanceText(item), - ]), - align: ["left", "left", "left", "left", "right", "left"], - cellColor: (_rowIndex, colIndex, value) => { - if (colIndex === 2) return value === "ALARM" ? color.red(value) : color.green(value); - if (colIndex === 3 && value === "ERROR") return color.red(value); - return undefined; - }, - }); - for (const line of lines) process.stdout.write(line + "\n"); - - if (page.nextToken) { - process.stdout.write(`Next page: --next-token ${page.nextToken}\n`); + let text: string | undefined; + if (format === "text") { + if (page.list.length === 0) { + text = ctx.localize({ + "en-US": "No alert history found in this range.", + "zh-CN": "此时间范围内未找到告警历史。", + }); + } else { + const color = ansi(process.stdout); + const lines = renderBoxTable({ + headers: [ + ctx.localize({ "en-US": "Start Time", "zh-CN": "开始时间" }), + ctx.localize({ "en-US": "Rule", "zh-CN": "规则" }), + ctx.localize({ "en-US": "Status", "zh-CN": "状态" }), + ctx.localize({ "en-US": "Max Level", "zh-CN": "最高级别" }), + ctx.localize({ "en-US": "Count", "zh-CN": "次数" }), + ctx.localize({ "en-US": "Instance", "zh-CN": "实例" }), + ], + rows: page.list.map((item) => [ + item.startTime ? formatDateTime(item.startTime) : "-", + item.ruleDisplayName ?? item.alertRuleId, + item.status ?? "-", + item.maxLevel ?? "-", + String(item.count ?? 0), + instanceText(item), + ]), + align: ["left", "left", "left", "left", "right", "left"], + cellColor: (_rowIndex, colIndex, value) => { + if (colIndex === 2) return value === "ALARM" ? color.red(value) : color.green(value); + if (colIndex === 3 && value === "ERROR") return color.red(value); + return undefined; + }, + }); + if (page.nextToken) + lines.push( + ctx.localize({ + "en-US": `Next page: --next-token ${page.nextToken}`, + "zh-CN": `下一页: --next-token ${page.nextToken}`, + }), + ); + text = lines.join("\n"); + } } + ctx.output.result(page, text === undefined ? undefined : { text }); }, }); diff --git a/packages/commands/src/commands/alert/list.ts b/packages/commands/src/commands/alert/list.ts index cd6d39b3b..b22c413d5 100644 --- a/packages/commands/src/commands/alert/list.ts +++ b/packages/commands/src/commands/alert/list.ts @@ -1,6 +1,6 @@ -import { detectOutputFormat } from "bailian-cli-core"; +import type { LocalizedText } from "bailian-cli-core"; import { defineTelemetryCommand, telemetryGatewayPreview } from "../shared/telemetry.ts"; -import { ansi, emitResult, renderBoxTable } from "bailian-cli-runtime"; +import { ansi, renderBoxTable } from "bailian-cli-runtime"; import { ensureAlertReady, extractAlertPage, ALERT_LEVELS } from "./shared.ts"; const LIST_RULES_API = "zeldaEasy.bailian-telemetry.alertRule.listAlertRules"; @@ -16,16 +16,22 @@ interface AlertRuleItem { gmtModified?: number; } -function printTable(list: AlertRuleItem[]): void { +function ruleTable(list: AlertRuleItem[], localize: (text: LocalizedText) => string): string[] { const color = ansi(process.stdout); if (list.length === 0) { - process.stdout.write("No alert rules found.\n"); - return; + return [localize({ "en-US": "No alert rules found.", "zh-CN": "未找到告警规则。" })]; } const lines = renderBoxTable({ - headers: ["Rule ID", "Name", "Level", "Enabled", "Models", "Interval"], + headers: [ + localize({ "en-US": "Rule ID", "zh-CN": "规则 ID" }), + localize({ "en-US": "Name", "zh-CN": "名称" }), + localize({ "en-US": "Level", "zh-CN": "级别" }), + localize({ "en-US": "Enabled", "zh-CN": "启用" }), + localize({ "en-US": "Models", "zh-CN": "模型" }), + localize({ "en-US": "Interval", "zh-CN": "间隔" }), + ], rows: list.map((rule) => [ rule.ruleId, rule.name, @@ -41,7 +47,7 @@ function printTable(list: AlertRuleItem[]): void { return undefined; }, }); - for (const line of lines) process.stdout.write(line + "\n"); + return lines; } export default defineTelemetryCommand({ @@ -113,7 +119,7 @@ export default defineTelemetryCommand({ exampleArgs: ["", "--enabled true", "--name 失败率 --level ERROR", "--output json"], async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const format = settings.output; const reqDTO = { workspaceId: settings.workspaceId, @@ -129,14 +135,11 @@ export default defineTelemetryCommand({ }; if (settings.dryRun) { - emitResult( - { - api: LIST_RULES_API, - data: { reqDTO }, - ...telemetryGatewayPreview(settings), - }, - format, - ); + ctx.output.result({ + api: LIST_RULES_API, + data: { reqDTO }, + ...telemetryGatewayPreview(settings), + }); return; } @@ -145,14 +148,23 @@ export default defineTelemetryCommand({ const raw = await ctx.client.console(LIST_RULES_API, { reqDTO }); const page = extractAlertPage(raw); - if (format === "json") { - emitResult(page, format); - return; - } - - printTable(page.list); - if (page.nextToken) { - process.stdout.write(`Next page: --next-token ${page.nextToken}\n`); - } + ctx.output.result( + page, + format === "text" + ? { + text: [ + ...ruleTable(page.list, ctx.localize), + ...(page.nextToken + ? [ + ctx.localize({ + "en-US": `Next page: --next-token ${page.nextToken}`, + "zh-CN": `下一页: --next-token ${page.nextToken}`, + }), + ] + : []), + ].join("\n"), + } + : undefined, + ); }, }); diff --git a/packages/commands/src/commands/alert/metrics.ts b/packages/commands/src/commands/alert/metrics.ts index e356f68e3..edef48f1f 100644 --- a/packages/commands/src/commands/alert/metrics.ts +++ b/packages/commands/src/commands/alert/metrics.ts @@ -1,6 +1,6 @@ -import { detectOutputFormat, unwrapResponse } from "bailian-cli-core"; +import { unwrapResponse } from "bailian-cli-core"; import { defineTelemetryCommand, telemetryGatewayPreview } from "../shared/telemetry.ts"; -import { emitResult, renderBoxTable } from "bailian-cli-runtime"; +import { renderBoxTable } from "bailian-cli-runtime"; import { ensureAlertReady } from "./shared.ts"; const LIST_METRICS_API = "zeldaEasy.bailian-telemetry.alertMetric.listMetrics"; @@ -23,17 +23,14 @@ export default defineTelemetryCommand({ exampleArgs: ["", "--output json"], async run(ctx) { const { settings } = ctx; - const format = detectOutputFormat(settings.output); + const format = settings.output; if (settings.dryRun) { - emitResult( - { - api: LIST_METRICS_API, - data: { reqDTO: { workspaceId: settings.workspaceId, resourceType: "model" } }, - ...telemetryGatewayPreview(settings), - }, - format, - ); + ctx.output.result({ + api: LIST_METRICS_API, + data: { reqDTO: { workspaceId: settings.workspaceId, resourceType: "model" } }, + ...telemetryGatewayPreview(settings), + }); return; } @@ -47,26 +44,32 @@ export default defineTelemetryCommand({ Array.isArray(resp) ? resp : (((resp.list ?? resp.result) as unknown[]) ?? []) ) as AlertMetric[]; - if (format === "json") { - emitResult(list, format); - return; - } - - if (list.length === 0) { - process.stdout.write("No alertable metrics found.\n"); - return; + let text: string | undefined; + if (format === "text") { + if (list.length === 0) { + text = ctx.localize({ + "en-US": "No alertable metrics found.", + "zh-CN": "未找到可告警的指标。", + }); + } else { + const lines = renderBoxTable({ + headers: [ + ctx.localize({ "en-US": "Metric", "zh-CN": "指标" }), + ctx.localize({ "en-US": "Aggregations", "zh-CN": "聚合方式" }), + ctx.localize({ "en-US": "Unit", "zh-CN": "单位" }), + ctx.localize({ "en-US": "Description", "zh-CN": "说明" }), + ], + rows: list.map((metric) => [ + metric.metricName, + (metric.aggregators ?? []).join(", "), + metric.unit ?? "-", + metric.desc ?? "-", + ]), + align: ["left", "left", "left", "left"], + }); + text = lines.join("\n"); + } } - - const lines = renderBoxTable({ - headers: ["Metric", "Aggregations", "Unit", "Description"], - rows: list.map((metric) => [ - metric.metricName, - (metric.aggregators ?? []).join(", "), - metric.unit ?? "-", - metric.desc ?? "-", - ]), - align: ["left", "left", "left", "left"], - }); - for (const line of lines) process.stdout.write(line + "\n"); + ctx.output.result(list, text === undefined ? undefined : { text }); }, }); diff --git a/packages/commands/src/commands/alert/template-create.ts b/packages/commands/src/commands/alert/template-create.ts index 0c96c2d3f..467eb55e1 100644 --- a/packages/commands/src/commands/alert/template-create.ts +++ b/packages/commands/src/commands/alert/template-create.ts @@ -1,5 +1,4 @@ -import { BailianError, ExitCode, detectOutputFormat, unwrapResponse } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; +import { BailianError, ExitCode, unwrapResponse } from "bailian-cli-core"; import { defineTelemetryCommand, telemetryGatewayPreview } from "../shared/telemetry.ts"; import { ensureAlertReady, @@ -101,27 +100,24 @@ export default defineTelemetryCommand({ validate: (flags) => validateTemplateConditions(flags.condition, Boolean(flags.from)), async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const format = settings.output; const conditions = flags.condition?.length ? flags.condition.map(parseCondition) : undefined; if (settings.dryRun) { - emitResult( - { - api: CREATE_TEMPLATE_API, - data: { - reqDTO: { - templateName: flags.name, - resourceType: "model", - logicalOperator: flags.logicalOperator ?? "or", - srcTemplateId: flags.from, - conditions: conditions ?? "(copied from --from template)", - }, + ctx.output.result({ + api: CREATE_TEMPLATE_API, + data: { + reqDTO: { + templateName: flags.name, + resourceType: "model", + logicalOperator: flags.logicalOperator ?? "or", + srcTemplateId: flags.from, + conditions: conditions ?? "(copied from --from template)", }, - ...telemetryGatewayPreview(settings), }, - format, - ); + ...telemetryGatewayPreview(settings), + }); return; } @@ -139,11 +135,16 @@ export default defineTelemetryCommand({ const resp = unwrapResponse(raw as Record); const templateId = (resp.data ?? resp.templateId ?? resp) as string | undefined; - if (format === "json") { - emitResult({ created: true, templateId }, format); - return; - } - - process.stdout.write(`Alert template created${templateId ? `: ${templateId}` : "."}\n`); + ctx.output.result( + { created: true, templateId }, + format === "text" + ? { + text: ctx.localize({ + "en-US": `Alert template created${templateId ? `: ${templateId}` : "."}`, + "zh-CN": `告警模板已创建${templateId ? `: ${templateId}` : "。"}`, + }), + } + : undefined, + ); }, }); diff --git a/packages/commands/src/commands/alert/template-delete.ts b/packages/commands/src/commands/alert/template-delete.ts index a87ece6db..4ab0efc7c 100644 --- a/packages/commands/src/commands/alert/template-delete.ts +++ b/packages/commands/src/commands/alert/template-delete.ts @@ -1,5 +1,4 @@ -import { detectOutputFormat, unwrapResponse } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; +import { unwrapResponse } from "bailian-cli-core"; import { parseCommaList } from "../shared/params.ts"; import { ensureAlertReady } from "./shared.ts"; import { defineTelemetryCommand, telemetryGatewayPreview } from "../shared/telemetry.ts"; @@ -40,19 +39,16 @@ export default defineTelemetryCommand({ ], async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const format = settings.output; const reqDTO = { templateIds: parseCommaList(flags.templateId) }; if (settings.dryRun) { - emitResult( - { - api: DELETE_TEMPLATES_API, - data: { reqDTO }, - ...telemetryGatewayPreview(settings), - }, - format, - ); + ctx.output.result({ + api: DELETE_TEMPLATES_API, + data: { reqDTO }, + ...telemetryGatewayPreview(settings), + }); return; } @@ -61,11 +57,16 @@ export default defineTelemetryCommand({ const raw = await ctx.client.console(DELETE_TEMPLATES_API, { reqDTO }); const resp = unwrapResponse(raw as Record); - if (format === "json") { - emitResult({ deleted: reqDTO.templateIds, result: resp }, format); - return; - } - - process.stdout.write(`Deleted ${reqDTO.templateIds.length} alert template(s).\n`); + ctx.output.result( + { deleted: reqDTO.templateIds, result: resp }, + format === "text" + ? { + text: ctx.localize({ + "en-US": `Deleted ${reqDTO.templateIds.length} alert template(s).`, + "zh-CN": `已删除 ${reqDTO.templateIds.length} 个告警模板。`, + }), + } + : undefined, + ); }, }); diff --git a/packages/commands/src/commands/alert/template-list.ts b/packages/commands/src/commands/alert/template-list.ts index 5fdb303ee..fbe98a615 100644 --- a/packages/commands/src/commands/alert/template-list.ts +++ b/packages/commands/src/commands/alert/template-list.ts @@ -1,6 +1,5 @@ -import { detectOutputFormat } from "bailian-cli-core"; import { defineTelemetryCommand, telemetryGatewayPreview } from "../shared/telemetry.ts"; -import { emitResult, renderBoxTable } from "bailian-cli-runtime"; +import { renderBoxTable } from "bailian-cli-runtime"; import { ensureAlertReady, extractAlertPage } from "./shared.ts"; const LIST_TEMPLATES_API = "zeldaEasy.bailian-telemetry.alertTemplate.listAlertTemplates"; @@ -82,7 +81,7 @@ export default defineTelemetryCommand({ exampleArgs: ["", "--source Official", "--name 失败率", "--template-id 123 --output json"], async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const format = settings.output; const reqDTO = { resourceType: "model", @@ -94,14 +93,11 @@ export default defineTelemetryCommand({ }; if (settings.dryRun) { - emitResult( - { - api: LIST_TEMPLATES_API, - data: { reqDTO }, - ...telemetryGatewayPreview(settings), - }, - format, - ); + ctx.output.result({ + api: LIST_TEMPLATES_API, + data: { reqDTO }, + ...telemetryGatewayPreview(settings), + }); return; } @@ -110,28 +106,37 @@ export default defineTelemetryCommand({ const raw = await ctx.client.console(LIST_TEMPLATES_API, { reqDTO }); const page = extractAlertPage(raw); - if (format === "json") { - emitResult(page, format); - return; + let text: string | undefined; + if (format === "text") { + if (page.list.length === 0) { + text = ctx.localize({ "en-US": "No alert templates found.", "zh-CN": "未找到告警模板。" }); + } else { + const lines = renderBoxTable({ + headers: [ + ctx.localize({ "en-US": "Template ID", "zh-CN": "模板 ID" }), + ctx.localize({ "en-US": "Name", "zh-CN": "名称" }), + ctx.localize({ "en-US": "Source", "zh-CN": "来源" }), + ctx.localize({ "en-US": "Conditions", "zh-CN": "条件" }), + ], + rows: page.list.map((template) => [ + template.templateId, + template.templateName, + template.source === "Official" + ? ctx.localize({ "en-US": "Official", "zh-CN": "官方" }) + : ctx.localize({ "en-US": "Custom", "zh-CN": "自定义" }), + (template.conditions ?? []) + .map(formatCondition) + .join( + template.logicalOperator === "and" + ? ctx.localize({ "en-US": " AND ", "zh-CN": " 且 " }) + : ctx.localize({ "en-US": " OR ", "zh-CN": " 或 " }), + ) || "-", + ]), + align: ["left", "left", "left", "left"], + }); + text = lines.join("\n"); + } } - - if (page.list.length === 0) { - process.stdout.write("No alert templates found.\n"); - return; - } - - const lines = renderBoxTable({ - headers: ["Template ID", "Name", "Source", "Conditions"], - rows: page.list.map((template) => [ - template.templateId, - template.templateName, - template.source === "Official" ? "Official" : "Custom", - (template.conditions ?? []) - .map(formatCondition) - .join(template.logicalOperator === "and" ? " AND " : " OR ") || "-", - ]), - align: ["left", "left", "left", "left"], - }); - for (const line of lines) process.stdout.write(line + "\n"); + ctx.output.result(page, text === undefined ? undefined : { text }); }, }); diff --git a/packages/commands/src/commands/alert/template-update.ts b/packages/commands/src/commands/alert/template-update.ts index 689bbf2f0..0ff7cc393 100644 --- a/packages/commands/src/commands/alert/template-update.ts +++ b/packages/commands/src/commands/alert/template-update.ts @@ -1,6 +1,4 @@ -import { detectOutputFormat } from "bailian-cli-core"; import { defineTelemetryCommand, telemetryGatewayPreview } from "../shared/telemetry.ts"; -import { emitResult } from "bailian-cli-runtime"; import { ensureAlertReady, parseCondition, validateTemplateConditions } from "./shared.ts"; const UPDATE_TEMPLATE_API = "zeldaEasy.bailian-telemetry.alertTemplate.updateAlertTemplate"; @@ -59,7 +57,7 @@ export default defineTelemetryCommand({ validate: (flags) => validateTemplateConditions(flags.condition, false), async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const format = settings.output; const reqDTO = { templateId: flags.templateId, @@ -70,14 +68,11 @@ export default defineTelemetryCommand({ }; if (settings.dryRun) { - emitResult( - { - api: UPDATE_TEMPLATE_API, - data: { reqDTO }, - ...telemetryGatewayPreview(settings), - }, - format, - ); + ctx.output.result({ + api: UPDATE_TEMPLATE_API, + data: { reqDTO }, + ...telemetryGatewayPreview(settings), + }); return; } @@ -85,11 +80,16 @@ export default defineTelemetryCommand({ await ctx.client.console(UPDATE_TEMPLATE_API, { reqDTO }); - if (format === "json") { - emitResult({ updated: true, templateId: flags.templateId }, format); - return; - } - - process.stdout.write(`Alert template updated: ${flags.templateId}\n`); + ctx.output.result( + { updated: true, templateId: flags.templateId }, + format === "text" + ? { + text: ctx.localize({ + "en-US": `Alert template updated: ${flags.templateId}`, + "zh-CN": `告警模板已更新: ${flags.templateId}`, + }), + } + : undefined, + ); }, }); diff --git a/packages/commands/src/commands/alert/update.ts b/packages/commands/src/commands/alert/update.ts index fb040ff61..cfe5af36a 100644 --- a/packages/commands/src/commands/alert/update.ts +++ b/packages/commands/src/commands/alert/update.ts @@ -1,6 +1,4 @@ -import { detectOutputFormat } from "bailian-cli-core"; import { defineTelemetryCommand, telemetryGatewayPreview } from "../shared/telemetry.ts"; -import { emitResult } from "bailian-cli-runtime"; import { ALERT_RULE_WRITE_FLAGS, buildAlertRuleReqDTO, @@ -37,7 +35,7 @@ export default defineTelemetryCommand({ validate: (flags) => validateAlertRuleFlags(flags), async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const format = settings.output; const reqDTO = { ...(settings.workspaceId ? { workspaceId: settings.workspaceId } : {}), @@ -46,14 +44,11 @@ export default defineTelemetryCommand({ }; if (settings.dryRun) { - emitResult( - { - api: UPDATE_RULE_API, - data: { reqDTO }, - ...telemetryGatewayPreview(settings), - }, - format, - ); + ctx.output.result({ + api: UPDATE_RULE_API, + data: { reqDTO }, + ...telemetryGatewayPreview(settings), + }); return; } @@ -61,11 +56,16 @@ export default defineTelemetryCommand({ await ctx.client.console(UPDATE_RULE_API, { reqDTO }); - if (format === "json") { - emitResult({ updated: true, ruleId: flags.ruleId }, format); - return; - } - - process.stdout.write(`Alert rule updated: ${flags.ruleId}\n`); + ctx.output.result( + { updated: true, ruleId: flags.ruleId }, + format === "text" + ? { + text: ctx.localize({ + "en-US": `Alert rule updated: ${flags.ruleId}`, + "zh-CN": `告警规则已更新: ${flags.ruleId}`, + }), + } + : undefined, + ); }, }); diff --git a/packages/commands/src/commands/app/call.ts b/packages/commands/src/commands/app/call.ts index 5c9b04708..d511236d6 100644 --- a/packages/commands/src/commands/app/call.ts +++ b/packages/commands/src/commands/app/call.ts @@ -1,14 +1,13 @@ +import { parseModelStreamEvent } from "../shared/model-stream.ts"; import { defineCommand, UsageError, appCompletionPath, parseSSE, - detectOutputFormat, type AppCompletionRequest, type AppStreamChunk, type AppCompletionResponse, } from "bailian-cli-core"; -import { ansi, emitResult, emitBare } from "bailian-cli-runtime"; export default defineCommand({ description: { @@ -123,7 +122,7 @@ export default defineCommand({ const prompt = flags.prompt; const shouldStream = flags.stream || process.stdout.isTTY; - const format = detectOutputFormat(settings.output); + const format = settings.output; const body: AppCompletionRequest = { input: { prompt }, @@ -173,7 +172,7 @@ export default defineCommand({ } if (settings.dryRun) { - emitResult({ endpoint: ctx.client.url(appCompletionPath(appId)), request: body }, format); + ctx.output.result({ endpoint: ctx.client.url(appCompletionPath(appId)), request: body }); return; } @@ -190,53 +189,52 @@ export default defineCommand({ let fullText = ""; let sessionId = ""; const writesStreamingStdout = format === "text"; - const stderrColor = ansi(process.stderr); for await (const event of parseSSE(res)) { if (event.data === "[DONE]") break; - try { - const chunk = JSON.parse(event.data) as AppStreamChunk; - const text = chunk.output?.text; - - if (text) { - // incremental_output: text is delta - if (writesStreamingStdout) process.stdout.write(text); - fullText += text; - } + const chunk = parseModelStreamEvent(event) as unknown as AppStreamChunk | undefined; + if (!chunk) continue; + const text = chunk.output?.text; + + if (text) { + // incremental_output: text is delta + if (writesStreamingStdout) process.stdout.write(text); + fullText += text; + } - // Capture session_id for multi-turn - if (chunk.output?.session_id) { - sessionId = chunk.output.session_id; - } + // Capture session_id for multi-turn + if (chunk.output?.session_id) { + sessionId = chunk.output.session_id; + } - // Show thoughts if available - if (chunk.output?.thoughts && flags.hasThoughts) { - for (const t of chunk.output.thoughts) { - if (t.thought) - process.stderr.write(`${stderrColor.dim(`[Thinking] ${t.thought}`)}\n`); - if (t.action_name) - process.stderr.write( - `${stderrColor.dim(`[Action] ${t.action_name}: ${t.action_input || ""}`)}\n`, - ); - if (t.observation) - process.stderr.write(`${stderrColor.dim(`[Observation] ${t.observation}`)}\n`); - } + // Explicit thoughts remain process information, separate from the final result. + if (chunk.output?.thoughts && flags.hasThoughts) { + for (const thought of chunk.output.thoughts) { + if (thought.thought) + ctx.output.info({ + "en-US": `[Thinking] ${thought.thought}`, + "zh-CN": `[思考] ${thought.thought}`, + }); + if (thought.action_name) + ctx.output.info({ + "en-US": `[Action] ${thought.action_name}: ${thought.action_input || ""}`, + "zh-CN": `[操作] ${thought.action_name}: ${thought.action_input || ""}`, + }); + if (thought.observation) + ctx.output.info({ + "en-US": `[Observation] ${thought.observation}`, + "zh-CN": `[观察] ${thought.observation}`, + }); } - } catch { - // skip unparseable } } - // Show session_id for multi-turn conversation - if (sessionId && !settings.quiet) { - process.stderr.write(`${stderrColor.dim(`Session ID: ${sessionId}`)}\n`); - } - - if (format === "json") { - emitResult({ text: fullText, session_id: sessionId }, format); - } else { - process.stdout.write("\n"); - } + if (sessionId) + ctx.output.info({ "en-US": `Session ID: ${sessionId}`, "zh-CN": `会话 ID: ${sessionId}` }); + ctx.output.result( + { text: fullText, session_id: sessionId }, + format === "text" ? { text: "" } : undefined, + ); } else { const response = await ctx.client.requestJson({ path: appCompletionPath(appId), @@ -246,11 +244,7 @@ export default defineCommand({ const text = response.output?.text ?? ""; - if (settings.quiet || format === "text") { - emitBare(text); - } else { - emitResult(response, format); - } + ctx.output.result(response, format === "text" ? { text } : undefined); } }, }); diff --git a/packages/commands/src/commands/app/list.ts b/packages/commands/src/commands/app/list.ts index f2d4b5561..85189f1ec 100644 --- a/packages/commands/src/commands/app/list.ts +++ b/packages/commands/src/commands/app/list.ts @@ -1,5 +1,4 @@ -import { defineCommand, detectOutputFormat } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; +import { defineCommand } from "bailian-cli-core"; const APP_LIST_API = "zeldaEasy.broadscope-bailian.app-control.list"; @@ -41,7 +40,6 @@ export default defineCommand({ const name = flags.name || ""; const pageNo = flags.page || 1; const pageSize = flags.pageSize || 30; - const format = detectOutputFormat(settings.output); const data = { reqDTO: { @@ -55,7 +53,7 @@ export default defineCommand({ }; if (settings.dryRun) { - emitResult({ api: APP_LIST_API, data }, format); + ctx.output.result({ api: APP_LIST_API, data }); return; } @@ -70,6 +68,6 @@ export default defineCommand({ user_prompt_params: item.config?.user_prompt_params ?? [], })); - emitResult({ total, apps }, format); + ctx.output.result({ total, apps }); }, }); diff --git a/packages/commands/src/commands/auth/console-ui.ts b/packages/commands/src/commands/auth/console-ui.ts index 7acb48745..a96c255cc 100644 --- a/packages/commands/src/commands/auth/console-ui.ts +++ b/packages/commands/src/commands/auth/console-ui.ts @@ -1,6 +1,7 @@ import { maskToken, type AuthStore, + type CommandOutput, type Identity, type LocalizedText, type Settings, @@ -38,9 +39,10 @@ export function makeAuthUiBridge(ctx: { identity: Identity; settings: Settings; authStore: AuthStore; + output: Pick; localize?: (text: LocalizedText) => string; }): AuthUiBridge { - const { identity, settings, authStore, localize } = ctx; + const { identity, settings, authStore, localize, output } = ctx; return { status() { const a = authStore.describe(); @@ -70,13 +72,16 @@ export function makeAuthUiBridge(ctx: { // endpoint to detect completion. Errors are logged, not surfaced. void runConsoleLogin( origin, - { identity, settings, authStore, localize }, + { identity, settings, authStore, localize, output }, { needApiKey: !hasApiKey, }, ).catch((err: unknown) => { const msg = err instanceof Error ? err.message : String(err); - process.stderr.write(`console login failed: ${msg}\n`); + output.warning({ + message: { "en-US": `Console login failed: ${msg}`, "zh-CN": `控制台登录失败:${msg}` }, + code: "CONSOLE_LOGIN_FAILED", + }); }); }, logout() { diff --git a/packages/commands/src/commands/auth/generate-access-token.ts b/packages/commands/src/commands/auth/generate-access-token.ts index fd95d0722..9cf89bb6a 100644 --- a/packages/commands/src/commands/auth/generate-access-token.ts +++ b/packages/commands/src/commands/auth/generate-access-token.ts @@ -1,10 +1,4 @@ -import { - defineCommand, - detectOutputFormat, - generateCLIAccessToken, - type FlagsDef, -} from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; +import { defineCommand, generateCLIAccessToken, type FlagsDef } from "bailian-cli-core"; const FLAGS = { accessKeyId: { @@ -43,17 +37,17 @@ export default defineCommand({ exampleArgs: ["--access-key-id LTAIxxxxx --access-key-secret xxxxx --security-token "], async run(ctx) { const { identity, settings, flags } = ctx; - const format = detectOutputFormat(settings.output); const resp = await generateCLIAccessToken({ identity, settings, + debug: (record) => ctx.output.debug(record), baseUrl: ctx.client.baseUrl, accessKeyId: flags.accessKeyId, accessKeySecret: flags.accessKeySecret, securityToken: flags.securityToken || undefined, }); - emitResult(resp, format); + ctx.output.result(resp); }, }); diff --git a/packages/commands/src/commands/auth/login-console.ts b/packages/commands/src/commands/auth/login-console.ts index f8f499b8f..788ff9358 100644 --- a/packages/commands/src/commands/auth/login-console.ts +++ b/packages/commands/src/commands/auth/login-console.ts @@ -6,6 +6,7 @@ import { ExitCode, validateConsoleAccessToken, type ConsoleGatewayTarget, + type CommandOutput, type AuthPersistPatch, type AuthStore, type ConfigFile, @@ -21,6 +22,7 @@ export interface LoginDeps { identity: Identity; settings: Settings; authStore: AuthStore; + output: Pick; /** 缺省时用户可见新文案回退到 en-US。 */ localize?: (text: LocalizedText) => string; } @@ -418,18 +420,14 @@ export async function runConsoleTokenLogin( ExitCode.USAGE, ); } - await validateConsoleAccessToken({ ...target, token }, deps.settings); + await validateConsoleAccessToken({ ...target, token }, deps.settings, { + debug: deps.output.debug, + }); await deps.authStore.login({ access_token: token, console_site: target.site, console_region: target.region, }); - process.stderr.write( - `${localizeLogin(deps, { - "en-US": `Console token validated and saved to ${deps.authStore.path}`, - "zh-CN": `控制台 token 已校验并保存至 ${deps.authStore.path}`, - })}\n`, - ); } export async function runConsoleLogin( @@ -525,24 +523,12 @@ export async function runConsoleLogin( persistBaseUrl: baseUrl || undefined, persistPatch: callbackPatch, }); - process.stderr.write( - `${localizeLogin(deps, { - "en-US": `Config saved to ${deps.authStore.path}`, - "zh-CN": `配置已保存至 ${deps.authStore.path}`, - })}\n`, - ); } else { await deps.authStore.login({ ...callbackPatch, base_url: baseUrl || undefined, }); savedSessionWithoutApiKey = true; - process.stderr.write( - `${localizeLogin(deps, { - "en-US": `Config saved to ${deps.authStore.path}`, - "zh-CN": `配置已保存至 ${deps.authStore.path}`, - })}\n`, - ); } savedCredentials = true; } catch (error: unknown) { @@ -613,36 +599,26 @@ export async function runConsoleLogin( server.closeAllConnections(); }, CONSOLE_LOGIN_TIMEOUT_MS); }); + // Native login prompt: the URL must remain visible in quiet text sessions. + // The command rejects browser login in JSON before starting this server. process.stderr.write( - `${localizeLogin(deps, { - "en-US": "Open this login URL in a browser on this machine:", - "zh-CN": "请在本机浏览器中打开以下登录地址:", - })}\n${loginUrl}\n${localizeLogin(deps, { - "en-US": - "Waiting up to 15 minutes for credentials. If blocked, check local network permission or open the same URL in Firefox. Press Ctrl+C to cancel.", - "zh-CN": - "最多等待 15 分钟接收凭证。如被拦截,请检查本地网络权限或在 Firefox 打开同一地址。按 Ctrl+C 取消。", - })}\n`, + `${localizeLogin(deps, { "en-US": "Open this login URL in a browser on this machine:", "zh-CN": "请在本机浏览器中打开以下登录地址:" })}\n${loginUrl}\n${localizeLogin(deps, { "en-US": "Waiting up to 15 minutes for credentials. If blocked, check local network permission or open the same URL in Firefox. Press Ctrl+C to cancel.", "zh-CN": "最多等待 15 分钟接收凭证。如被拦截,请检查本地网络权限或在 Firefox 打开同一地址。按 Ctrl+C 取消。" })}\n`, ); if (!opts?.noBrowser) { void openInBrowser(loginUrl).then( () => { if (!finished) - process.stderr.write( - `${localizeLogin(deps, { - "en-US": "Opened the login page in your default browser.", - "zh-CN": "已在默认浏览器打开登录页面。", - })}\n`, - ); + deps.output.info({ + "en-US": "Opened the login page in your default browser.", + "zh-CN": "已在默认浏览器打开登录页面。", + }); }, () => { if (!finished) - process.stderr.write( - `${localizeLogin(deps, { - "en-US": "Could not open the default browser. Open the URL above manually.", - "zh-CN": "无法打开默认浏览器,请手动打开上方地址。", - })}\n`, - ); + deps.output.info({ + "en-US": "Could not open the default browser. Open the URL above manually.", + "zh-CN": "无法打开默认浏览器,请手动打开上方地址。", + }); }, ); } @@ -667,11 +643,12 @@ export async function runConsoleLogin( if (opts?.needApiKey && savedSessionWithoutApiKey) { const binName = deps.identity.binName; - process.stderr.write( - `${localizeLogin(deps, { + deps.output.warning({ + message: { "en-US": `Saved the console session only. Model commands still need an API key: ${binName} auth login --console, or ${binName} auth login --api-key `, "zh-CN": `仅保存了控制台会话。模型命令仍需要 API Key:${binName} auth login --console,或 ${binName} auth login --api-key `, - })}\n`, - ); + }, + code: "MODEL_API_KEY_MISSING", + }); } } diff --git a/packages/commands/src/commands/auth/login.ts b/packages/commands/src/commands/auth/login.ts index 216ab2cad..89d328004 100644 --- a/packages/commands/src/commands/auth/login.ts +++ b/packages/commands/src/commands/auth/login.ts @@ -1,5 +1,7 @@ import { defineCommand, + BailianError, + ExitCode, CONSOLE_AUTH_FLAGS, generateCLIAccessToken, resolveCLIAccessTokenTarget, @@ -7,7 +9,6 @@ import { getModelProfilePreset, normalizeModelBaseUrl, } from "bailian-cli-core"; -import { emitBare } from "bailian-cli-runtime"; import { persistApiKey, validateApiKey } from "./login-api-key.ts"; import { resolveConsoleOrigin, runConsoleLogin, runConsoleTokenLogin } from "./login-console.ts"; @@ -158,35 +159,72 @@ export default defineCommand({ async run(ctx) { const { identity, settings, flags } = ctx; const store = ctx.authStore; - const deps = { identity, settings, authStore: store, localize: ctx.localize }; + const deps = { + identity, + settings, + authStore: store, + localize: ctx.localize, + output: ctx.output, + debug: (record: Parameters[0]) => ctx.output.debug(record), + }; + const finish = (method: string, text: string) => + ctx.output.result( + { + authenticated: true, + method, + config: settings.configName ?? "default", + config_file: store.path, + }, + { text }, + ); const key = flags.apiKey; const baseUrl = flags.baseUrl ? normalizeModelBaseUrl(flags.baseUrl) : undefined; if (flags.console) { if (settings.dryRun) { - emitBare( - ctx.localize( - flags.tokenStdin - ? { - "en-US": - "Would read a console token from stdin, validate it and save it without opening a browser or local server.", - "zh-CN": "将从 stdin 读取控制台 token,校验后保存,不启动浏览器或本地服务。", - } - : flags.noBrowser + ctx.output.result( + { + action: "auth.login", + method: "console", + token_stdin: !!flags.tokenStdin, + no_browser: !!flags.noBrowser, + config_file: store.path, + }, + { + text: ctx.localize( + flags.tokenStdin ? { "en-US": - "Would bind a free port on 127.0.0.1 and print the console login URL without opening a browser.", - "zh-CN": "将在 127.0.0.1 监听空闲端口并打印控制台登录地址,不自动打开浏览器。", + "Would read a console token from stdin, validate it and save it without opening a browser or local server.", + "zh-CN": "将从 stdin 读取控制台 token,校验后保存,不启动浏览器或本地服务。", } - : { - "en-US": - "Would bind a free port on 127.0.0.1 and open the console login URL in your browser.", - "zh-CN": "将在 127.0.0.1 监听空闲端口并在浏览器中打开控制台登录地址。", - }, - ), + : flags.noBrowser + ? { + "en-US": + "Would bind a free port on 127.0.0.1 and print the console login URL without opening a browser.", + "zh-CN": + "将在 127.0.0.1 监听空闲端口并打印控制台登录地址,不自动打开浏览器。", + } + : { + "en-US": + "Would bind a free port on 127.0.0.1 and open the console login URL in your browser.", + "zh-CN": "将在 127.0.0.1 监听空闲端口并在浏览器中打开控制台登录地址。", + }, + ), + }, ); return; } + if (settings.output === "json" && !flags.tokenStdin) + throw new BailianError( + ctx.localize({ + "en-US": + "Browser login requires text output. Use --output text, or --console --token-stdin with --console-region for JSON output.", + "zh-CN": + "浏览器登录需要文本输出。请使用 --output text,或通过 --console --token-stdin 和 --console-region 使用 JSON 输出。", + }), + ExitCode.USAGE, + ); const hasApiKey = !!(key || store.stored().apiKey); // 本次登录站点:参数缺省时用配置默认(file 里上次登录存的站点)。 const site = flags.consoleSite || settings.consoleSite || "domestic"; @@ -196,32 +234,60 @@ export default defineCommand({ region: flags.consoleRegion!, switchAgent: settings.consoleSwitchAgent, }); + finish( + "console", + ctx.localize({ + "en-US": `Console token validated and saved to ${store.path}`, + "zh-CN": `控制台 token 已校验并保存至 ${store.path}`, + }), + ); return; } await runConsoleLogin(resolveConsoleOrigin(site), deps, { needApiKey: !hasApiKey, noBrowser: flags.noBrowser, }); + finish( + "console", + ctx.localize({ + "en-US": `Config saved to ${store.path}`, + "zh-CN": `配置已保存至 ${store.path}`, + }), + ); return; } if (flags.openApi) { if (settings.dryRun) { - emitBare("Would save OpenAPI AK/SK credentials and generate CLI access token."); + ctx.output.result( + { action: "auth.login", method: "openapi", config_file: store.path }, + { + text: ctx.localize({ + "en-US": "Would save OpenAPI AK/SK credentials and generate CLI access token.", + "zh-CN": "将保存 OpenAPI AK/SK 凭证并生成 CLI access token。", + }), + }, + ); return; } const resolvedBaseUrl = store.resolveBaseUrl(); const target = resolveCLIAccessTokenTarget(settings, resolvedBaseUrl); - process.stderr.write("Generating CLI access token... "); + ctx.output.progress({ + "en-US": "Generating CLI access token...", + "zh-CN": "正在生成 CLI access token...", + }); const resp = await generateCLIAccessToken({ identity, settings, + debug: (record) => ctx.output.debug(record), baseUrl: resolvedBaseUrl, accessKeyId: flags.accessKeyId!, accessKeySecret: flags.accessKeySecret!, }); const accessToken = resp.cliAccessToken; - await validateConsoleAccessToken({ ...target, token: accessToken }, settings); + await validateConsoleAccessToken({ ...target, token: accessToken }, settings, { + debug: (record) => ctx.output.debug(record), + }); await store.login({ access_key_id: flags.accessKeyId, access_key_secret: flags.accessKeySecret, @@ -229,7 +295,13 @@ export default defineCommand({ console_region: target.region, console_site: target.site, }); - process.stderr.write(`OpenAPI credentials saved to ${store.path}\n`); + finish( + "openapi", + ctx.localize({ + "en-US": `OpenAPI credentials saved to ${store.path}`, + "zh-CN": `OpenAPI 凭证已保存至 ${store.path}`, + }), + ); return; } @@ -237,7 +309,15 @@ export default defineCommand({ if (!key) return; if (settings.dryRun) { - emitBare("Would validate and save API key."); + ctx.output.result( + { action: "auth.login", method: "api_key", config_file: store.path }, + { + text: ctx.localize({ + "en-US": "Would validate and save API key.", + "zh-CN": "将校验并保存 API Key。", + }), + }, + ); return; } const stored = store.stored(); @@ -260,6 +340,12 @@ export default defineCommand({ defaultSpeechRecognitionModel: profilePreset?.defaultSpeechRecognitionModel, apiKeyCapabilities: profilePreset?.apiKeyCapabilities, }); - process.stderr.write(`API key validated and saved to ${store.path}\n`); + finish( + "api_key", + ctx.localize({ + "en-US": `API key validated and saved to ${store.path}`, + "zh-CN": `API Key 已校验并保存至 ${store.path}`, + }), + ); }, }); diff --git a/packages/commands/src/commands/auth/logout.ts b/packages/commands/src/commands/auth/logout.ts index 9e69d167e..219d691c3 100644 --- a/packages/commands/src/commands/auth/logout.ts +++ b/packages/commands/src/commands/auth/logout.ts @@ -1,5 +1,4 @@ import { defineCommand } from "bailian-cli-core"; -import { emitBare } from "bailian-cli-runtime"; export default defineCommand({ description: { @@ -32,69 +31,93 @@ export default defineCommand({ const store = ctx.authStore; const stored = store.stored(); - if (flags.console) { - if (settings.dryRun) { - 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 ${store.path}\n`); - if (stored.apiKey) { - process.stderr.write( - "api_key is still configured and will be used for authentication.\n", - ); - } - } else { - process.stderr.write("No console access_token to clear.\n"); - } - return; - } - - if (flags.openApi) { - if (settings.dryRun) { - if (stored.openapi) - emitBare( - `Would clear access_key_id / access_key_secret / security_token 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 / security_token from ${store.path}\n`, - ); - if (stored.apiKey || stored.console) { - process.stderr.write( - "Other credentials are still configured and will be used for authentication.\n", - ); - } - } else { - process.stderr.write("No OpenAPI AK/SK credentials to clear.\n"); - } - return; - } - - const hasStoredAuth = stored.apiKey || stored.console || stored.openapi || !!stored.baseUrl; - + const scope = flags.console ? "console" : flags.openApi ? "openapi" : "all"; + const hasStoredAuth = + scope === "console" + ? stored.console + : scope === "openapi" + ? stored.openapi + : !!(stored.apiKey || stored.console || stored.openapi || stored.baseUrl); + const fields = + scope === "console" + ? "access_token" + : scope === "openapi" + ? "access_key_id / access_key_secret / security_token" + : "api_key / base_url / access_token / access_key_id / access_key_secret / security_token"; + const empty = + scope === "console" + ? { + "en-US": "No console access_token to clear.", + "zh-CN": "没有可清除的控制台 access_token。", + } + : scope === "openapi" + ? { + "en-US": "No OpenAPI AK/SK credentials to clear.", + "zh-CN": "没有可清除的 OpenAPI AK/SK 凭证。", + } + : { + "en-US": "No credentials or model Base URL to clear.", + "zh-CN": "没有可清除的凭证或模型 Base URL。", + }; if (settings.dryRun) { - if (hasStoredAuth) - emitBare( - `Would clear api_key / base_url / access_token / access_key_id / access_key_secret / security_token from ${store.path}`, - ); - else emitBare("No credentials or model Base URL to clear."); - emitBare("No changes made."); + ctx.output.result( + { + action: "auth.logout", + scope, + would_clear: hasStoredAuth, + config: settings.configName ?? "default", + config_file: store.path, + }, + { + text: [ + ctx.localize( + hasStoredAuth + ? { + "en-US": `Would clear ${fields} from ${store.path}`, + "zh-CN": `将从 ${store.path} 清除 ${fields}`, + } + : empty, + ), + ctx.localize({ "en-US": "No changes made.", "zh-CN": "未进行任何修改。" }), + ].join("\n"), + }, + ); return; } - - if (await store.logout("all")) { - process.stderr.write( - `Cleared api_key / base_url / access_token / access_key_id / access_key_secret / security_token from ${store.path}\n`, + const cleared = await store.logout(scope); + const lines = [ + ctx.localize( + cleared + ? { + "en-US": `Cleared ${fields} from ${store.path}`, + "zh-CN": `已从 ${store.path} 清除 ${fields}`, + } + : empty, + ), + ]; + if (cleared && scope === "console" && stored.apiKey) + lines.push( + ctx.localize({ + "en-US": "api_key is still configured and will be used for authentication.", + "zh-CN": "api_key 仍已配置,将用于认证。", + }), ); - } else { - process.stderr.write("No credentials or model Base URL to clear.\n"); - } + if (cleared && scope === "openapi" && (stored.apiKey || stored.console)) + lines.push( + ctx.localize({ + "en-US": "Other credentials are still configured and will be used for authentication.", + "zh-CN": "其他凭证仍已配置,将用于认证。", + }), + ); + ctx.output.result( + { + action: "auth.logout", + scope, + cleared, + config: settings.configName ?? "default", + config_file: store.path, + }, + { text: lines.join("\n") }, + ); }, }); diff --git a/packages/commands/src/commands/auth/status.ts b/packages/commands/src/commands/auth/status.ts index bce37cfa8..73aa7600d 100644 --- a/packages/commands/src/commands/auth/status.ts +++ b/packages/commands/src/commands/auth/status.ts @@ -1,5 +1,4 @@ import { defineCommand, detectOutputFormat, maskToken } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { API_KEY_PAGE } from "bailian-cli-runtime"; export default defineCommand({ @@ -48,62 +47,70 @@ export default defineCommand({ }); if (!authenticated) { - emitResult( - { - authenticated: false, - config: configName, - config_file: configFile, - message: "Not authenticated.", - hint: [ - `API key (model): ${identity.binName} auth login --api-key or DASHSCOPE_API_KEY`, - `Console gateway: ${identity.binName} auth login --console`, - `OpenAPI (AK/SK): ${identity.binName} auth login --open-api --access-key-id --access-key-secret `, - `Get API Key: ${API_KEY_PAGE}`, - ].join("\n"), - }, - format, - ); + ctx.output.result({ + authenticated: false, + config: configName, + config_file: configFile, + message: "Not authenticated.", + hint: [ + `API key (model): ${identity.binName} auth login --api-key or DASHSCOPE_API_KEY`, + `Console gateway: ${identity.binName} auth login --console`, + `OpenAPI (AK/SK): ${identity.binName} auth login --open-api --access-key-id --access-key-secret `, + `Get API Key: ${API_KEY_PAGE}`, + ].join("\n"), + }); return; } if (format !== "text") { - emitResult( - { - authenticated: true, - config: configName, - config_file: configFile, - api_key: apiKey, - console: consoleCred, - openapi, - ...(modelKeyHint ? { hint: modelKeyHint } : {}), - }, - format, - ); + ctx.output.result({ + authenticated: true, + config: configName, + config_file: configFile, + api_key: apiKey, + console: consoleCred, + openapi, + ...(modelKeyHint ? { hint: modelKeyHint } : {}), + }); return; } - emitBare(`Config: ${configName}`); - emitBare(`Config file: ${configFile}`); - emitBare("Authentication Status:"); + const lines = [`Config: ${configName}`]; + lines.push(`Config file: ${configFile}`); + lines.push("Authentication Status:"); if (apiKey) { - emitBare(` API key (model): ${apiKey.source} ${apiKey.masked}`); + lines.push(` API key (model): ${apiKey.source} ${apiKey.masked}`); } else { - emitBare(" API key (model): not configured"); - if (modelKeyHint) emitBare(` ${modelKeyHint}`); + lines.push(" API key (model): not configured"); + if (modelKeyHint) lines.push(` ${modelKeyHint}`); } if (consoleCred) { - emitBare( + lines.push( ` Console gateway: ${consoleCred.source} ${consoleCred.masked} (${consoleCred.region}, ${consoleCred.site})`, ); } else { - emitBare(` Console gateway: not configured (run ${identity.binName} auth login --console)`); + lines.push( + ` Console gateway: not configured (run ${identity.binName} auth login --console)`, + ); } if (openapi) { - emitBare(` OpenAPI (AK/SK): ${openapi.source} ${openapi.access_key_id}`); + lines.push(` OpenAPI (AK/SK): ${openapi.source} ${openapi.access_key_id}`); } else { - emitBare( + lines.push( ` OpenAPI (AK/SK): not configured (run ${identity.binName} auth login --open-api)`, ); } + ctx.output.result( + { + authenticated: true, + config: configName, + config_file: configFile, + api_key: apiKey, + console: consoleCred, + openapi, + ...(modelKeyHint ? { hint: modelKeyHint } : {}), + }, + { text: lines.join("\n") }, + ); }, }); diff --git a/packages/commands/src/commands/batch/cancel.ts b/packages/commands/src/commands/batch/cancel.ts index 6e0942149..1bfe3481c 100644 --- a/packages/commands/src/commands/batch/cancel.ts +++ b/packages/commands/src/commands/batch/cancel.ts @@ -1,5 +1,4 @@ import { defineCommand, cancelBatch } from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; import { BATCH_ID_FLAGS, DRY_RUN_NOTE } from "./shared.ts"; export default defineCommand({ description: { "en-US": "Request cancellation of a Batch task", "zh-CN": "请求取消 Batch 任务" }, @@ -18,11 +17,11 @@ export default defineCommand({ ], async run(ctx) { if (ctx.settings.dryRun) { - emitResult({ action: "batch.cancel", batch_id: ctx.flags.batchId }, "json"); + const preview = { action: "batch.cancel", batch_id: ctx.flags.batchId }; + ctx.output.result(preview, { text: JSON.stringify(preview, null, 2) }); return; } const job = await cancelBatch(ctx.client, ctx.flags.batchId); - if (ctx.settings.quiet) emitBare(job.id); - else emitResult(job, ctx.settings.output === "text" ? "text" : "json"); + ctx.output.result(job); }, }); diff --git a/packages/commands/src/commands/batch/convert.ts b/packages/commands/src/commands/batch/convert.ts index bf1a8e53a..218dbb366 100644 --- a/packages/commands/src/commands/batch/convert.ts +++ b/packages/commands/src/commands/batch/convert.ts @@ -1,5 +1,4 @@ import { defineCommand, prepareBatch, batchDataNotes, type FlagsDef } from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; import { ENDPOINT_FLAGS, FORCE_FLAGS, DRY_RUN_NOTE } from "./shared.ts"; const CONVERT_FLAGS = { file: { @@ -74,8 +73,8 @@ export default defineCommand({ ], async run(ctx) { const report = await prepareBatch({ ...ctx.flags, dryRun: ctx.settings.dryRun }); - for (const warning of report.warnings) console.error(warning); - if (ctx.settings.quiet) emitBare(report.output_file); - else emitResult(report, ctx.settings.output === "text" ? "text" : "json"); + for (const warning of report.warnings) + ctx.output.warning({ code: "BATCH_VALIDATION_WARNING", message: warning }); + ctx.output.result(report); }, }); diff --git a/packages/commands/src/commands/batch/create.ts b/packages/commands/src/commands/batch/create.ts index a4b8feb04..1ca0120bc 100644 --- a/packages/commands/src/commands/batch/create.ts +++ b/packages/commands/src/commands/batch/create.ts @@ -10,7 +10,6 @@ import { type BatchReport, type BatchCreateParams, } from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; import { ENDPOINT_FLAGS, DRY_RUN_NOTE, @@ -120,7 +119,8 @@ export default defineCommand({ report = await validateBatch(blob ?? flags.file); if (flags.endpoint && flags.endpoint !== report.endpoint) batchUsage("--endpoint differs from JSONL url.", "--endpoint 与 JSONL url 不一致。"); - for (const warning of report.warnings) console.error(warning); + for (const warning of report.warnings) + ctx.output.warning({ code: "BATCH_VALIDATION_WARNING", message: warning }); } const request: BatchCreateParams = { input_file_id: flags.inputFileId ?? "", @@ -129,15 +129,13 @@ export default defineCommand({ metadata: batchMetadata(flags), }; if (settings.dryRun) { - emitResult( - { - action: "batch.create", - request, - validation: report, - upload: flags.file ? { file: flags.file, purpose: "batch" } : undefined, - }, - "json", - ); + const preview = { + action: "batch.create", + request, + validation: report, + upload: flags.file ? { file: flags.file, purpose: "batch" } : undefined, + }; + ctx.output.result(preview, { text: JSON.stringify(preview, null, 2) }); return; } let uploaded: string | undefined; @@ -152,16 +150,17 @@ export default defineCommand({ } try { const job = await createBatch(ctx.client, request); - if (settings.quiet) emitBare(job.id); - else emitResult(job, settings.output === "text" ? "text" : "json"); + ctx.output.result(job); } catch (error) { if (uploaded) - console.error( - ctx.localize({ + ctx.output.warning({ + code: "BATCH_INPUT_UPLOADED", + message: { "en-US": `Input uploaded: ${uploaded}. Creation failed; reuse --input-file-id with --endpoint or inspect dataset file management. The file was not deleted.`, "zh-CN": `输入已上传:${uploaded}。创建失败;可通过 --input-file-id 与 --endpoint 复用,或检查 dataset 文件管理。未删除该文件。`, - }), - ); + }, + details: { file_id: uploaded }, + }); throw error; } }, diff --git a/packages/commands/src/commands/batch/download.ts b/packages/commands/src/commands/batch/download.ts index e88030446..658e6a870 100644 --- a/packages/commands/src/commands/batch/download.ts +++ b/packages/commands/src/commands/batch/download.ts @@ -11,7 +11,6 @@ import { ExitCode, type FlagsDef, } from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; import { BATCH_ID_FLAGS, FORCE_FLAGS, DRY_RUN_NOTE } from "./shared.ts"; const DOWNLOAD_FLAGS = { ...BATCH_ID_FLAGS, @@ -61,27 +60,25 @@ export default defineCommand({ join("batch-results", encodeURIComponent(ctx.flags.batchId).replace(/\./g, "%2E")), ); if (ctx.settings.dryRun) { - emitResult( - { - action: "batch.download", - batch_id: ctx.flags.batchId, - out_dir: directory, - planned_files: [ - ctx.localize({ - "en-US": "result.jsonl (if available)", - "zh-CN": "result.jsonl(若可用)", - }), - ctx.localize({ - "en-US": "error.jsonl (if available)", - "zh-CN": "error.jsonl(若可用)", - }), - ...(ctx.flags.csv ? ["result.csv"] : []), - ], - csv: ctx.flags.csv, - force: ctx.flags.force, - }, - "json", - ); + const preview = { + action: "batch.download", + batch_id: ctx.flags.batchId, + out_dir: directory, + planned_files: [ + ctx.localize({ + "en-US": "result.jsonl (if available)", + "zh-CN": "result.jsonl(若可用)", + }), + ctx.localize({ + "en-US": "error.jsonl (if available)", + "zh-CN": "error.jsonl(若可用)", + }), + ...(ctx.flags.csv ? ["result.csv"] : []), + ], + csv: ctx.flags.csv, + force: ctx.flags.force, + }; + ctx.output.result(preview, { text: JSON.stringify(preview, null, 2) }); return; } const job = await getBatch(ctx.client, ctx.flags.batchId); @@ -154,28 +151,33 @@ export default defineCommand({ csvReport = await exportBatchCsv(saved, target, ctx.flags.force); saved.push(target); if (csvReport.unknown_records) - console.error( - ctx.localize({ + ctx.output.warning({ + code: "BATCH_CSV_UNKNOWN_RECORDS", + message: { "en-US": `${csvReport.unknown_records} unrecognized result envelopes; inspect original JSONL.`, "zh-CN": `${csvReport.unknown_records} 条结果包络无法识别,请检查原始 JSONL。`, - }), - ); + }, + details: { unknown_records: csvReport.unknown_records }, + }); } } catch (error) { if (saved.length) - console.error( - ctx.localize({ + ctx.output.warning({ + code: "BATCH_DOWNLOAD_PARTIAL", + message: { "en-US": `Failed at ${stage}. Preserved files: ${saved.join(", ")}. Rerun batch download with --force to download and convert again.`, "zh-CN": `${stage} 阶段失败。已保留文件:${saved.join(",")}。可用 --force 重跑 batch download 重新下载转换。`, - }), - ); + }, + details: { stage, files: saved }, + }); throw error; } - if (ctx.settings.quiet) emitBare(saved.join("\n")); - else - emitResult( - { batch_id: job.id, status: job.status, files: saved, line_counts: counts, csv: csvReport }, - ctx.settings.output === "text" ? "text" : "json", - ); + ctx.output.result({ + batch_id: job.id, + status: job.status, + files: saved, + line_counts: counts, + csv: csvReport, + }); }, }); diff --git a/packages/commands/src/commands/batch/get.ts b/packages/commands/src/commands/batch/get.ts index ef3e71fc6..1a1b1b94d 100644 --- a/packages/commands/src/commands/batch/get.ts +++ b/packages/commands/src/commands/batch/get.ts @@ -6,7 +6,7 @@ import { ExitCode, type FlagsDef, } from "bailian-cli-core"; -import { emitBare, emitResult, registerInterruptHandler } from "bailian-cli-runtime"; +import { registerInterruptHandler } from "bailian-cli-runtime"; import { BATCH_ID_FLAGS, DRY_RUN_NOTE, batchInteger, followBatch } from "./shared.ts"; const GET_FLAGS = { ...BATCH_ID_FLAGS, @@ -65,7 +65,8 @@ export default defineCommand({ }, async run(ctx) { if (ctx.settings.dryRun) { - emitResult({ action: "batch.get", ...ctx.flags }, "json"); + const preview = { action: "batch.get", ...ctx.flags }; + ctx.output.result(preview, { text: JSON.stringify(preview, null, 2) }); return; } const controller = new AbortController(); @@ -88,27 +89,27 @@ export default defineCommand({ pollTimeout: ctx.flags.pollTimeout, signal: controller.signal, progress: (current) => { - if (!ctx.settings.quiet) - console.error( - `${current.id}: ${current.status} ${JSON.stringify(current.request_counts ?? {})}`, - ); + ctx.output.progress( + `${current.id}: ${current.status} ${JSON.stringify(current.request_counts ?? {})}`, + ); if ( !BATCH_STATUSES.includes(current.status as (typeof BATCH_STATUSES)[number]) && !unknown.has(current.status) ) { unknown.add(current.status); - console.error( - ctx.localize({ + ctx.output.warning({ + code: "BATCH_UNKNOWN_STATE", + message: { "en-US": `Unknown state ${current.status}; keep polling without inferring completion.`, "zh-CN": `未知状态 ${current.status};继续轮询,不推断已完成。`, - }), - ); + }, + details: { batch_id: current.id, status: current.status }, + }); } }, }) : await getBatch(ctx.client, ctx.flags.batchId); - if (ctx.settings.quiet) emitBare(job.id); - else emitResult(job, ctx.settings.output === "text" ? "text" : "json"); + ctx.output.result(job); if (ctx.flags.follow) { if (job.status !== "completed") throw new BailianError( @@ -124,13 +125,10 @@ export default defineCommand({ }), { rawResponse: JSON.stringify(job) }, ); - if (!ctx.settings.quiet) - console.error( - ctx.localize({ - "en-US": `Completed; failed rows: ${job.request_counts?.failed ?? "unknown"}. Next: batch download --batch-id ${job.id}`, - "zh-CN": `已完成;失败行:${job.request_counts?.failed ?? "未知"}。下一步:batch download --batch-id ${job.id}`, - }), - ); + ctx.output.info({ + "en-US": `Completed; failed rows: ${job.request_counts?.failed ?? "unknown"}. Next: batch download --batch-id ${job.id}`, + "zh-CN": `已完成;失败行:${job.request_counts?.failed ?? "未知"}。下一步:batch download --batch-id ${job.id}`, + }); } } finally { releaseInterrupt?.(); diff --git a/packages/commands/src/commands/batch/list.ts b/packages/commands/src/commands/batch/list.ts index da242c9ed..183c253ed 100644 --- a/packages/commands/src/commands/batch/list.ts +++ b/packages/commands/src/commands/batch/list.ts @@ -1,5 +1,5 @@ import { defineCommand, listBatches, BATCH_STATUSES, type FlagsDef } from "bailian-cli-core"; -import { emitBare, emitResult, formatTable } from "bailian-cli-runtime"; +import { formatTable } from "bailian-cli-runtime"; import { batchDate, batchInteger, DRY_RUN_NOTE } from "./shared.ts"; const LIST_FLAGS = { limit: { @@ -91,36 +91,37 @@ export default defineCommand({ create_before: ctx.flags.createdBefore, }; if (ctx.settings.dryRun) { - emitResult({ action: "batch.list", query }, "json"); + const preview = { action: "batch.list", query }; + ctx.output.result(preview, { text: JSON.stringify(preview, null, 2) }); return; } const response = await listBatches(ctx.client, query); - if (ctx.settings.quiet) emitBare(response.data.map((job) => job.id).join("\n")); - else if (ctx.settings.output === "text") - emitBare( - formatTable( - ctx - .localize({ - "en-US": "ID|NAME|STATUS|COMPLETED/FAILED/TOTAL|CREATED", - "zh-CN": "ID|名称|状态|成功/失败/总数|创建时间", - }) - .split("|"), - response.data.map((job) => [ - job.id, - job.metadata?.ds_name ?? "", - job.status, - `${job.request_counts?.completed ?? ""}/${job.request_counts?.failed ?? ""}/${job.request_counts?.total ?? ""}`, - job.created_at ? new Date(job.created_at * 1000).toISOString() : "", - ]), - ).join("\n"), - ); - else emitResult(response, "json"); - if (response.has_more && response.last_id && !ctx.settings.quiet) - console.error( - ctx.localize({ - "en-US": `Next page: batch list --after ${response.last_id}`, - "zh-CN": `下一页:batch list --after ${response.last_id}`, - }), - ); + ctx.output.result( + response, + ctx.settings.output === "text" + ? { + text: formatTable( + ctx + .localize({ + "en-US": "ID|NAME|STATUS|COMPLETED/FAILED/TOTAL|CREATED", + "zh-CN": "ID|名称|状态|成功/失败/总数|创建时间", + }) + .split("|"), + response.data.map((job) => [ + job.id, + job.metadata?.ds_name ?? "", + job.status, + `${job.request_counts?.completed ?? ""}/${job.request_counts?.failed ?? ""}/${job.request_counts?.total ?? ""}`, + job.created_at ? new Date(job.created_at * 1000).toISOString() : "", + ]), + ).join("\n"), + } + : undefined, + ); + if (response.has_more && response.last_id) + ctx.output.info({ + "en-US": `Next page: batch list --after ${response.last_id}`, + "zh-CN": `下一页:batch list --after ${response.last_id}`, + }); }, }); diff --git a/packages/commands/src/commands/config/agent/index.ts b/packages/commands/src/commands/config/agent/index.ts index c2e257028..27b280df5 100644 --- a/packages/commands/src/commands/config/agent/index.ts +++ b/packages/commands/src/commands/config/agent/index.ts @@ -1,6 +1,5 @@ import { platform } from "os"; -import { defineCommand, detectOutputFormat, maskToken, type FlagsDef } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; +import { defineCommand, maskToken, type FlagsDef } from "bailian-cli-core"; import { AGENTS, VALID_AGENT_NAMES, type WriteParams } from "./writers.ts"; import { decodeTokenPlanKey } from "./decode-key.ts"; import { resolveRegionBaseUrl } from "./writers/utils.ts"; @@ -103,26 +102,26 @@ export default defineCommand({ // even --dry-run validates the token. const apiKey = flags.key ? decodeTokenPlanKey(flags.key) : flags.apiKey!; const agentDef = AGENTS[agentName]; - const format = detectOutputFormat(settings.output); // Hermes has no native Windows support. if (agentName === "hermes" && platform() === "win32") { - process.stderr.write( - "Warning: Hermes Agent does not support native Windows. Please use WSL2.\n", - ); + ctx.output.warning({ + message: { + "en-US": "Hermes Agent does not support native Windows. Please use WSL2.", + "zh-CN": "Hermes Agent 不支持原生 Windows,请使用 WSL2。", + }, + code: "AGENT_PLATFORM_UNSUPPORTED", + }); } if (settings.dryRun) { - emitResult( - { - agent: agentName, - label: agentDef.label, - base_url: baseUrl, - api_key: maskToken(apiKey), - model, - }, - format, - ); + ctx.output.result({ + agent: agentName, + label: agentDef.label, + base_url: baseUrl, + api_key: maskToken(apiKey), + model, + }); return; } @@ -136,13 +135,22 @@ export default defineCommand({ }; const summary = agentDef.write(params); - if (!settings.quiet) { - emitBare(`${agentDef.label} configured successfully.`); - for (const path of summary.paths) emitBare(` Written: ${path}`); - emitBare(` ${summary.nextStep}`); - for (const warning of summary.warnings ?? []) { - process.stderr.write(`Warning: ${warning}\n`); - } - } + for (const warning of summary.warnings ?? []) + ctx.output.warning({ message: warning, code: "AGENT_CONFIG_WARNING" }); + ctx.output.result( + { agent: agentName, label: agentDef.label, ...summary }, + { + text: [ + ctx.localize({ + "en-US": `${agentDef.label} configured successfully.`, + "zh-CN": `${agentDef.label} 配置成功。`, + }), + ...summary.paths.map((path) => + ctx.localize({ "en-US": ` Written: ${path}`, "zh-CN": ` 已写入:${path}` }), + ), + ` ${summary.nextStep}`, + ].join("\n"), + }, + ); }, }); diff --git a/packages/commands/src/commands/config/list.ts b/packages/commands/src/commands/config/list.ts index 15ec6cb62..129d726ff 100644 --- a/packages/commands/src/commands/config/list.ts +++ b/packages/commands/src/commands/config/list.ts @@ -1,5 +1,4 @@ -import { defineCommand, detectOutputFormat } from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; +import { defineCommand } from "bailian-cli-core"; export default defineCommand({ description: { @@ -11,24 +10,16 @@ export default defineCommand({ async run(ctx) { const profiles = ctx.configStore.profiles(); const names = ["default", ...Object.keys(profiles.named).sort()]; - const format = detectOutputFormat(ctx.settings.output); - - if (format === "json") { - emitResult( - { - active_config: profiles.active, - profiles: names, - config_file: ctx.configStore.path, - }, - format, - ); - return; - } - + const result = { + active_config: profiles.active, + profiles: names, + config_file: ctx.configStore.path, + }; const nameWidth = Math.max("NAME".length, ...names.map((name) => name.length)); - emitBare(`${"NAME".padEnd(nameWidth)} ACTIVE`); + const lines = [`${"NAME".padEnd(nameWidth)} ACTIVE`]; for (const name of names) { - emitBare(`${name.padEnd(nameWidth)} ${name === profiles.active ? "*" : ""}`); + lines.push(`${name.padEnd(nameWidth)} ${name === profiles.active ? "*" : ""}`); } + ctx.output.result(result, { text: lines.join("\n") }); }, }); diff --git a/packages/commands/src/commands/config/set.ts b/packages/commands/src/commands/config/set.ts index f81a7be78..a042428f2 100644 --- a/packages/commands/src/commands/config/set.ts +++ b/packages/commands/src/commands/config/set.ts @@ -1,5 +1,4 @@ -import { defineCommand, detectOutputFormat, maskToken, type ConfigFile } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; +import { defineCommand, maskToken, type ConfigFile } from "bailian-cli-core"; import { SECRET_KEYS, resolveKey, validateAndCoerce } from "./shared.ts"; export default defineCommand({ @@ -42,17 +41,12 @@ export default defineCommand({ const resolvedKey: string = resolveKey(key); const coerced = validateAndCoerce(key, value); - const format = detectOutputFormat(settings.output); - if (settings.dryRun) { - emitResult( - { - would_set: { [resolvedKey]: coerced }, - config: settings.configName ?? "default", - config_file: ctx.configStore.path, - }, - format, - ); + ctx.output.result({ + would_set: { [resolvedKey]: coerced }, + config: settings.configName ?? "default", + config_file: ctx.configStore.path, + }); return; } @@ -60,16 +54,11 @@ export default defineCommand({ [resolvedKey]: coerced, } as Partial); - if (!settings.quiet) { - const shown = SECRET_KEYS.has(resolvedKey) ? maskToken(String(coerced)) : coerced; - emitResult( - { - [resolvedKey]: shown, - config: settings.configName ?? "default", - config_file: ctx.configStore.path, - }, - format, - ); - } + const shown = SECRET_KEYS.has(resolvedKey) ? maskToken(String(coerced)) : coerced; + ctx.output.result({ + [resolvedKey]: shown, + config: settings.configName ?? "default", + config_file: ctx.configStore.path, + }); }, }); diff --git a/packages/commands/src/commands/config/show.ts b/packages/commands/src/commands/config/show.ts index ed7289a36..e51d61bdf 100644 --- a/packages/commands/src/commands/config/show.ts +++ b/packages/commands/src/commands/config/show.ts @@ -86,10 +86,12 @@ export default defineCommand({ if (format === "json") { result.active = active; - process.stdout.write(formatOutput(result, format) + "\n"); + ctx.output.result(result); return; } + const payload = { ...result, active }; + // Text: append (active) to the winning layer's displayed value. if (typeof result.api_key === "string") { result.api_key = markActive( @@ -120,8 +122,8 @@ export default defineCommand({ // Color after render so YAML does not escape ANSI into values. const rendered = formatOutput(result, format); - process.stdout.write( - rendered.replaceAll(activeSuffix, ansi(process.stdout).cyan(activeSuffix)) + "\n", - ); + ctx.output.result(payload, { + text: rendered.replaceAll(activeSuffix, ansi(process.stdout).cyan(activeSuffix)), + }); }, }); diff --git a/packages/commands/src/commands/config/ui.ts b/packages/commands/src/commands/config/ui.ts index 082f05e7f..63e5e93fe 100644 --- a/packages/commands/src/commands/config/ui.ts +++ b/packages/commands/src/commands/config/ui.ts @@ -5,7 +5,6 @@ import { extname } from "node:path"; import { defineCommand, - detectOutputFormat, BailianError, ExitCode, normalizeConfigName, @@ -18,7 +17,6 @@ import { type FlagsDef, type Language, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { listenLocalServer, openInBrowser, openPath } from "../shared/local-server.ts"; import { renderConfigUiHtml } from "./ui-html.ts"; import { @@ -177,6 +175,7 @@ export function createConfigUiServer( configStore: ConfigStore, outputBase: string = defaultOutputBase(), authBridge?: AuthUiBridge, + onError?: (error: unknown) => void, ): http.Server { let activatedUiProfile: string | null = null; const uiLanguage = (): Language => { @@ -667,7 +666,7 @@ export function createConfigUiServer( } catch (err) { // Log server-side so failures are diagnosable, and return a JSON error // instead of an empty 500 body. - console.error("[config ui] request failed:", err); + onError?.(err); if (res.headersSent) { res.end(); return; @@ -688,50 +687,66 @@ export default defineCommand({ exampleArgs: ["", "--port 8787", "--no-open"], async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); if (settings.dryRun) { - emitResult( - { - host: "127.0.0.1", - port: flags.port ?? "random free port", - config_file: ctx.configStore.path, - routes: [ - "GET / -> web UI", - "GET /api/config -> read all profiles", - "GET /api/skills -> list installed agent skills", - "GET /api/skill -> read one skill's SKILL.md detail", - "POST /api/skill/install -> install a skill from an uploaded .zip into a skills root", - "GET /api/mcp -> list local MCP servers", - "POST /api/mcp -> create or update one MCP server (writes its source config)", - "DELETE /api/mcp -> remove one MCP server from its source config", - "GET /api/health -> runtime environment info (node, platform, cwd)", - "GET /api/agents -> list coding agent frameworks", - "GET /api/agent -> one agent's config detail (secrets masked)", - "POST /api/agent/open -> open one agent's config file with the OS default app", - "GET /api/auth/status -> current auth state", - "POST /api/auth/login -> start console login (opens browser)", - "POST /api/auth/logout -> clear all stored credentials", - "GET /api/assets -> list generated assets", - "GET /api/asset/file -> stream one asset file", - "POST /api/asset/open -> open one asset with the OS default app", - "POST /api/agent/launch -> launch a coding agent CLI in a new terminal", - "GET /api/scenarios -> list Playground scenarios and dispatchable agents", - "POST /api/agent/dispatch -> dispatch a scenario prompt to a connected agent", - "POST /api/profile -> save a profile", - "POST /api/active -> activate a profile", - "DELETE /api/profile -> delete a named profile", - "DELETE /api/asset -> delete one asset file", - ], - }, - format, - ); + ctx.output.result({ + host: "127.0.0.1", + port: flags.port ?? "random free port", + config_file: ctx.configStore.path, + routes: [ + "GET / -> web UI", + "GET /api/config -> read all profiles", + "GET /api/skills -> list installed agent skills", + "GET /api/skill -> read one skill's SKILL.md detail", + "POST /api/skill/install -> install a skill from an uploaded .zip into a skills root", + "GET /api/mcp -> list local MCP servers", + "POST /api/mcp -> create or update one MCP server (writes its source config)", + "DELETE /api/mcp -> remove one MCP server from its source config", + "GET /api/health -> runtime environment info (node, platform, cwd)", + "GET /api/agents -> list coding agent frameworks", + "GET /api/agent -> one agent's config detail (secrets masked)", + "POST /api/agent/open -> open one agent's config file with the OS default app", + "GET /api/auth/status -> current auth state", + "POST /api/auth/login -> start console login (opens browser)", + "POST /api/auth/logout -> clear all stored credentials", + "GET /api/assets -> list generated assets", + "GET /api/asset/file -> stream one asset file", + "POST /api/asset/open -> open one asset with the OS default app", + "POST /api/agent/launch -> launch a coding agent CLI in a new terminal", + "GET /api/scenarios -> list Playground scenarios and dispatchable agents", + "POST /api/agent/dispatch -> dispatch a scenario prompt to a connected agent", + "POST /api/profile -> save a profile", + "POST /api/active -> activate a profile", + "DELETE /api/profile -> delete a named profile", + "DELETE /api/asset -> delete one asset file", + ], + }); return; } + if (settings.output === "json") + throw new BailianError( + ctx.localize({ + "en-US": + "The interactive config UI requires --output text. Use --dry-run for a JSON preview.", + "zh-CN": "交互式配置 UI 需要 --output text。可使用 --dry-run 查看 JSON 预览。", + }), + ExitCode.USAGE, + ); const token = randomBytes(16).toString("hex"); const outputBase = settings.outputDir || defaultOutputBase(); - const server = createConfigUiServer(token, ctx.configStore, outputBase, makeAuthUiBridge(ctx)); + const server = createConfigUiServer( + token, + ctx.configStore, + outputBase, + makeAuthUiBridge(ctx), + (error) => + ctx.output.debug({ + message: { "en-US": "Config UI request failed.", "zh-CN": "配置 UI 请求失败。" }, + code: "CONFIG_UI_REQUEST_FAILED", + details: { error }, + }), + ); let port: number; try { @@ -748,14 +763,37 @@ export default defineCommand({ if (!flags.noOpen) { try { await openInBrowser(url); - emitBare("Opened the config UI in your default browser."); + ctx.output.info({ + "en-US": "Opened the config UI in your default browser.", + "zh-CN": "已在默认浏览器中打开配置 UI。", + }); } catch { - emitBare("Could not open the browser automatically. Open the URL below manually."); + ctx.output.warning({ + message: { + "en-US": "Could not open the browser automatically. Open the URL below manually.", + "zh-CN": "无法自动打开浏览器,请手动打开以下地址。", + }, + code: "BROWSER_OPEN_FAILED", + }); } } - emitBare(`Config UI running at ${url}`); - emitBare("Note: credentials are shown in cleartext in the browser (localhost only)."); - emitBare("Press Ctrl+C to stop."); + // Long-running native UI: publish the listening address once, then await shutdown. + ctx.output.result( + { url, config_file: ctx.configStore.path }, + { + text: [ + ctx.localize({ + "en-US": `Config UI running at ${url}`, + "zh-CN": `配置 UI 运行于 ${url}`, + }), + ctx.localize({ + "en-US": "Note: credentials are shown in cleartext in the browser (localhost only).", + "zh-CN": "注意:凭证将在浏览器中以明文显示(仅限 localhost)。", + }), + ctx.localize({ "en-US": "Press Ctrl+C to stop.", "zh-CN": "按 Ctrl+C 停止。" }), + ].join("\n"), + }, + ); await new Promise((resolve) => { const shutdown = () => server.close(() => resolve()); diff --git a/packages/commands/src/commands/config/use.ts b/packages/commands/src/commands/config/use.ts index e85137c95..584a57d12 100644 --- a/packages/commands/src/commands/config/use.ts +++ b/packages/commands/src/commands/config/use.ts @@ -1,5 +1,4 @@ -import { defineCommand, detectOutputFormat } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; +import { defineCommand } from "bailian-cli-core"; export default defineCommand({ description: { "en-US": "Set the active config profile", "zh-CN": "设置当前激活的配置 Profile" }, @@ -18,28 +17,19 @@ export default defineCommand({ }, exampleArgs: ["--name token-plan", "--name default"], async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); if (ctx.settings.dryRun) { const activeConfig = ctx.configStore.validateActivation(ctx.flags.name); - emitResult( - { - would_activate: activeConfig, - config_file: ctx.configStore.path, - }, - format, - ); + ctx.output.result({ + would_activate: activeConfig, + config_file: ctx.configStore.path, + }); return; } const activeConfig = await ctx.configStore.activate(ctx.flags.name); - if (!ctx.settings.quiet) { - emitResult( - { - active_config: activeConfig, - config_file: ctx.configStore.path, - }, - format, - ); - } + ctx.output.result({ + active_config: activeConfig, + config_file: ctx.configStore.path, + }); }, }); diff --git a/packages/commands/src/commands/console/call.ts b/packages/commands/src/commands/console/call.ts index 2ff9bfa43..89c6dece6 100644 --- a/packages/commands/src/commands/console/call.ts +++ b/packages/commands/src/commands/console/call.ts @@ -1,10 +1,4 @@ -import { - defineCommand, - UsageError, - effectiveConsoleGatewayConfig, - detectOutputFormat, -} from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; +import { defineCommand, UsageError, effectiveConsoleGatewayConfig } from "bailian-cli-core"; export default defineCommand({ description: { @@ -46,22 +40,17 @@ export default defineCommand({ throw new UsageError("--data must be valid JSON"); } - const format = detectOutputFormat(settings.output); - if (settings.dryRun) { - emitResult( - { - api, - data, - ...effectiveConsoleGatewayConfig(settings), - }, - format, - ); + ctx.output.result({ + api, + data, + ...effectiveConsoleGatewayConfig(settings), + }); return; } const result = await ctx.client.console(api, data); - emitResult(result, format); + ctx.output.result(result); }, }); diff --git a/packages/commands/src/commands/dataset/delete.ts b/packages/commands/src/commands/dataset/delete.ts index 8d97f6c1b..0087e6bb1 100644 --- a/packages/commands/src/commands/dataset/delete.ts +++ b/packages/commands/src/commands/dataset/delete.ts @@ -1,5 +1,4 @@ import { defineCommand, deleteDataset, type FlagsDef } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; const DELETE_FLAGS = { fileId: { @@ -11,6 +10,7 @@ const DELETE_FLAGS = { } satisfies FlagsDef; export default defineCommand({ + defaultOutput: "json", description: { "en-US": "Delete a dataset file by ID", "zh-CN": "通过 ID 删除数据集文件" }, auth: "apiKey", risk: { @@ -32,16 +32,19 @@ export default defineCommand({ const fileId = flags.fileId; if (settings.dryRun) { - emitResult({ action: "dataset.delete", file_id: fileId }, "json"); + const resultData1 = { action: "dataset.delete", file_id: fileId }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, + ); return; } const response = await deleteDataset(ctx.client, fileId); - if (settings.quiet) { - emitBare(fileId); - } else { - emitResult(response, "json"); - } + ctx.output.result( + response, + ctx.settings.output === "text" ? { text: JSON.stringify(response, null, 2) } : undefined, + ); }, }); diff --git a/packages/commands/src/commands/dataset/get.ts b/packages/commands/src/commands/dataset/get.ts index f9bb3674b..68aa62ad9 100644 --- a/packages/commands/src/commands/dataset/get.ts +++ b/packages/commands/src/commands/dataset/get.ts @@ -1,5 +1,4 @@ import { defineCommand, getDataset, type FlagsDef } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; const GET_FLAGS = { fileId: { @@ -11,6 +10,7 @@ const GET_FLAGS = { } satisfies FlagsDef; export default defineCommand({ + defaultOutput: "json", description: { "en-US": "Get details of a single dataset file", "zh-CN": "获取单个数据集文件的详情", @@ -24,7 +24,11 @@ export default defineCommand({ const fileId = flags.fileId; if (settings.dryRun) { - emitResult({ action: "dataset.get", file_id: fileId }, "json"); + const resultData1 = { action: "dataset.get", file_id: fileId }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, + ); return; } @@ -32,7 +36,12 @@ export default defineCommand({ const file = response.data; if (!file) { - emitBare(`No data returned for ${fileId}`); + ctx.output.result(null, { + text: ctx.localize({ + "en-US": `No data returned for ${fileId}`, + "zh-CN": `未返回 ${fileId} 的数据`, + }), + }); return; } @@ -47,10 +56,12 @@ export default defineCommand({ description: file.description ?? "", }; - if (settings.quiet) { - emitBare(item.file_id); - } else { - emitResult({ ...item, request_id: response.request_id }, "json"); + { + const resultData2 = { ...item, request_id: response.request_id }; + ctx.output.result( + resultData2, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData2, null, 2) } : undefined, + ); } }, }); diff --git a/packages/commands/src/commands/dataset/list.ts b/packages/commands/src/commands/dataset/list.ts index 7d37d6fa1..0a47afeb3 100644 --- a/packages/commands/src/commands/dataset/list.ts +++ b/packages/commands/src/commands/dataset/list.ts @@ -1,5 +1,4 @@ import { defineCommand, listDatasets, type FlagsDef } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; const LIST_FLAGS = { page: { @@ -26,6 +25,7 @@ const LIST_FLAGS = { } satisfies FlagsDef; export default defineCommand({ + defaultOutput: "json", description: { "en-US": "List uploaded dataset files", "zh-CN": "列出已上传的数据集文件" }, auth: "apiKey", usageArgs: "[--page ] [--page-size ] [--purpose ]", @@ -35,14 +35,15 @@ export default defineCommand({ const { settings, flags } = ctx; if (settings.dryRun) { - emitResult( - { - action: "dataset.list", - page: flags.page, - page_size: flags.pageSize, - purpose: flags.purpose, - }, - "json", + const resultData1 = { + action: "dataset.list", + page: flags.page, + page_size: flags.pageSize, + purpose: flags.purpose, + }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, ); return; } @@ -62,10 +63,12 @@ export default defineCommand({ purpose: item.purpose ?? "", })); - if (settings.quiet) { - for (const item of items) emitBare(item.file_id); - } else { - emitResult({ items, total, request_id: response.request_id }, "json"); + { + const resultData2 = { items, total, request_id: response.request_id }; + ctx.output.result( + resultData2, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData2, null, 2) } : undefined, + ); } }, }); diff --git a/packages/commands/src/commands/dataset/upload.ts b/packages/commands/src/commands/dataset/upload.ts index d22284cba..e9730c58b 100644 --- a/packages/commands/src/commands/dataset/upload.ts +++ b/packages/commands/src/commands/dataset/upload.ts @@ -12,7 +12,6 @@ import { ExitCode, type FlagsDef, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; const UPLOAD_FLAGS = { file: { @@ -61,6 +60,7 @@ const UPLOAD_FLAGS = { } satisfies FlagsDef; export default defineCommand({ + defaultOutput: "json", description: { "en-US": "Upload a dataset file (.jsonl or .zip) to Bailian", "zh-CN": "将数据集文件(.jsonl 或 .zip)上传到百炼", @@ -136,29 +136,30 @@ export default defineCommand({ throw new BailianError(lines.join("\n"), ExitCode.GENERAL); } // Surface warnings to stderr but keep going. - if (result.warnings.length > 0 && !settings.quiet) { - process.stderr.write( - `Dataset validation passed with ${result.warnings.length} warning(s):\n`, - ); - for (const warning of result.warnings.slice(0, 5)) - process.stderr.write(`${formatIssue(warning)}\n`); - if (result.warnings.length > 5) { - process.stderr.write(` … and ${result.warnings.length - 5} more.\n`); - } + if (result.warnings.length > 0) { + ctx.output.warning({ + code: "DATASET_VALIDATION_WARNINGS", + message: { + "en-US": `Dataset validation passed with ${result.warnings.length} warning(s).`, + "zh-CN": `数据集校验通过,共 ${result.warnings.length} 条警告。`, + }, + details: { file: filePath, warnings: result.warnings }, + }); } } if (settings.dryRun) { - emitResult( - { - action: "dataset.upload", - file: filePath, - purpose, - max_bytes: maxBytes, - validate: !flags.noValidate, - schema: schema ?? "auto", - }, - "json", + const resultData1 = { + action: "dataset.upload", + file: filePath, + purpose, + max_bytes: maxBytes, + validate: !flags.noValidate, + schema: schema ?? "auto", + }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, ); return; } @@ -169,10 +170,12 @@ export default defineCommand({ }); const { request_id, ...file } = uploaded; - if (settings.quiet) { - emitBare(file.file_id); - } else { - emitResult({ ...file, request_id }, "json"); + { + const resultData2 = { ...file, request_id }; + ctx.output.result( + resultData2, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData2, null, 2) } : undefined, + ); } }, }); diff --git a/packages/commands/src/commands/dataset/validate.ts b/packages/commands/src/commands/dataset/validate.ts index fa34fcff5..6de1fe5ee 100644 --- a/packages/commands/src/commands/dataset/validate.ts +++ b/packages/commands/src/commands/dataset/validate.ts @@ -7,7 +7,6 @@ import { ExitCode, type FlagsDef, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; const VALIDATE_FLAGS = { file: { @@ -39,6 +38,7 @@ const VALIDATE_FLAGS = { } satisfies FlagsDef; export default defineCommand({ + defaultOutput: "json", description: { "en-US": "Locally validate a dataset file (.jsonl or .zip) without uploading", "zh-CN": "在本地验证数据集文件(.jsonl 或 .zip),不执行上传", @@ -88,14 +88,15 @@ export default defineCommand({ const filePath = flags.file; const schema = parseDatasetSchemaFlag(flags.schema); if (settings.dryRun) { - emitResult( - { - action: "dataset.validate", - file: filePath, - full: flags.fullValidate, - schema: schema ?? "auto", - }, - "json", + const resultData1 = { + action: "dataset.validate", + file: filePath, + full: flags.fullValidate, + schema: schema ?? "auto", + }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, ); return; } @@ -106,11 +107,10 @@ export default defineCommand({ maxBytes: schema === "decision" ? MAX_DECISION_DATASET_BYTES : undefined, }); - if (settings.quiet) { - emitBare(result.valid ? "ok" : "fail"); - } else { - emitResult(result, "json"); - } + ctx.output.result( + result, + ctx.settings.output === "text" ? { text: JSON.stringify(result, null, 2) } : undefined, + ); if (!result.valid) { // Match the upload command's exit-code convention; details already printed. diff --git a/packages/commands/src/commands/decision/predict.ts b/packages/commands/src/commands/decision/predict.ts index 509a264e4..32aec5e4b 100644 --- a/packages/commands/src/commands/decision/predict.ts +++ b/packages/commands/src/commands/decision/predict.ts @@ -1,13 +1,12 @@ import { defineCommand, - detectOutputFormat, DECISION_REGIONS, DEFAULT_DECISION_MODEL, DEFAULT_DECISION_REGION, type DecisionRequest, type DecisionResponse, } from "bailian-cli-core"; -import { emitBare, emitRequestId, emitResult } from "bailian-cli-runtime"; + import { buildDecisionInput, decisionInputFlagsError, @@ -17,6 +16,7 @@ import { } from "./shared.ts"; export default defineCommand({ + defaultOutput: "json", description: { "en-US": "Run the decision model: typed choice / noul / score answers with probabilities", "zh-CN": "调用决策模型:一次返回 choice / noul / score 判定结果及概率分布", @@ -150,10 +150,10 @@ export default defineCommand({ client: ctx.client, localize: ctx.localize, }); - const format = settings.outputExplicit ? detectOutputFormat(settings.output) : "json"; + const format = settings.output; if (settings.dryRun) { - emitResult({ endpoint, request: body }, format); + ctx.output.result({ endpoint, request: body }); return; } @@ -164,15 +164,13 @@ export default defineCommand({ }); if (format !== "text") { - emitResult(response, format); + ctx.output.result(response); return; } - emitBare(formatDecisionAnswers(response)); - if (!settings.quiet) { - const stats = formatDecisionStats(response); - if (stats) process.stderr.write(`${stats}\n`); - } - emitRequestId(response.request_id, settings.quiet); + ctx.output.result(response, { text: formatDecisionAnswers(response) }); + const stats = formatDecisionStats(response); + if (stats) ctx.output.info(stats); + if (response.request_id) ctx.output.info(`request_id: ${response.request_id}`); }, }); diff --git a/packages/commands/src/commands/deploy/capacity-get.ts b/packages/commands/src/commands/deploy/capacity-get.ts index 746457817..c3fe97c3d 100644 --- a/packages/commands/src/commands/deploy/capacity-get.ts +++ b/packages/commands/src/commands/deploy/capacity-get.ts @@ -1,8 +1,9 @@ import { defineCommand, getCapacityInstance } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; + import { CAPACITY_INSTANCE_FLAG, DEPLOYED_MODEL_FLAG, validateQueryIds } from "./query-shared.ts"; export default defineCommand({ + defaultOutput: "json", description: { "en-US": "Get a throughput reservation capacity instance", "zh-CN": "查询吞吐预留容量实例详情", @@ -23,19 +24,25 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; if (settings.dryRun) { - emitResult( - { - action: "deploy.capacity.get", - deployed_model: flags.deployedModel, - instance_id: flags.instanceId, - }, - "json", + const resultData1 = { + action: "deploy.capacity.get", + deployed_model: flags.deployedModel, + instance_id: flags.instanceId, + }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, ); return; } - emitResult( - await getCapacityInstance(ctx.client, flags.deployedModel, flags.instanceId), - "json", + const resultData2 = await getCapacityInstance( + ctx.client, + flags.deployedModel, + flags.instanceId, + ); + ctx.output.result( + resultData2, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData2, null, 2) } : undefined, ); }, }); diff --git a/packages/commands/src/commands/deploy/capacity-list.ts b/packages/commands/src/commands/deploy/capacity-list.ts index 48694c62a..9709393dc 100644 --- a/packages/commands/src/commands/deploy/capacity-list.ts +++ b/packages/commands/src/commands/deploy/capacity-list.ts @@ -1,5 +1,5 @@ import { defineCommand, listCapacityInstances, type FlagsDef } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; + import { DEPLOYED_MODEL_FLAG, queryValues, @@ -49,6 +49,7 @@ const FLAGS = { } satisfies FlagsDef; export default defineCommand({ + defaultOutput: "json", description: { "en-US": "List throughput reservation capacity instances", "zh-CN": "查询吞吐预留容量实例列表", @@ -96,22 +97,27 @@ export default defineCommand({ chargeTypes: queryValues(flags.chargeTypes), }; if (settings.dryRun) { - emitResult( - { - action: "deploy.capacity.list", - deployed_model: flags.deployedModel, - query: { - page_no: params.pageNo, - page_size: params.pageSize, - include_deleted: params.includeDeleted, - statuses: params.statuses?.join(","), - charge_types: params.chargeTypes?.join(","), - }, + const resultData1 = { + action: "deploy.capacity.list", + deployed_model: flags.deployedModel, + query: { + page_no: params.pageNo, + page_size: params.pageSize, + include_deleted: params.includeDeleted, + statuses: params.statuses?.join(","), + charge_types: params.chargeTypes?.join(","), }, - "json", + }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, ); return; } - emitResult(await listCapacityInstances(ctx.client, flags.deployedModel, params), "json"); + const resultData2 = await listCapacityInstances(ctx.client, flags.deployedModel, params); + ctx.output.result( + resultData2, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData2, null, 2) } : undefined, + ); }, }); diff --git a/packages/commands/src/commands/deploy/capacity-unsubscribe.ts b/packages/commands/src/commands/deploy/capacity-unsubscribe.ts index 9e435083c..5f10dffbf 100644 --- a/packages/commands/src/commands/deploy/capacity-unsubscribe.ts +++ b/packages/commands/src/commands/deploy/capacity-unsubscribe.ts @@ -1,8 +1,9 @@ import { defineCommand } from "bailian-cli-core"; -import { emitResult, emitBare, refundConsolePage } from "bailian-cli-runtime"; +import { refundConsolePage } from "bailian-cli-runtime"; import { CAPACITY_INSTANCE_FLAG, validateQueryIds } from "./query-shared.ts"; export default defineCommand({ + defaultOutput: "json", description: { "en-US": "Build the Aliyun billing console unsubscribe link for a prepaid capacity instance", "zh-CN": "生成预付费容量实例在阿里云费用中心的退订链接", @@ -27,20 +28,18 @@ export default defineCommand({ ], validate: validateQueryIds, async run(ctx) { - const { settings, flags } = ctx; + const { flags } = ctx; // Pure link builder: no network call, so dry-run and execution share one path. const refundUrl = refundConsolePage(flags.instanceId); - if (settings.quiet) { - emitBare(refundUrl); - return; - } - emitResult( - { - action: "deploy.capacity.unsubscribe", - instance_id: flags.instanceId, - refund_url: refundUrl, - }, - "json", + + const resultData1 = { + action: "deploy.capacity.unsubscribe", + instance_id: flags.instanceId, + refund_url: refundUrl, + }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, ); }, }); diff --git a/packages/commands/src/commands/deploy/capacity-write.ts b/packages/commands/src/commands/deploy/capacity-write.ts index 3655933d8..b10bae0b0 100644 --- a/packages/commands/src/commands/deploy/capacity-write.ts +++ b/packages/commands/src/commands/deploy/capacity-write.ts @@ -14,7 +14,7 @@ import { hasPrepaidFlags, type FlagsDef, } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; + import { CAPACITY_INSTANCE_FLAG, DEPLOYED_MODEL_FLAG, validateQueryIds } from "./query-shared.ts"; import { CAPACITY_FLAGS, @@ -46,6 +46,7 @@ const CREATE_FLAGS = { } satisfies FlagsDef; export const deployCapacityCreate = defineCommand({ + defaultOutput: "json", description: { "en-US": "Purchase an additional capacity instance for a ModelCode", "zh-CN": "为已有 ModelCode 叠加购买容量实例", @@ -93,15 +94,16 @@ export const deployCapacityCreate = defineCommand({ ...(prepaid ? { pre_paid_info: prepaid } : {}), }; if (settings.dryRun) { - emitResult( - { - action: "deploy.capacity.create", - deployed_model: flags.deployedModel, - body, - client_request_id: flags.requestId ?? "", - wait: flags.wait, - }, - "json", + const resultData1 = { + action: "deploy.capacity.create", + deployed_model: flags.deployedModel, + body, + client_request_id: flags.requestId ?? "", + wait: flags.wait, + }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, ); return; } @@ -111,7 +113,7 @@ export const deployCapacityCreate = defineCommand({ requestId, }), ); - await finishMutation(ctx.client, flags.deployedModel, response, flags, requestId, true); + await finishMutation(ctx, flags.deployedModel, response, flags, requestId, true); }, }); @@ -125,6 +127,7 @@ const SCALE_FLAGS = { } satisfies FlagsDef; export const deployCapacityScale = defineCommand({ + defaultOutput: "json", description: { "en-US": "Scale the absolute capacity of one reservation instance", "zh-CN": "调整单个预留实例的绝对容量", @@ -168,17 +171,18 @@ export const deployCapacityScale = defineCommand({ ...(prepaid ? { pre_paid_info: prepaid } : {}), }; if (settings.dryRun) { - emitResult( - { - action: "deploy.capacity.scale", - deployed_model: flags.deployedModel, - instance_id: flags.instanceId, - body, - client_request_id: flags.requestId ?? "", - precheck: "can_scale", - wait: flags.wait, - }, - "json", + const resultData2 = { + action: "deploy.capacity.scale", + deployed_model: flags.deployedModel, + instance_id: flags.instanceId, + body, + client_request_id: flags.requestId ?? "", + precheck: "can_scale", + wait: flags.wait, + }; + ctx.output.result( + resultData2, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData2, null, 2) } : undefined, ); return; } @@ -202,7 +206,7 @@ export const deployCapacityScale = defineCommand({ const response = await submitCapacityWrite(requestId, () => scaleCapacityInstance(ctx.client, flags.deployedModel, flags.instanceId, body, { requestId }), ); - await finishMutation(ctx.client, flags.deployedModel, response, flags, requestId, true); + await finishMutation(ctx, flags.deployedModel, response, flags, requestId, true); }, }); @@ -223,6 +227,7 @@ const RENEW_FLAGS = { } satisfies FlagsDef; export const deployCapacityRenew = defineCommand({ + defaultOutput: "json", description: { "en-US": "Renew a prepaid capacity instance, optionally changing capacity", "zh-CN": "续订预付费容量实例,可同时变更容量", @@ -265,17 +270,18 @@ export const deployCapacityRenew = defineCommand({ ...(capacity ? { ptu_capacity: capacity } : {}), }; if (settings.dryRun) { - emitResult( - { - action: "deploy.capacity.renew", - deployed_model: flags.deployedModel, - instance_id: flags.instanceId, - body, - client_request_id: flags.requestId ?? "", - precheck: "can_renew/pre_paid/configured_capacity", - wait: flags.wait, - }, - "json", + const resultData3 = { + action: "deploy.capacity.renew", + deployed_model: flags.deployedModel, + instance_id: flags.instanceId, + body, + client_request_id: flags.requestId ?? "", + precheck: "can_renew/pre_paid/configured_capacity", + wait: flags.wait, + }; + ctx.output.result( + resultData3, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData3, null, 2) } : undefined, ); return; } @@ -305,7 +311,7 @@ export const deployCapacityRenew = defineCommand({ const response = await submitCapacityWrite(requestId, () => renewCapacityInstance(ctx.client, flags.deployedModel, flags.instanceId, body, { requestId }), ); - await finishMutation(ctx.client, flags.deployedModel, response, flags, requestId, true); + await finishMutation(ctx, flags.deployedModel, response, flags, requestId, true); }, }); @@ -324,6 +330,7 @@ const DELETE_FLAGS = { } satisfies FlagsDef; export const deployCapacityDelete = defineCommand({ + defaultOutput: "json", description: { "en-US": "Release a capacity instance without deleting its ModelCode", "zh-CN": "释放容量实例,保留所属 ModelCode", @@ -354,17 +361,18 @@ export const deployCapacityDelete = defineCommand({ async run(ctx) { const { flags, settings } = ctx; if (settings.dryRun) { - emitResult( - { - action: "deploy.capacity.delete", - deployed_model: flags.deployedModel, - instance_id: flags.instanceId, - query: flags.reason === undefined ? {} : { reason: flags.reason }, - client_request_id: flags.requestId ?? "", - precheck: "can_delete/prepaid_unsubscribe", - wait: flags.wait, - }, - "json", + const resultData4 = { + action: "deploy.capacity.delete", + deployed_model: flags.deployedModel, + instance_id: flags.instanceId, + query: flags.reason === undefined ? {} : { reason: flags.reason }, + client_request_id: flags.requestId ?? "", + precheck: "can_delete/prepaid_unsubscribe", + wait: flags.wait, + }; + ctx.output.result( + resultData4, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData4, null, 2) } : undefined, ); return; } @@ -392,7 +400,7 @@ export const deployCapacityDelete = defineCommand({ }), ); await finishMutation( - ctx.client, + ctx, flags.deployedModel, response, flags, diff --git a/packages/commands/src/commands/deploy/create.ts b/packages/commands/src/commands/deploy/create.ts index 8c7dfb833..9f81b17b7 100644 --- a/packages/commands/src/commands/deploy/create.ts +++ b/packages/commands/src/commands/deploy/create.ts @@ -12,7 +12,7 @@ import { type FlagsDef, type ParsedFlags, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; + import { CAPACITY_FLAGS, PREPAID_FLAGS, @@ -226,23 +226,29 @@ async function runCreate( // A dry strategy resolution must precede any catalog lookup or write. if (settings.dryRun) { - emitResult({ action: "deploy.create", body: await resolveBody(true) }, "json"); + const resultData1 = { action: "deploy.create", body: await resolveBody(true) }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, + ); return; } const response = await createDeployment(ctx.client, await resolveBody(false)); const deployment = response.output ?? response.data; if (plan === "ptu") { - await finishMutation(ctx.client, deployment?.deployed_model, response, flags); - } else if (settings.quiet) { - emitBare(deployment?.deployed_model ?? ""); + await finishMutation(ctx, deployment?.deployed_model, response, flags); } else { - emitResult(response, "json"); + ctx.output.result( + response, + ctx.settings.output === "text" ? { text: JSON.stringify(response, null, 2) } : undefined, + ); } } /** `bl deploy text create` — deploy a text model. */ export const deployTextCreate = defineCommand({ + defaultOutput: "json", description: { "en-US": "Create a text model deployment", "zh-CN": "创建文本模型部署" }, auth: "apiKey", risk: CREATE_RISK, @@ -262,6 +268,7 @@ export const deployTextCreate = defineCommand({ /** `bl deploy audio create` — deploy an audio (TTS) model. Defaults to plan=mu. */ export const deployAudioCreate = defineCommand({ + defaultOutput: "json", description: { "en-US": "Create an audio (TTS) model deployment", "zh-CN": "创建音频(TTS)模型部署", @@ -282,6 +289,7 @@ export const deployAudioCreate = defineCommand({ /** `bl deploy image create` — deploy an image generation model. */ export const deployImageCreate = defineCommand({ + defaultOutput: "json", description: { "en-US": "Create an image generation model deployment", "zh-CN": "创建图片生成模型部署", diff --git a/packages/commands/src/commands/deploy/delete.ts b/packages/commands/src/commands/deploy/delete.ts index 489fd9083..eb19ef40b 100644 --- a/packages/commands/src/commands/deploy/delete.ts +++ b/packages/commands/src/commands/deploy/delete.ts @@ -8,7 +8,6 @@ import { ExitCode, type FlagsDef, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; const DELETE_FLAGS = { deployedModel: { @@ -30,6 +29,7 @@ const DELETE_FLAGS = { } satisfies FlagsDef; export default defineCommand({ + defaultOutput: "json", description: { "en-US": "Delete a model deployment (PTU must be STOPPED with all capacity released)", "zh-CN": "删除模型部署(PTU 须为 STOPPED 且已释放全部容量)", @@ -70,7 +70,11 @@ export default defineCommand({ const deployedModel = flags.deployedModel; if (settings.dryRun) { - emitResult({ action: "deploy.delete", deployed_model: deployedModel }, "json"); + const resultData1 = { action: "deploy.delete", deployed_model: deployedModel }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, + ); return; } @@ -155,10 +159,9 @@ export default defineCommand({ // Prechecks are not atomic and cannot enumerate queued operations; the server decides. const response = await deleteDeployment(ctx.client, deployedModel); - if (settings.quiet) { - emitBare(deployedModel); - } else { - emitResult(response, "json"); - } + ctx.output.result( + response, + ctx.settings.output === "text" ? { text: JSON.stringify(response, null, 2) } : undefined, + ); }, }); diff --git a/packages/commands/src/commands/deploy/get.ts b/packages/commands/src/commands/deploy/get.ts index d0d3063d1..4fc515c09 100644 --- a/packages/commands/src/commands/deploy/get.ts +++ b/packages/commands/src/commands/deploy/get.ts @@ -1,10 +1,11 @@ import { defineCommand, getDeployment } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; + import { DEPLOYED_MODEL_FLAG, validateQueryIds } from "./query-shared.ts"; const GET_FLAGS = DEPLOYED_MODEL_FLAG; export default defineCommand({ + defaultOutput: "json", description: { "en-US": "Get details of a single model deployment", "zh-CN": "获取单个模型部署的详情", @@ -30,7 +31,11 @@ export default defineCommand({ const deployedModel = flags.deployedModel; if (settings.dryRun) { - emitResult({ action: "deploy.get", deployed_model: deployedModel }, "json"); + const resultData1 = { action: "deploy.get", deployed_model: deployedModel }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, + ); return; } @@ -38,7 +43,11 @@ export default defineCommand({ const deployment = response.output ?? response.data; if (!deployment) { - emitResult({ deployed_model: deployedModel, request_id: response.request_id }, "json"); + const resultData2 = { deployed_model: deployedModel, request_id: response.request_id }; + ctx.output.result( + resultData2, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData2, null, 2) } : undefined, + ); return; } @@ -63,6 +72,10 @@ export default defineCommand({ if (deployment.gmt_create) item.created_at = deployment.gmt_create; if (deployment.gmt_modified) item.updated_at = deployment.gmt_modified; - emitResult({ ...item, request_id: response.request_id }, "json"); + const resultData3 = { ...item, request_id: response.request_id }; + ctx.output.result( + resultData3, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData3, null, 2) } : undefined, + ); }, }); diff --git a/packages/commands/src/commands/deploy/list.ts b/packages/commands/src/commands/deploy/list.ts index b2ae39b93..f4458bc81 100644 --- a/packages/commands/src/commands/deploy/list.ts +++ b/packages/commands/src/commands/deploy/list.ts @@ -1,5 +1,5 @@ import { defineCommand, listDeployments, type FlagsDef } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; + import { validatePagination } from "./query-shared.ts"; const LIST_FLAGS = { @@ -35,6 +35,7 @@ const LIST_FLAGS = { } satisfies FlagsDef; export default defineCommand({ + defaultOutput: "json", description: { "en-US": "List model deployments and throughput reservations", "zh-CN": "列出模型部署及吞吐预留", @@ -72,16 +73,17 @@ export default defineCommand({ const plan = flags.plan?.trim(); if (settings.dryRun) { - emitResult( - { - action: "deploy.list", - page, - page_size: pageSize, - plan, - status, - ...(status ? { filter_scope: "page" } : {}), - }, - "json", + const resultData1 = { + action: "deploy.list", + page, + page_size: pageSize, + plan, + status, + ...(status ? { filter_scope: "page" } : {}), + }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, ); return; } @@ -100,25 +102,26 @@ export default defineCommand({ created_at: item.gmt_create ?? "", })); - emitResult( - { - items, - total: payload?.total, - page_no: payload?.page_no ?? page, - page_size: payload?.page_size ?? pageSize, - ...(status - ? { - local_filter: { - status, - scope: "page", - matched_count: items.length, - unfiltered_page_count: deployments.length, - }, - } - : {}), - request_id: response.request_id, - }, - "json", + const resultData2 = { + items, + total: payload?.total, + page_no: payload?.page_no ?? page, + page_size: payload?.page_size ?? pageSize, + ...(status + ? { + local_filter: { + status, + scope: "page", + matched_count: items.length, + unfiltered_page_count: deployments.length, + }, + } + : {}), + request_id: response.request_id, + }; + ctx.output.result( + resultData2, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData2, null, 2) } : undefined, ); }, }); diff --git a/packages/commands/src/commands/deploy/models.ts b/packages/commands/src/commands/deploy/models.ts index fd038fb1f..b1ae3c3c6 100644 --- a/packages/commands/src/commands/deploy/models.ts +++ b/packages/commands/src/commands/deploy/models.ts @@ -1,5 +1,4 @@ import { defineCommand, listDeployableModels, type FlagsDef } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; const MODELS_FLAGS = { page: { @@ -32,6 +31,7 @@ const MODELS_FLAGS = { } satisfies FlagsDef; export default defineCommand({ + defaultOutput: "json", description: { "en-US": "List models available for deployment", "zh-CN": "列出可部署的模型" }, auth: "apiKey", usageArgs: "[--page ] [--page-size ] [--catalog-version ] [--source ]", @@ -50,15 +50,16 @@ export default defineCommand({ const modelSource = flags.source || undefined; if (settings.dryRun) { - emitResult( - { - action: "deploy.models", - page: flags.page, - page_size: flags.pageSize, - version, - model_source: modelSource, - }, - "json", + const resultData1 = { + action: "deploy.models", + page: flags.page, + page_size: flags.pageSize, + version, + model_source: modelSource, + }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, ); return; } @@ -125,6 +126,10 @@ export default defineCommand({ } return out; }); - emitResult({ items, total, request_id: response.request_id }, "json"); + const resultData2 = { items, total, request_id: response.request_id }; + ctx.output.result( + resultData2, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData2, null, 2) } : undefined, + ); }, }); diff --git a/packages/commands/src/commands/deploy/mutation-shared.ts b/packages/commands/src/commands/deploy/mutation-shared.ts index f5b2df734..3d9c64c00 100644 --- a/packages/commands/src/commands/deploy/mutation-shared.ts +++ b/packages/commands/src/commands/deploy/mutation-shared.ts @@ -5,11 +5,12 @@ import { type CapacityInstance, type CapacityOperation, type Client, + type CommandContext, type Deployment, type FlagsDef, type LocalizedText, } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; + import { waitForCapacityOperation } from "./operation-wait.ts"; export const CAPACITY_FLAGS = { @@ -230,7 +231,7 @@ export async function submitCapacityWrite( } export async function finishMutation( - client: Client, + ctx: Pick, code: string | undefined, response: MutationResponse, flags: MutationOptions, @@ -238,6 +239,7 @@ export async function finishMutation( requireOperationStatus = false, releasedInstanceId?: string, ): Promise { + const { client } = ctx; const operation = response.output ?? response.data; const result = { ...response, @@ -272,7 +274,10 @@ export async function finishMutation( ); } if (!flags.wait) { - emitResult(result, "json"); + ctx.output.result( + result, + ctx.settings.output === "text" ? { text: JSON.stringify(result, null, 2) } : undefined, + ); return; } const returnedCode = @@ -287,9 +292,19 @@ export async function finishMutation( ); } // stderr survives a later polling failure or Ctrl-C without producing two stdout JSON objects. - process.stderr.write( - `${JSON.stringify({ submitted: true, deployed_model: returnedCode, operation_id: operationId, client_request_id: requestId })}\n`, - ); + ctx.output.warning({ + code: "DEPLOYMENT_OPERATION_SUBMITTED", + message: { + "en-US": "Deployment operation submitted; keep these identifiers if waiting fails.", + "zh-CN": "部署操作已提交;若等待失败,请保留这些标识用于查询。", + }, + details: { + submitted: true, + deployed_model: returnedCode, + operation_id: operationId, + client_request_id: requestId, + }, + }); const controller = new AbortController(); const onInterrupt = () => controller.abort(new Error("Interrupted. / 已中断。")); process.once("SIGINT", onInterrupt); @@ -326,7 +341,10 @@ export async function finishMutation( { cause: finalResult }, ); } - emitResult(finalResult, "json"); + ctx.output.result( + finalResult, + ctx.settings.output === "text" ? { text: JSON.stringify(finalResult, null, 2) } : undefined, + ); } catch (error) { if (!controller.signal.aborted) throw error; } finally { diff --git a/packages/commands/src/commands/deploy/operation-get.ts b/packages/commands/src/commands/deploy/operation-get.ts index eeb367d8c..bffa0dabc 100644 --- a/packages/commands/src/commands/deploy/operation-get.ts +++ b/packages/commands/src/commands/deploy/operation-get.ts @@ -1,8 +1,9 @@ import { defineCommand, getCapacityOperation } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; + import { CAPACITY_OPERATION_FLAGS, validateQueryIds } from "./query-shared.ts"; export default defineCommand({ + defaultOutput: "json", description: { "en-US": "Query a throughput reservation capacity operation once", "zh-CN": "单次查询吞吐预留容量操作", @@ -23,19 +24,25 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; if (settings.dryRun) { - emitResult( - { - action: "deploy.operation.get", - deployed_model: flags.deployedModel, - operation_id: flags.operationId, - }, - "json", + const resultData1 = { + action: "deploy.operation.get", + deployed_model: flags.deployedModel, + operation_id: flags.operationId, + }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, ); return; } - emitResult( - await getCapacityOperation(ctx.client, flags.deployedModel, flags.operationId), - "json", + const resultData2 = await getCapacityOperation( + ctx.client, + flags.deployedModel, + flags.operationId, + ); + ctx.output.result( + resultData2, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData2, null, 2) } : undefined, ); }, }); diff --git a/packages/commands/src/commands/deploy/operation-wait.ts b/packages/commands/src/commands/deploy/operation-wait.ts index 82943026a..03361f84c 100644 --- a/packages/commands/src/commands/deploy/operation-wait.ts +++ b/packages/commands/src/commands/deploy/operation-wait.ts @@ -9,7 +9,7 @@ import { type FlagsDef, type GetCapacityOperationResponse, } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; + import { CAPACITY_OPERATION_FLAGS, validateQueryIds } from "./query-shared.ts"; const DEFAULT_INTERVAL = 2; @@ -150,6 +150,7 @@ const FLAGS = { } satisfies FlagsDef; export default defineCommand({ + defaultOutput: "json", description: { "en-US": "Wait for a capacity operation and refresh confirmed capacity", "zh-CN": "等待容量操作完成并刷新已确认容量", @@ -200,15 +201,16 @@ export default defineCommand({ const interval = flags.interval ?? DEFAULT_INTERVAL; const pollTimeout = flags.pollTimeout ?? DEFAULT_POLL_TIMEOUT; if (settings.dryRun) { - emitResult( - { - action: "deploy.operation.wait", - deployed_model: flags.deployedModel, - operation_id: flags.operationId, - interval, - poll_timeout: pollTimeout, - }, - "json", + const resultData1 = { + action: "deploy.operation.wait", + deployed_model: flags.deployedModel, + operation_id: flags.operationId, + interval, + poll_timeout: pollTimeout, + }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, ); return; } @@ -222,7 +224,10 @@ export default defineCommand({ flags.operationId, { interval, pollTimeout, signal: controller.signal }, ); - emitResult(result, "json"); + ctx.output.result( + result, + ctx.settings.output === "text" ? { text: JSON.stringify(result, null, 2) } : undefined, + ); } catch (error) { // The CLI runtime owns SIGINT's exit status (130). Do not emit success on interruption. if (!controller.signal.aborted) throw error; diff --git a/packages/commands/src/commands/deploy/overflow.ts b/packages/commands/src/commands/deploy/overflow.ts index 699809252..e80fc3e87 100644 --- a/packages/commands/src/commands/deploy/overflow.ts +++ b/packages/commands/src/commands/deploy/overflow.ts @@ -5,10 +5,11 @@ import { getDeployment, updateDeploymentOverflow, } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; + import { DEPLOYED_MODEL_FLAG, validateQueryIds } from "./query-shared.ts"; export default defineCommand({ + defaultOutput: "json", description: { "en-US": "Set the overflow strategy for a throughput reservation ModelCode", "zh-CN": "设置吞吐预留 ModelCode 的溢出策略", @@ -52,13 +53,14 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; if (settings.dryRun) { - emitResult( - { - action: "deploy.overflow", - deployed_model: flags.deployedModel, - body: { overflow_strategy: flags.strategy }, - }, - "json", + const resultData1 = { + action: "deploy.overflow", + deployed_model: flags.deployedModel, + body: { overflow_strategy: flags.strategy }, + }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, ); return; } @@ -76,6 +78,10 @@ export default defineCommand({ { cause: { response, deployment } }, ); } - emitResult({ ...response, deployment }, "json"); + const resultData2 = { ...response, deployment }; + ctx.output.result( + resultData2, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData2, null, 2) } : undefined, + ); }, }); diff --git a/packages/commands/src/commands/deploy/pause.ts b/packages/commands/src/commands/deploy/pause.ts index 99b18ce50..e8ce1b41e 100644 --- a/packages/commands/src/commands/deploy/pause.ts +++ b/packages/commands/src/commands/deploy/pause.ts @@ -7,7 +7,6 @@ import { ExitCode, type FlagsDef, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; const PAUSE_FLAGS = { deployedModel: { @@ -36,6 +35,7 @@ const PAUSE_FLAGS = { * Precheck: status must be RUNNING or PENDING. */ export default defineCommand({ + defaultOutput: "json", description: { "en-US": "Pause a running model deployment (stops billing for mu/ptu)", "zh-CN": "暂停运行中的模型部署(mu/ptu 方案将停止计费)", @@ -67,7 +67,11 @@ export default defineCommand({ const deployedModel = flags.deployedModel; if (settings.dryRun) { - emitResult({ action: "deploy.pause", deployed_model: deployedModel }, "json"); + const resultData1 = { action: "deploy.pause", deployed_model: deployedModel }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, + ); return; } @@ -95,10 +99,12 @@ export default defineCommand({ const response = await stopModelService(ctx.client, deployedModel); - if (settings.quiet) { - emitBare(deployedModel); - } else { - emitResult({ deployed_model: deployedModel, action: "pause", ...response }, "json"); + { + const resultData2 = { deployed_model: deployedModel, action: "pause", ...response }; + ctx.output.result( + resultData2, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData2, null, 2) } : undefined, + ); } }, }); diff --git a/packages/commands/src/commands/deploy/resume.ts b/packages/commands/src/commands/deploy/resume.ts index d7e2e4ce0..71cb14f9d 100644 --- a/packages/commands/src/commands/deploy/resume.ts +++ b/packages/commands/src/commands/deploy/resume.ts @@ -7,7 +7,6 @@ import { ExitCode, type FlagsDef, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; const RESUME_FLAGS = { deployedModel: { @@ -35,6 +34,7 @@ const RESUME_FLAGS = { * Precheck: status must be STOPPED. */ export default defineCommand({ + defaultOutput: "json", description: { "en-US": "Resume a paused model deployment (brings service back online)", "zh-CN": "恢复已暂停的模型部署(使服务重新上线)", @@ -63,7 +63,11 @@ export default defineCommand({ const deployedModel = flags.deployedModel; if (settings.dryRun) { - emitResult({ action: "deploy.resume", deployed_model: deployedModel }, "json"); + const resultData1 = { action: "deploy.resume", deployed_model: deployedModel }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, + ); return; } @@ -91,10 +95,12 @@ export default defineCommand({ const response = await startModelService(ctx.client, deployedModel); - if (settings.quiet) { - emitBare(deployedModel); - } else { - emitResult({ deployed_model: deployedModel, action: "resume", ...response }, "json"); + { + const resultData2 = { deployed_model: deployedModel, action: "resume", ...response }; + ctx.output.result( + resultData2, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData2, null, 2) } : undefined, + ); } }, }); diff --git a/packages/commands/src/commands/deploy/scale.ts b/packages/commands/src/commands/deploy/scale.ts index cecc55a82..255fd79e7 100644 --- a/packages/commands/src/commands/deploy/scale.ts +++ b/packages/commands/src/commands/deploy/scale.ts @@ -14,7 +14,7 @@ import { type FlagsDef, type ScaleDeploymentRequest, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; + import { CAPACITY_FLAGS, PREPAID_FLAGS, @@ -88,6 +88,7 @@ async function resolveInstanceId(client: Client, deployedModel: string): Promise } export default defineCommand({ + defaultOutput: "json", description: { "en-US": "Scale a deployment's capacity", "zh-CN": "调整部署容量", @@ -161,19 +162,20 @@ export default defineCommand({ : { capacity: flags.capacity }; if (settings.dryRun) { - emitResult( - { - action: "deploy.scale", - deployed_model: deployedModel, - body, - ...(isPtu && flags.instanceId === undefined - ? { - instance_resolution: - "Execution must resolve exactly one unreleased instance or require --instance-id. / 执行时须确认唯一未释放实例,否则必须指定 --instance-id。", - } - : {}), - }, - "json", + const resultData1 = { + action: "deploy.scale", + deployed_model: deployedModel, + body, + ...(isPtu && flags.instanceId === undefined + ? { + instance_resolution: + "Execution must resolve exactly one unreleased instance or require --instance-id. / 执行时须确认唯一未释放实例,否则必须指定 --instance-id。", + } + : {}), + }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, ); return; } @@ -203,15 +205,14 @@ export default defineCommand({ const response = await submitCapacityWrite(requestId, () => scaleDeployment(ctx.client, deployedModel, body, undefined, requestId), ); - await finishMutation(ctx.client, deployedModel, response, flags, requestId); + await finishMutation(ctx, deployedModel, response, flags, requestId); return; } const response = await scaleDeployment(ctx.client, deployedModel, body); - if (settings.quiet) { - emitBare(deployedModel); - } else { - emitResult(response, "json"); - } + ctx.output.result( + response, + ctx.settings.output === "text" ? { text: JSON.stringify(response, null, 2) } : undefined, + ); }, }); diff --git a/packages/commands/src/commands/deploy/update.ts b/packages/commands/src/commands/deploy/update.ts index 326feb205..3c66d4fa1 100644 --- a/packages/commands/src/commands/deploy/update.ts +++ b/packages/commands/src/commands/deploy/update.ts @@ -1,5 +1,4 @@ import { defineCommand, updateDeployment, type FlagsDef } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; const UPDATE_FLAGS = { deployedModel: { @@ -30,6 +29,7 @@ const UPDATE_FLAGS = { * Body: at least one of `rpm_limit` (requests/min) or `tpm_limit` (tokens/min). */ export default defineCommand({ + defaultOutput: "json", description: { "en-US": "Update a deployment's rate limits (rpm_limit / tpm_limit)", "zh-CN": "更新部署的限流配置(rpm_limit / tpm_limit)", @@ -60,16 +60,19 @@ export default defineCommand({ if (flags.tpmLimit !== undefined) body.tpm_limit = flags.tpmLimit; if (settings.dryRun) { - emitResult({ action: "deploy.update", deployed_model: deployedModel, body }, "json"); + const resultData1 = { action: "deploy.update", deployed_model: deployedModel, body }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, + ); return; } const response = await updateDeployment(ctx.client, deployedModel, body); - if (settings.quiet) { - emitBare(deployedModel); - } else { - emitResult(response, "json"); - } + ctx.output.result( + response, + ctx.settings.output === "text" ? { text: JSON.stringify(response, null, 2) } : undefined, + ); }, }); diff --git a/packages/commands/src/commands/file/upload.ts b/packages/commands/src/commands/file/upload.ts index 5030810d7..bc0202339 100644 --- a/packages/commands/src/commands/file/upload.ts +++ b/packages/commands/src/commands/file/upload.ts @@ -1,5 +1,4 @@ -import { defineCommand, detectOutputFormat } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; +import { defineCommand } from "bailian-cli-core"; export default defineCommand({ description: { @@ -39,27 +38,20 @@ export default defineCommand({ const filePath = flags.file; const model = flags.model; - const format = detectOutputFormat(settings.output); - if (settings.dryRun) { - emitResult({ action: "upload", file: filePath, model }, format); + ctx.output.result({ action: "upload", file: filePath, model }); return; } const ossUrl = await ctx.client.uploadFile(filePath, model); - if (settings.quiet) { - emitBare(ossUrl); - } else { - emitResult( - { - url: ossUrl, - model, - expires_in: "48 hours", - note: "When using this URL in API calls, add header: X-DashScope-OssResourceResolve: enable", - }, - format, - ); + { + ctx.output.result({ + url: ossUrl, + model, + expires_in: "48 hours", + note: "When using this URL in API calls, add header: X-DashScope-OssResourceResolve: enable", + }); } }, }); diff --git a/packages/commands/src/commands/finetune/cancel.ts b/packages/commands/src/commands/finetune/cancel.ts index 4fb7a055b..d3a2f2465 100644 --- a/packages/commands/src/commands/finetune/cancel.ts +++ b/packages/commands/src/commands/finetune/cancel.ts @@ -1,5 +1,4 @@ import { defineCommand, cancelFineTune, type FlagsDef } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; const CANCEL_FLAGS = { jobId: { @@ -11,6 +10,7 @@ const CANCEL_FLAGS = { } satisfies FlagsDef; export default defineCommand({ + defaultOutput: "json", description: { "en-US": "Cancel a running fine-tune job", "zh-CN": "取消正在运行的微调任务" }, auth: "apiKey", usageArgs: "--job-id ", @@ -29,16 +29,19 @@ export default defineCommand({ const jobId = flags.jobId; if (settings.dryRun) { - emitResult({ action: "finetune.cancel", job_id: jobId }, "json"); + const resultData1 = { action: "finetune.cancel", job_id: jobId }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, + ); return; } const response = await cancelFineTune(ctx.client, jobId); - if (settings.quiet) { - emitBare(jobId); - } else { - emitResult(response, "json"); - } + ctx.output.result( + response, + ctx.settings.output === "text" ? { text: JSON.stringify(response, null, 2) } : undefined, + ); }, }); diff --git a/packages/commands/src/commands/finetune/capability.ts b/packages/commands/src/commands/finetune/capability.ts index 024ec09c7..5bafddea2 100644 --- a/packages/commands/src/commands/finetune/capability.ts +++ b/packages/commands/src/commands/finetune/capability.ts @@ -13,7 +13,6 @@ import { type ModelCapability, type FlagsDef, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; /** * Page through every foundation-model page (listFoundationModels, public — no @@ -46,6 +45,7 @@ const CAPABILITY_FLAGS = { } satisfies FlagsDef; export default defineCommand({ + defaultOutput: "json", description: { "en-US": "Query fine-tune training capability — by model (which training types it supports) or by training type (which models support it)", @@ -89,13 +89,14 @@ export default defineCommand({ const trainingType = flags.trainingType || undefined; if (settings.dryRun) { - emitResult( - { - action: "finetune.capability", - model, - training_type: trainingType, - }, - "json", + const resultData1 = { + action: "finetune.capability", + model, + training_type: trainingType, + }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, ); return; } @@ -104,22 +105,26 @@ export default defineCommand({ if (model) { const capability = await fetchModelCapability(settings, model); if (!capability) { - emitResult({ model, error: `No foundation model found matching "${model}".` }, "json"); + const resultData2 = { model, error: `No foundation model found matching "${model}".` }; + ctx.output.result( + resultData2, + ctx.settings.output === "text" + ? { text: JSON.stringify(resultData2, null, 2) } + : undefined, + ); return; } const supported = listSupportedTrainingTypes(capability); - if (settings.quiet) { - for (const value of supported) emitBare(value); - return; - } - emitResult( - { - model: capability.model ?? model, - supported, - supports: capability.supports, - trainingTypes: capability.trainingTypes, - }, - "json", + + const resultData3 = { + model: capability.model ?? model, + supported, + supports: capability.supports, + trainingTypes: capability.trainingTypes, + }; + ctx.output.result( + resultData3, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData3, null, 2) } : undefined, ); return; } @@ -141,19 +146,16 @@ export default defineCommand({ .filter((entry) => Boolean(entry.model)) .sort((left, right) => left.model.localeCompare(right.model)); - if (settings.quiet) { - for (const entry of matched) emitBare(entry.model); - return; - } - emitResult( - { - training_type: trainingType, - method, - variant, - count: matched.length, - models: matched, - }, - "json", + const resultData4 = { + training_type: trainingType, + method, + variant, + count: matched.length, + models: matched, + }; + ctx.output.result( + resultData4, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData4, null, 2) } : undefined, ); }, }); diff --git a/packages/commands/src/commands/finetune/checkpoints.ts b/packages/commands/src/commands/finetune/checkpoints.ts index d8f02dfa2..6c3aeb7ce 100644 --- a/packages/commands/src/commands/finetune/checkpoints.ts +++ b/packages/commands/src/commands/finetune/checkpoints.ts @@ -1,5 +1,4 @@ import { defineCommand, listCheckpoints, type FlagsDef } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; const CHECKPOINTS_FLAGS = { jobId: { @@ -13,6 +12,7 @@ const CHECKPOINTS_FLAGS = { const EXPIRY_WARN_THRESHOLD_MS = 72 * 60 * 60 * 1000; // 72 hours export default defineCommand({ + defaultOutput: "json", description: { "en-US": "List checkpoints produced by a fine-tune job", "zh-CN": "列出微调任务生成的 Checkpoint", @@ -39,7 +39,11 @@ export default defineCommand({ const jobId = flags.jobId; if (settings.dryRun) { - emitResult({ action: "finetune.checkpoints", job_id: jobId }, "json"); + const resultData1 = { action: "finetune.checkpoints", job_id: jobId }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, + ); return; } @@ -56,7 +60,11 @@ export default defineCommand({ expire_time: item.expire_time ?? "", })); - emitResult({ items, total, request_id: response.request_id }, "json"); + const resultData2 = { items, total, request_id: response.request_id }; + ctx.output.result( + resultData2, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData2, null, 2) } : undefined, + ); // Near-expiry warning: check if any non-expired checkpoint is within 72h of expiry. const now = Date.now(); @@ -68,10 +76,14 @@ export default defineCommand({ return remaining > 0 && remaining < EXPIRY_WARN_THRESHOLD_MS; }); if (expiringSoon.length > 0) { - process.stderr.write( - `\n[warning] ${expiringSoon.length} checkpoint(s) will expire within 72 hours. ` + - "Export or deploy before expiry to avoid losing the model artifact.\n", - ); + ctx.output.warning({ + code: "CHECKPOINTS_EXPIRING_SOON", + message: { + "en-US": `${expiringSoon.length} checkpoint(s) will expire within 72 hours. Export or deploy before expiry to avoid losing the model artifact.`, + "zh-CN": `${expiringSoon.length} 个 Checkpoint 将在 72 小时内过期。请在过期前导出或部署,避免丢失模型产物。`, + }, + details: { checkpoints: expiringSoon }, + }); } }, }); diff --git a/packages/commands/src/commands/finetune/compress-cancel.ts b/packages/commands/src/commands/finetune/compress-cancel.ts index 6d15ea310..8e03efdb4 100644 --- a/packages/commands/src/commands/finetune/compress-cancel.ts +++ b/packages/commands/src/commands/finetune/compress-cancel.ts @@ -1,8 +1,9 @@ import { defineCommand, cancelCompressJob } from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; + import { COMPRESS_JOB_ID_FLAGS } from "./compress-shared.ts"; export default defineCommand({ + defaultOutput: "json", description: { "en-US": "Cancel a compression job", "zh-CN": "取消压缩任务" }, auth: "apiKey", usageArgs: "--job-id ", @@ -17,11 +18,17 @@ export default defineCommand({ async run(ctx) { const jobId = ctx.flags.jobId; if (ctx.settings.dryRun) { - emitResult({ action: "finetune.compress.cancel", job_id: jobId }, "json"); + const resultData1 = { action: "finetune.compress.cancel", job_id: jobId }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, + ); return; } const response = await cancelCompressJob(ctx.client, jobId); - if (ctx.settings.quiet) emitBare(jobId); - else emitResult(response, "json"); + ctx.output.result( + response, + ctx.settings.output === "text" ? { text: JSON.stringify(response, null, 2) } : undefined, + ); }, }); diff --git a/packages/commands/src/commands/finetune/compress-create.ts b/packages/commands/src/commands/finetune/compress-create.ts index 331b1a524..dd9ae2d09 100644 --- a/packages/commands/src/commands/finetune/compress-create.ts +++ b/packages/commands/src/commands/finetune/compress-create.ts @@ -4,7 +4,7 @@ import { type CreateCompressJobRequest, type FlagsDef, } from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; + import { parseCommaSeparated, parseCompressHyperParameters } from "./compress-shared.ts"; const CREATE_FLAGS = { @@ -66,6 +66,7 @@ const CREATE_FLAGS = { } satisfies FlagsDef; export default defineCommand({ + defaultOutput: "json", description: { "en-US": "Create a model compression job", "zh-CN": "创建模型压缩任务" }, auth: "apiKey", usageArgs: "--model --template-id [flags]", @@ -106,11 +107,17 @@ export default defineCommand({ ...(ctx.flags.suffix ? { output_model_suffix: ctx.flags.suffix } : {}), }; if (ctx.settings.dryRun) { - emitResult({ action: "finetune.compress.create", request: body }, "json"); + const resultData1 = { action: "finetune.compress.create", request: body }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, + ); return; } const response = await createCompressJob(ctx.client, body); - if (ctx.settings.quiet) emitBare(response.output?.job_id ?? ""); - else emitResult(response, "json"); + ctx.output.result( + response, + ctx.settings.output === "text" ? { text: JSON.stringify(response, null, 2) } : undefined, + ); }, }); diff --git a/packages/commands/src/commands/finetune/compress-delete.ts b/packages/commands/src/commands/finetune/compress-delete.ts index fe1bc14bd..04d59b120 100644 --- a/packages/commands/src/commands/finetune/compress-delete.ts +++ b/packages/commands/src/commands/finetune/compress-delete.ts @@ -1,8 +1,9 @@ import { defineCommand, deleteCompressJob } from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; + import { COMPRESS_JOB_ID_FLAGS } from "./compress-shared.ts"; export default defineCommand({ + defaultOutput: "json", description: { "en-US": "Delete a compression job record", "zh-CN": "删除压缩任务记录" }, auth: "apiKey", risk: { @@ -25,11 +26,17 @@ export default defineCommand({ async run(ctx) { const jobId = ctx.flags.jobId; if (ctx.settings.dryRun) { - emitResult({ action: "finetune.compress.delete", job_id: jobId }, "json"); + const resultData1 = { action: "finetune.compress.delete", job_id: jobId }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, + ); return; } const response = await deleteCompressJob(ctx.client, jobId); - if (ctx.settings.quiet) emitBare(jobId); - else emitResult(response, "json"); + ctx.output.result( + response, + ctx.settings.output === "text" ? { text: JSON.stringify(response, null, 2) } : undefined, + ); }, }); diff --git a/packages/commands/src/commands/finetune/compress-get.ts b/packages/commands/src/commands/finetune/compress-get.ts index 8d0279284..c8275a998 100644 --- a/packages/commands/src/commands/finetune/compress-get.ts +++ b/packages/commands/src/commands/finetune/compress-get.ts @@ -1,8 +1,9 @@ import { defineCommand, getCompressJob } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; + import { COMPRESS_JOB_ID_FLAGS } from "./compress-shared.ts"; export default defineCommand({ + defaultOutput: "json", description: { "en-US": "Get a compression job", "zh-CN": "查询压缩任务详情" }, auth: "apiKey", usageArgs: "--job-id ", @@ -11,9 +12,17 @@ export default defineCommand({ async run(ctx) { const jobId = ctx.flags.jobId; if (ctx.settings.dryRun) { - emitResult({ action: "finetune.compress.get", job_id: jobId }, "json"); + const resultData1 = { action: "finetune.compress.get", job_id: jobId }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, + ); return; } - emitResult(await getCompressJob(ctx.client, jobId), "json"); + const resultData2 = await getCompressJob(ctx.client, jobId); + ctx.output.result( + resultData2, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData2, null, 2) } : undefined, + ); }, }); diff --git a/packages/commands/src/commands/finetune/compress-list.ts b/packages/commands/src/commands/finetune/compress-list.ts index 4539654f7..f1f4cb4da 100644 --- a/packages/commands/src/commands/finetune/compress-list.ts +++ b/packages/commands/src/commands/finetune/compress-list.ts @@ -1,5 +1,4 @@ import { defineCommand, listCompressJobs, type FlagsDef } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; const LIST_FLAGS = { status: { @@ -82,6 +81,7 @@ const LIST_FLAGS = { } satisfies FlagsDef; export default defineCommand({ + defaultOutput: "json", description: { "en-US": "List model compression jobs", "zh-CN": "列出模型压缩任务" }, auth: "apiKey", usageArgs: "[filters] [--page ] [--page-size ]", @@ -120,7 +120,11 @@ export default defineCommand({ pageSize: ctx.flags.pageSize, }; if (ctx.settings.dryRun) { - emitResult({ action: "finetune.compress.list", ...params }, "json"); + const resultData1 = { action: "finetune.compress.list", ...params }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, + ); return; } const response = await listCompressJobs(ctx.client, params); @@ -134,15 +138,16 @@ export default defineCommand({ quantized_output: job.quantized_output ?? "", create_time: job.create_time ?? "", })); - emitResult( - { - items, - total: output?.total, - page_no: output?.page_no, - page_size: output?.page_size, - request_id: response.request_id, - }, - "json", + const resultData2 = { + items, + total: output?.total, + page_no: output?.page_no, + page_size: output?.page_size, + request_id: response.request_id, + }; + ctx.output.result( + resultData2, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData2, null, 2) } : undefined, ); }, }); diff --git a/packages/commands/src/commands/finetune/compress-logs.ts b/packages/commands/src/commands/finetune/compress-logs.ts index 1a0544ad7..a1db5f17d 100644 --- a/packages/commands/src/commands/finetune/compress-logs.ts +++ b/packages/commands/src/commands/finetune/compress-logs.ts @@ -1,5 +1,5 @@ import { defineCommand, getCompressJobLogs, type FlagsDef } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; + import { COMPRESS_JOB_ID_FLAGS } from "./compress-shared.ts"; const LOG_FLAGS = { @@ -28,6 +28,7 @@ const LOG_FLAGS = { } satisfies FlagsDef; export default defineCommand({ + defaultOutput: "json", description: { "en-US": "Get compression job logs", "zh-CN": "获取压缩任务日志" }, auth: "apiKey", usageArgs: "--job-id [--offset ] [--lines | --tail ]", @@ -57,15 +58,16 @@ export default defineCommand({ async run(ctx) { const jobId = ctx.flags.jobId; if (ctx.settings.dryRun) { - emitResult( - { - action: "finetune.compress.logs", - job_id: jobId, - offset: ctx.flags.offset, - line: ctx.flags.lines, - tail: ctx.flags.tail, - }, - "json", + const resultData1 = { + action: "finetune.compress.logs", + job_id: jobId, + offset: ctx.flags.offset, + line: ctx.flags.lines, + tail: ctx.flags.tail, + }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, ); return; } @@ -73,18 +75,23 @@ export default defineCommand({ const first = await getCompressJobLogs(ctx.client, jobId, { offset: 0, line: 1 }); const total = first.output?.total ?? 0; const offset = Math.max(0, total - ctx.flags.tail); - emitResult( - await getCompressJobLogs(ctx.client, jobId, { offset, line: ctx.flags.tail }), - "json", + const resultData2 = await getCompressJobLogs(ctx.client, jobId, { + offset, + line: ctx.flags.tail, + }); + ctx.output.result( + resultData2, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData2, null, 2) } : undefined, ); return; } - emitResult( - await getCompressJobLogs(ctx.client, jobId, { - offset: ctx.flags.offset, - line: ctx.flags.lines, - }), - "json", + const resultData3 = await getCompressJobLogs(ctx.client, jobId, { + offset: ctx.flags.offset, + line: ctx.flags.lines, + }); + ctx.output.result( + resultData3, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData3, null, 2) } : undefined, ); }, }); diff --git a/packages/commands/src/commands/finetune/compress-templates.ts b/packages/commands/src/commands/finetune/compress-templates.ts index 10a0cea0c..a69f61d48 100644 --- a/packages/commands/src/commands/finetune/compress-templates.ts +++ b/packages/commands/src/commands/finetune/compress-templates.ts @@ -1,5 +1,4 @@ import { defineCommand, listCompressTemplates, type FlagsDef } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; const TEMPLATE_FLAGS = { model: { @@ -13,6 +12,7 @@ const TEMPLATE_FLAGS = { } satisfies FlagsDef; export default defineCommand({ + defaultOutput: "json", description: { "en-US": "List quantizable models and their compression templates", "zh-CN": "列出可量化模型及其压缩模板", @@ -50,10 +50,17 @@ export default defineCommand({ const model = ctx.flags.model || undefined; const lang = ctx.localize({ "en-US": "en-US", "zh-CN": "zh-CN" }); if (ctx.settings.dryRun) { - emitResult({ action: "finetune.compress.templates", model, lang }, "json"); + const resultData1 = { action: "finetune.compress.templates", model, lang }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, + ); return; } const response = await listCompressTemplates(ctx.client, { model, lang }); - emitResult(response, "json"); + ctx.output.result( + response, + ctx.settings.output === "text" ? { text: JSON.stringify(response, null, 2) } : undefined, + ); }, }); diff --git a/packages/commands/src/commands/finetune/compress-watch.ts b/packages/commands/src/commands/finetune/compress-watch.ts index 76d59abac..d54ce8494 100644 --- a/packages/commands/src/commands/finetune/compress-watch.ts +++ b/packages/commands/src/commands/finetune/compress-watch.ts @@ -5,7 +5,7 @@ import { getCompressJob, type FlagsDef, } from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; + import { COMPRESS_JOB_ID_FLAGS, COMPRESS_TERMINAL_STATUSES, @@ -42,6 +42,7 @@ const WATCH_FLAGS = { } satisfies FlagsDef; export default defineCommand({ + defaultOutput: "json", description: { "en-US": "Probe or follow a compression job's status", "zh-CN": "查询或持续跟踪压缩任务状态", @@ -78,33 +79,33 @@ export default defineCommand({ const jobId = ctx.flags.jobId; const intervalSec = ctx.flags.interval ?? DEFAULT_INTERVAL_SEC; if (ctx.settings.dryRun) { - emitResult( - { - action: "finetune.compress.watch", - job_id: jobId, - follow: ctx.flags.follow, - interval: intervalSec, - timeout: ctx.flags.pollTimeout, - }, - "json", + const resultData1 = { + action: "finetune.compress.watch", + job_id: jobId, + follow: ctx.flags.follow, + interval: intervalSec, + timeout: ctx.flags.pollTimeout, + }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, ); return; } if (!ctx.flags.follow) { const response = await getCompressJob(ctx.client, jobId); const status = String(response.output?.status ?? "UNKNOWN").toUpperCase(); - if (ctx.settings.quiet) emitBare(status); - else - emitResult( - { - job_id: jobId, - status, - terminal: COMPRESS_TERMINAL_STATUSES.has(status), - quantized_output: response.output?.quantized_output, - request_id: response.request_id, - }, - "json", - ); + const resultData2 = { + job_id: jobId, + status, + terminal: COMPRESS_TERMINAL_STATUSES.has(status), + quantized_output: response.output?.quantized_output, + request_id: response.request_id, + }; + ctx.output.result( + resultData2, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData2, null, 2) } : undefined, + ); if (COMPRESS_TERMINAL_STATUSES.has(status) && status !== "SUCCEEDED") throw formatCompressFailure(jobId, status, response.output?.error?.message); return; @@ -119,8 +120,12 @@ export default defineCommand({ const response = await getCompressJob(ctx.client, jobId, controller.signal); const status = String(response.output?.status ?? "UNKNOWN").toUpperCase(); if (COMPRESS_TERMINAL_STATUSES.has(status)) { - if (ctx.settings.quiet) emitBare(status); - else emitResult(response, "json"); + ctx.output.result( + response, + ctx.settings.output === "text" + ? { text: JSON.stringify(response, null, 2) } + : undefined, + ); if (status !== "SUCCEEDED") throw formatCompressFailure(jobId, status, response.output?.error?.message); return; @@ -138,9 +143,7 @@ export default defineCommand({ } } catch (error) { if (controller.signal.aborted) { - process.stderr.write( - `\n${ctx.localize({ "en-US": "Interrupted.", "zh-CN": "已中断。" })}\n`, - ); + ctx.output.info({ "en-US": "Interrupted.", "zh-CN": "已中断。" }); return; } throw error; diff --git a/packages/commands/src/commands/finetune/create.ts b/packages/commands/src/commands/finetune/create.ts index d0e56e66a..5b4081f43 100644 --- a/packages/commands/src/commands/finetune/create.ts +++ b/packages/commands/src/commands/finetune/create.ts @@ -14,8 +14,6 @@ import { formatIssue, BailianError, ExitCode, - type Client, - type Settings, type CommandContext, type CreateFineTuneRequest, type FineTuneHyperParameters, @@ -26,7 +24,6 @@ import { } from "bailian-cli-core"; import { existsSync, statSync } from "fs"; import { basename } from "path"; -import { emitResult, emitBare } from "bailian-cli-runtime"; /** * A `--datasets` / `--validations` token is treated as a local file to upload @@ -74,8 +71,7 @@ interface ResolvedDataset { * validated (the preview never touches the network or the disk beyond stat). */ async function analyzeDatasetTokens( - settings: Settings, - binName: string, + context: Pick, raw: string, label: string, profile: TrainingProfile, @@ -84,6 +80,8 @@ async function analyzeDatasetTokens( /** Pre-detected modality for a known path (avoids re-opening the file). */ knownModality?: { path: string; modality: DataModality }, ): Promise { + const { settings } = context; + const binName = context.identity.binName; const tokens = raw .split(",") .map((token) => token.trim()) @@ -142,16 +140,15 @@ async function analyzeDatasetTokens( ); throw new BailianError(lines.join("\n"), ExitCode.GENERAL); } - if (result.warnings.length > 0 && !settings.quiet) { - process.stderr.write( - `Dataset validation passed with ${result.warnings.length} warning(s) for ${token}:\n`, - ); - for (const warning of result.warnings.slice(0, 5)) { - process.stderr.write(`${formatIssue(warning)}\n`); - } - if (result.warnings.length > 5) { - process.stderr.write(` … and ${result.warnings.length - 5} more.\n`); - } + if (result.warnings.length > 0) { + context.output.warning({ + code: "DATASET_VALIDATION_WARNINGS", + message: { + "en-US": `Dataset validation passed with ${result.warnings.length} warning(s) for ${token}.`, + "zh-CN": `${token} 数据集校验通过,共 ${result.warnings.length} 条警告。`, + }, + details: { file: token, warnings: result.warnings }, + }); } // Accumulate the sample count so the caller can pre-flight the batch-size @@ -179,12 +176,12 @@ async function analyzeDatasetTokens( * happened in `analyzeDatasetTokens`, so this is pure upload. */ async function uploadResolvedLocal( - client: Client, - settings: Settings, + context: Pick, resolved: ResolvedDataset, purpose: string, label: string, ): Promise { + const { client } = context; const uploaded: DatasetFile[] = []; for (const [index, token] of resolved.fileIds.entries()) { if (!isLocalPath(token)) continue; @@ -197,11 +194,10 @@ async function uploadResolvedLocal( } uploaded.push(file); resolved.fileIds[index] = file.file_id; - if (!settings.quiet) { - process.stderr.write( - `Uploaded ${basename(token)} → ${file.file_id} (auto from --${label})\n`, - ); - } + context.output.info({ + "en-US": `Uploaded ${basename(token)} → ${file.file_id} (auto from --${label})`, + "zh-CN": `已上传 ${basename(token)} → ${file.file_id}(来自 --${label})`, + }); } return uploaded; } @@ -565,7 +561,7 @@ async function runCreate( commandModality: CommandModality, ctx: CommandContext, ): Promise { - const { identity, settings } = ctx; + const { settings } = ctx; const flags = ctx.flags as Record; const model = flags.baseModel as string; const datasetsRaw = flags.datasets as string; @@ -631,8 +627,7 @@ async function runCreate( } const training = await analyzeDatasetTokens( - settings, - identity.binName, + ctx, datasetsRaw, "datasets", profile, @@ -644,8 +639,7 @@ async function runCreate( const validation = flags.validations ? await analyzeDatasetTokens( - settings, - identity.binName, + ctx, flags.validations as string, "validations", profile, @@ -745,30 +739,33 @@ async function runCreate( if ( flags.batchSize !== undefined && hp.batch_size !== undefined && - !settings.quiet && !profile.shouldSkipGate("batch_size", modality) ) { const requested = flags.batchSize as number; if (hp.batch_size !== requested) { - process.stderr.write( - `warning: --batch-size ${requested} clamped to ${hp.batch_size} ` + - `(server range [8, 1024] for the common training types).\n`, - ); + ctx.output.warning({ + code: "TRAINING_BATCH_SIZE_CLAMPED", + message: { + "en-US": `--batch-size ${requested} clamped to ${hp.batch_size} (server range [8, 1024] for the common training types).`, + "zh-CN": `--batch-size ${requested} 已调整为 ${hp.batch_size}(常用训练类型的服务端范围为 [8, 1024])。`, + }, + details: { requested, batch_size: hp.batch_size }, + }); } } // For modalities that skip the batch_size gate, warn the user that their // explicit --batch-size was discarded (model uses a fixed batch_size). - if ( - flags.batchSize !== undefined && - !settings.quiet && - profile.shouldSkipGate("batch_size", modality) - ) { + if (flags.batchSize !== undefined && profile.shouldSkipGate("batch_size", modality)) { const requested = flags.batchSize as number; if (hp.batch_size !== undefined && hp.batch_size !== requested) { - process.stderr.write( - `warning: --batch-size ${requested} ignored for ${modality} training ` + - `(model uses a fixed batch_size of ${hp.batch_size}).\n`, - ); + ctx.output.warning({ + code: "TRAINING_BATCH_SIZE_IGNORED", + message: { + "en-US": `--batch-size ${requested} ignored for ${modality} training (model uses a fixed batch_size of ${hp.batch_size}).`, + "zh-CN": `${modality} 训练忽略了 --batch-size ${requested}(模型使用固定 batch_size ${hp.batch_size})。`, + }, + details: { requested, batch_size: hp.batch_size, modality }, + }); } } @@ -823,12 +820,14 @@ async function runCreate( try { capability = await fetchModelCapability(settings, model); } catch (error) { - if (!settings.quiet) { - process.stderr.write( - `warning: model capability lookup failed (${(error as Error).message}); ` + - "proceeding without local pre-flight.\n", - ); - } + ctx.output.warning({ + code: "TRAINING_CAPABILITY_LOOKUP_FAILED", + message: { + "en-US": "Model capability lookup failed; proceeding without local pre-flight.", + "zh-CN": "模型能力查询失败;将跳过本地预检继续提交。", + }, + details: { reason: error instanceof Error ? error.message : undefined }, + }); } if (capability && !listSupportedTrainingTypes(capability).includes(trainingType)) { const supported = listSupportedTrainingTypes(capability); @@ -846,9 +845,9 @@ async function runCreate( // capability check) has cleared them. This swaps the placeholder path // entries in `training.fileIds` / `validation?.fileIds` for real file-ids. if (!settings.dryRun) { - await uploadResolvedLocal(ctx.client, settings, training, "fine-tune", "datasets"); + await uploadResolvedLocal(ctx, training, "fine-tune", "datasets"); if (validation) { - await uploadResolvedLocal(ctx.client, settings, validation, "fine-tune", "validations"); + await uploadResolvedLocal(ctx, validation, "fine-tune", "validations"); } } @@ -872,27 +871,28 @@ async function runCreate( ...training.localPaths.map((path) => ({ field: "datasets", path })), ...(validation?.localPaths ?? []).map((path) => ({ field: "validations", path })), ]; - emitResult( + const resultData1 = pending.length > 0 ? { action: "finetune.create", body, pending_uploads: pending } - : { action: "finetune.create", body }, - "json", + : { action: "finetune.create", body }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, ); return; } const response = await createFineTune(ctx.client, body); - const job = response.output ?? response.data; - if (settings.quiet) { - if (job?.job_id) emitBare(job.job_id); - } else { - emitResult(response, "json"); - } + ctx.output.result( + response, + ctx.settings.output === "text" ? { text: JSON.stringify(response, null, 2) } : undefined, + ); } /** `bl finetune text create` — fine-tune a text model. Datasets are `.jsonl`. */ export const finetuneTextCreate = defineCommand({ + defaultOutput: "json", description: { "en-US": "Create a text model fine-tune job (sft | sft-lora | dpo | dpo-lora | cpt)", "zh-CN": "创建文本模型微调任务(sft | sft-lora | dpo | dpo-lora | cpt)", @@ -917,6 +917,7 @@ export const finetuneTextCreate = defineCommand({ /** `bl finetune audio create` — fine-tune an audio TTS model. Datasets are `.zip`. */ export const finetuneAudioCreate = defineCommand({ + defaultOutput: "json", description: { "en-US": "Create an audio TTS model fine-tune job (sft-lora)", "zh-CN": "创建音频 TTS 模型微调任务(sft-lora)", @@ -937,6 +938,7 @@ export const finetuneAudioCreate = defineCommand({ /** `bl finetune image create` — fine-tune an image generation model. Datasets are `.zip`. */ export const finetuneImageCreate = defineCommand({ + defaultOutput: "json", description: { "en-US": "Create an image generation model fine-tune job (sft-lora)", "zh-CN": "创建图片生成模型微调任务(sft-lora)", @@ -976,6 +978,7 @@ const VIDEO_NOTES = [ /** `bl finetune video create` — fine-tune a video generation model. Datasets are `.zip`. */ export const finetuneVideoCreate = defineCommand({ + defaultOutput: "json", description: { "en-US": "Create a video generation model fine-tune job (Wan i2v/kf2v, efficient_sft)", "zh-CN": "创建视频生成模型微调任务(Wan i2v/kf2v,efficient_sft)", diff --git a/packages/commands/src/commands/finetune/delete.ts b/packages/commands/src/commands/finetune/delete.ts index 3cb7e474f..cf2cf6d5a 100644 --- a/packages/commands/src/commands/finetune/delete.ts +++ b/packages/commands/src/commands/finetune/delete.ts @@ -1,5 +1,4 @@ import { defineCommand, deleteFineTune, type FlagsDef } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; const DELETE_FLAGS = { jobId: { @@ -11,6 +10,7 @@ const DELETE_FLAGS = { } satisfies FlagsDef; export default defineCommand({ + defaultOutput: "json", description: { "en-US": "Delete a fine-tune job record", "zh-CN": "删除微调任务记录" }, auth: "apiKey", risk: { @@ -39,16 +39,19 @@ export default defineCommand({ const jobId = flags.jobId; if (settings.dryRun) { - emitResult({ action: "finetune.delete", job_id: jobId }, "json"); + const resultData1 = { action: "finetune.delete", job_id: jobId }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, + ); return; } const response = await deleteFineTune(ctx.client, jobId); - if (settings.quiet) { - emitBare(jobId); - } else { - emitResult(response, "json"); - } + ctx.output.result( + response, + ctx.settings.output === "text" ? { text: JSON.stringify(response, null, 2) } : undefined, + ); }, }); diff --git a/packages/commands/src/commands/finetune/export.ts b/packages/commands/src/commands/finetune/export.ts index f753b6366..422654718 100644 --- a/packages/commands/src/commands/finetune/export.ts +++ b/packages/commands/src/commands/finetune/export.ts @@ -1,5 +1,4 @@ import { defineCommand, exportCheckpoint, type FlagsDef } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; const EXPORT_FLAGS = { jobId: { @@ -26,6 +25,7 @@ const EXPORT_FLAGS = { } satisfies FlagsDef; export default defineCommand({ + defaultOutput: "json", description: { "en-US": "Publish a checkpoint as a deployable model", "zh-CN": "将 Checkpoint 发布为可部署模型", @@ -49,26 +49,24 @@ export default defineCommand({ const modelName = flags.modelName; if (settings.dryRun) { - emitResult( - { - action: "finetune.export", - job_id: jobId, - checkpoint, - model_name: modelName, - }, - "json", + const resultData1 = { + action: "finetune.export", + job_id: jobId, + checkpoint, + model_name: modelName, + }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, ); return; } const response = await exportCheckpoint(ctx.client, jobId, checkpoint, modelName); - const payload = response.output ?? response.data; - const exported = payload?.model_name ?? modelName; - if (settings.quiet) { - emitBare(exported); - } else { - emitResult(response, "json"); - } + ctx.output.result( + response, + ctx.settings.output === "text" ? { text: JSON.stringify(response, null, 2) } : undefined, + ); }, }); diff --git a/packages/commands/src/commands/finetune/get.ts b/packages/commands/src/commands/finetune/get.ts index 9d2894daa..546134de0 100644 --- a/packages/commands/src/commands/finetune/get.ts +++ b/packages/commands/src/commands/finetune/get.ts @@ -1,5 +1,5 @@ import { defineCommand, getFineTune, type FlagsDef } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; + import { computeActualFee } from "./fee.ts"; const GET_FLAGS = { @@ -12,6 +12,7 @@ const GET_FLAGS = { } satisfies FlagsDef; export default defineCommand({ + defaultOutput: "json", description: { "en-US": "Get details of a single fine-tune job", "zh-CN": "获取单个微调任务的详情", @@ -25,7 +26,11 @@ export default defineCommand({ const jobId = flags.jobId; if (settings.dryRun) { - emitResult({ action: "finetune.get", job_id: jobId }, "json"); + const resultData1 = { action: "finetune.get", job_id: jobId }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, + ); return; } @@ -33,7 +38,11 @@ export default defineCommand({ const job = response.output ?? response.data; if (!job) { - emitResult({ job_id: jobId, error: "No data returned" }, "json"); + const resultData2 = { job_id: jobId, error: "No data returned" }; + ctx.output.result( + resultData2, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData2, null, 2) } : undefined, + ); return; } @@ -75,6 +84,10 @@ export default defineCommand({ } } - emitResult({ ...item, request_id: response.request_id }, "json"); + const resultData3 = { ...item, request_id: response.request_id }; + ctx.output.result( + resultData3, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData3, null, 2) } : undefined, + ); }, }); diff --git a/packages/commands/src/commands/finetune/list.ts b/packages/commands/src/commands/finetune/list.ts index e5c08a862..ded6b394f 100644 --- a/packages/commands/src/commands/finetune/list.ts +++ b/packages/commands/src/commands/finetune/list.ts @@ -1,5 +1,4 @@ import { defineCommand, listFineTunes, type FlagsDef } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; const LIST_FLAGS = { page: { @@ -34,6 +33,7 @@ const LIST_FLAGS = { } satisfies FlagsDef; export default defineCommand({ + defaultOutput: "json", description: { "en-US": "List fine-tune jobs", "zh-CN": "列出微调任务" }, auth: "apiKey", usageArgs: "[--page ] [--page-size ] [--status ] [--base-model ]", @@ -47,9 +47,16 @@ export default defineCommand({ const model = flags.baseModel || undefined; if (settings.dryRun) { - emitResult( - { action: "finetune.list", page: pageNo, page_size: pageSize, status, model }, - "json", + const resultData1 = { + action: "finetune.list", + page: pageNo, + page_size: pageSize, + status, + model, + }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, ); return; } @@ -68,6 +75,10 @@ export default defineCommand({ created_at: job.create_time ?? job.gmt_create ?? "", })); - emitResult({ items, total, request_id: response.request_id }, "json"); + const resultData2 = { items, total, request_id: response.request_id }; + ctx.output.result( + resultData2, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData2, null, 2) } : undefined, + ); }, }); diff --git a/packages/commands/src/commands/finetune/logs.ts b/packages/commands/src/commands/finetune/logs.ts index 82c1232e5..7a29c7e69 100644 --- a/packages/commands/src/commands/finetune/logs.ts +++ b/packages/commands/src/commands/finetune/logs.ts @@ -5,7 +5,6 @@ import { type FineTuneLogEntry, type FlagsDef, } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; /** * Render a single log entry as a single line (used for search matching: @@ -103,6 +102,7 @@ const LOGS_FLAGS = { } satisfies FlagsDef; export default defineCommand({ + defaultOutput: "json", description: { "en-US": "Fetch training logs for a fine-tune job", "zh-CN": "获取微调任务的训练日志", @@ -127,16 +127,17 @@ export default defineCommand({ const tail = flags.tail; if (settings.dryRun) { - emitResult( - { - action: "finetune.logs", - job_id: jobId, - page: pageNo, - page_size: pageSize, - search, - tail, - }, - "json", + const resultData1 = { + action: "finetune.logs", + job_id: jobId, + page: pageNo, + page_size: pageSize, + search, + tail, + }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, ); return; } @@ -161,22 +162,26 @@ export default defineCommand({ const result = tailApplied !== undefined ? scanned.slice(scanned.length - tailApplied) : scanned; - emitResult( - { - ...(matched !== undefined ? { matched } : {}), - scanned: entries.length, - total: total || entries.length, - ...(search ? { search } : {}), - ...(tailApplied !== undefined ? { tail: tailApplied } : {}), - logs: result, - }, - "json", + const resultData2 = { + ...(matched !== undefined ? { matched } : {}), + scanned: entries.length, + total: total || entries.length, + ...(search ? { search } : {}), + ...(tailApplied !== undefined ? { tail: tailApplied } : {}), + logs: result, + }; + ctx.output.result( + resultData2, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData2, null, 2) } : undefined, ); return; } // Default: single page, verbatim response. const response = await getFineTuneLogs(ctx.client, jobId, { pageNo, pageSize }); - emitResult(response, "json"); + ctx.output.result( + response, + ctx.settings.output === "text" ? { text: JSON.stringify(response, null, 2) } : undefined, + ); }, }); diff --git a/packages/commands/src/commands/finetune/price.ts b/packages/commands/src/commands/finetune/price.ts index e24c59a7b..8e0d4ec53 100644 --- a/packages/commands/src/commands/finetune/price.ts +++ b/packages/commands/src/commands/finetune/price.ts @@ -7,7 +7,6 @@ import { ExitCode, type FlagsDef, } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; const PRICE_FLAGS = { baseModel: { @@ -56,6 +55,7 @@ const ESTIMATE_MAX_LENGTH = 8192; const DEFAULT_N_EPOCHS = 3; export default defineCommand({ + defaultOutput: "json", description: { "en-US": "Estimate the training cost for a fine-tune job (token billing)", "zh-CN": "估算微调任务的训练费用(按 Token 计费)", @@ -106,9 +106,16 @@ export default defineCommand({ } if (settings.dryRun) { - emitResult( - { action: "finetune.price", model, datasets: datasetIds, trainingType, nEpochs }, - "json", + const resultData1 = { + action: "finetune.price", + model, + datasets: datasetIds, + trainingType, + nEpochs, + }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, ); return; } @@ -146,21 +153,22 @@ export default defineCommand({ const minFee = (minTokens / 1000) * unitPrice; const maxFee = (maxTokens / 1000) * unitPrice; - emitResult( - { - model, - training_type: trainingType, - n_epochs: nEpochs, - unit_price: unitPrice, - price_unit: priceInfo.priceUnit ?? "千Token", - estimated_tokens: { min: minTokens, max: maxTokens }, - estimated_fee_yuan: { - min: Number(minFee.toFixed(4)), - max: Number(maxFee.toFixed(4)), - }, - disclaimer: "Server-side estimate; final cost is subject to the bill.", + const resultData2 = { + model, + training_type: trainingType, + n_epochs: nEpochs, + unit_price: unitPrice, + price_unit: priceInfo.priceUnit ?? "千Token", + estimated_tokens: { min: minTokens, max: maxTokens }, + estimated_fee_yuan: { + min: Number(minFee.toFixed(4)), + max: Number(maxFee.toFixed(4)), }, - "json", + disclaimer: "Server-side estimate; final cost is subject to the bill.", + }; + ctx.output.result( + resultData2, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData2, null, 2) } : undefined, ); }, }); diff --git a/packages/commands/src/commands/finetune/watch.ts b/packages/commands/src/commands/finetune/watch.ts index a4fad6eb8..0f627bffe 100644 --- a/packages/commands/src/commands/finetune/watch.ts +++ b/packages/commands/src/commands/finetune/watch.ts @@ -5,7 +5,7 @@ import { ExitCode, type FlagsDef, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; + import { computeActualFee } from "./fee.ts"; const DEFAULT_INTERVAL_SEC = 10; @@ -83,6 +83,7 @@ const WATCH_FLAGS = { } satisfies FlagsDef; export default defineCommand({ + defaultOutput: "json", description: { "en-US": "Probe a fine-tune job's status (default: single non-blocking fetch). Pass --follow to poll until terminal.", @@ -139,15 +140,16 @@ export default defineCommand({ const pollTimeoutSec = flags.pollTimeout; if (settings.dryRun) { - emitResult( - { - action: "finetune.watch", - job_id: jobId, - follow, - interval: intervalSec, - timeout: pollTimeoutSec, - }, - "json", + const resultData1 = { + action: "finetune.watch", + job_id: jobId, + follow, + interval: intervalSec, + timeout: pollTimeoutSec, + }; + ctx.output.result( + resultData1, + ctx.settings.output === "text" ? { text: JSON.stringify(resultData1, null, 2) } : undefined, ); return; } @@ -162,10 +164,7 @@ export default defineCommand({ const status = String(job?.status ?? "").toUpperCase(); const terminal = TERMINAL_STATUSES.has(status); - if (settings.quiet) { - // Just the status word — ideal for `status=$(... finetune watch ... --quiet)`. - emitBare(status || "UNKNOWN"); - } else { + { const output: Record = { job_id: jobId, status: status || "UNKNOWN", @@ -182,7 +181,10 @@ export default defineCommand({ output.cost_basis = `${fee.unitPrice} 元/${fee.priceUnit}`; } } - emitResult(output, "json"); + ctx.output.result( + output, + ctx.settings.output === "text" ? { text: JSON.stringify(output, null, 2) } : undefined, + ); } if (terminal && status !== "SUCCEEDED") { @@ -209,16 +211,17 @@ export default defineCommand({ const job = response.output ?? response.data; const status = String(job?.status ?? "").toUpperCase(); - if (!settings.quiet && status !== lastStatus) { - process.stderr.write(`${nowStamp()} ${jobId} ${status || "UNKNOWN"}\n`); + if (status !== lastStatus) { + ctx.output.progress({ + "en-US": `${nowStamp()} ${jobId} ${status || "UNKNOWN"}`, + "zh-CN": `${nowStamp()} ${jobId} ${status || "UNKNOWN"}`, + }); lastStatus = status; } if (TERMINAL_STATUSES.has(status)) { const elapsed = Date.now() - startedAt; - if (settings.quiet) { - emitBare(status || "UNKNOWN"); - } else { + { // Enrich the raw response with actual fee when usage is available. const usageTokens = typeof job?.usage === "number" ? job.usage : undefined; const enriched: Record = { ...response }; @@ -230,7 +233,12 @@ export default defineCommand({ enriched.cost_basis = `${fee.unitPrice} 元/${fee.priceUnit}`; } } - emitResult(enriched, "json"); + ctx.output.result( + enriched, + ctx.settings.output === "text" + ? { text: JSON.stringify(enriched, null, 2) } + : undefined, + ); } if (status !== "SUCCEEDED") { throw new BailianError( @@ -256,7 +264,7 @@ export default defineCommand({ // Any other error (including the BailianError thrown above) propagates to // the central handler. if (controller.signal.aborted) { - process.stderr.write("\nInterrupted.\n"); + ctx.output.info({ "en-US": "Interrupted.", "zh-CN": "已中断。" }); return; } throw error; diff --git a/packages/commands/src/commands/image/edit.ts b/packages/commands/src/commands/image/edit.ts index e56ee9656..8a38b27d2 100644 --- a/packages/commands/src/commands/image/edit.ts +++ b/packages/commands/src/commands/image/edit.ts @@ -1,17 +1,16 @@ import { defineCommand, taskPath, - detectOutputFormat, resolveOutputDir, generateFilename, stripUndefined, + type CommandOutput, type Client, type DashScopeImageRequest, type DashScopeImageSyncResponse, type DashScopeAsyncResponse, type DashScopeTaskResponse, type FlagsDef, - type OutputFormat, type ParsedFlags, type Settings, ExitCode, @@ -31,7 +30,6 @@ import { import { poll } from "bailian-cli-runtime"; import { downloadFile } from "bailian-cli-runtime"; import { runConcurrent, downloadParallel, getConcurrency } from "bailian-cli-runtime"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { resolveImageSize } from "bailian-cli-runtime"; import { join } from "path"; import { BOOL_FLAG_PROMPT_EXTEND_CLI_TRUE, BOOL_FLAG_WATERMARK } from "bailian-cli-runtime"; @@ -216,14 +214,7 @@ export default defineCommand({ const resolvedModel = flags.model || settings.defaultImageModel; if (isAutoModel(resolvedModel)) { - await handleAutoMcpMode( - ctx.client, - ctx.localize, - settings, - flags, - rawImages, - detectOutputFormat(settings.output), - ); + await handleAutoMcpMode(ctx.client, ctx.output, ctx.localize, settings, flags, rawImages); return; } @@ -305,8 +296,6 @@ export default defineCommand({ // Remove undefined parameters stripUndefined(body.parameters as Record); - const format = detectOutputFormat(settings.output); - if (settings.dryRun) { let previewBody: DashScopeImageRequest = body; if ("messages" in body.input) { @@ -341,36 +330,36 @@ export default defineCommand({ }, }; } - emitResult( - { request: previewBody, mode: route.useSync ? "sync" : "async", path: route.path }, - format, - ); + ctx.output.result({ + request: previewBody, + mode: route.useSync ? "sync" : "async", + path: route.path, + }); return; } - if (!settings.quiet) { - process.stderr.write( - `[Model: ${model}] [Mode: ${route.useSync ? "sync" : "async"}] [Images: ${resolvedImages.length}]\n`, - ); - } + ctx.output.info({ + "en-US": `[Model: ${model}] [Mode: ${route.useSync ? "sync" : "async"}] [Images: ${resolvedImages.length}]`, + "zh-CN": `[模型:${model}] [模式:${route.useSync ? "同步" : "异步"}] [图片:${resolvedImages.length}]`, + }); const concurrent = getConcurrency(flags); if (route.useSync) { - await handleSyncMode(ctx.client, settings, route.path, body, flags, format, concurrent); + await handleSyncMode(ctx.client, ctx.output, settings, route.path, body, flags, concurrent); } else { - await handleAsyncMode(ctx.client, settings, route.path, body, flags, format, concurrent); + await handleAsyncMode(ctx.client, ctx.output, settings, route.path, body, flags, concurrent); } }, }); async function handleAutoMcpMode( client: Client, + commandOutput: CommandOutput, localize: Localize, settings: Settings, flags: EditFlags, rawImages: string[], - format: OutputFormat, ): Promise { assertAutoMcpAvailable(client, localize); assertMcpCompatible("imageEdit", flags, localize); @@ -394,27 +383,25 @@ async function handleAutoMcpMode( }); if (settings.dryRun) { - emitResult( - { - server: AIGC_MCP_SERVER.image, - url: client.url(bailianMcpPath(AIGC_MCP_SERVER.image)), - tool: AIGC_MCP_TOOL.imageEdit, - arguments: { ...args, image_url: redactDataUri(imageUrl) }, - mode: "mcp", - concurrent, - }, - format, - ); + commandOutput.result({ + server: AIGC_MCP_SERVER.image, + url: client.url(bailianMcpPath(AIGC_MCP_SERVER.image)), + tool: AIGC_MCP_TOOL.imageEdit, + arguments: { ...args, image_url: redactDataUri(imageUrl) }, + mode: "mcp", + concurrent, + }); return; } - if (!settings.quiet) { - process.stderr.write("[Model: auto] [Mode: mcp] [Images: 1]\n"); - } + commandOutput.info({ + "en-US": "[Model: auto] [Mode: mcp] [Images: 1]", + "zh-CN": "[模型:auto] [模式:mcp] [图片:1]", + }); // One shared MCP session; --concurrent fans out parallel tools/call on it. await withAigcMcpSession(client, AIGC_MCP_SERVER.image, localize, async (callTool) => { - const perRequest = await runConcurrent(concurrent, settings, async () => { + const perRequest = await runConcurrent(concurrent, commandOutput, async () => { const inner = await callTool(AIGC_MCP_TOOL.imageEdit, args); return parseAigcImageResults(inner); }); @@ -424,20 +411,20 @@ async function handleAutoMcpMode( throw new BailianError("Edit completed but no images returned.", ExitCode.GENERAL); } - await saveImages(imageUrls, flags, settings, format); + await saveImages(imageUrls, flags, settings, commandOutput); }); } async function handleSyncMode( client: Client, + commandOutput: CommandOutput, settings: Settings, path: string, body: DashScopeImageRequest, flags: EditFlags, - format: OutputFormat, concurrent: number, ): Promise { - const results = await runConcurrent(concurrent, settings, () => + const results = await runConcurrent(concurrent, commandOutput, () => client.requestJson({ path, method: "POST", @@ -455,21 +442,21 @@ async function handleSyncMode( throw new BailianError("Edit completed but no images returned.", ExitCode.GENERAL); } - await saveImages(imageUrls, flags, settings, format); + await saveImages(imageUrls, flags, settings, commandOutput); } async function handleAsyncMode( client: Client, + commandOutput: CommandOutput, settings: Settings, path: string, body: DashScopeImageRequest, flags: EditFlags, - format: OutputFormat, concurrent: number, ): Promise { const responses = await runConcurrent( concurrent, - settings, + commandOutput, () => client.requestJson({ path, @@ -482,13 +469,14 @@ async function handleAsyncMode( const taskIds = responses.map((response) => response.output.task_id); if (flags.async) { - emitResult({ task_ids: taskIds }, format); + commandOutput.result({ task_ids: taskIds }); return; } const pollInterval = flags.pollInterval ?? 3; const pollPromises = taskIds.map((taskId) => - poll(client, settings, { + poll(client, { + output: commandOutput, url: client.url(taskPath(taskId)), intervalSec: pollInterval, timeoutSec: settings.timeout, @@ -525,14 +513,14 @@ async function handleAsyncMode( throw new BailianError("All tasks completed but no images returned.", ExitCode.GENERAL); } - await saveImages(imageUrls, flags, settings, format); + await saveImages(imageUrls, flags, settings, commandOutput); } async function saveImages( imageUrls: string[], flags: EditFlags, settings: Settings, - format: OutputFormat, + commandOutput: CommandOutput, ): Promise { const outDir = resolveOutputDir(settings, { flagDir: flags.outDir, @@ -550,11 +538,9 @@ async function saveImages( }) : [{ url: imageUrls[0], destPath: join(outDir, `${prefix}.png`) }]; - const saved = await downloadParallel(items, downloadFile, { quiet: settings.quiet }); + const saved = await downloadParallel(items, downloadFile, { + quiet: settings.quiet || settings.output === "json", + }); - if (settings.quiet) { - emitBare(saved.join("\n")); - } else { - emitResult({ urls: imageUrls, saved, total: imageUrls.length }, format); - } + commandOutput.result({ urls: imageUrls, saved, total: imageUrls.length }); } diff --git a/packages/commands/src/commands/image/generate.ts b/packages/commands/src/commands/image/generate.ts index 00eda1310..0997c072b 100644 --- a/packages/commands/src/commands/image/generate.ts +++ b/packages/commands/src/commands/image/generate.ts @@ -1,7 +1,7 @@ import { defineCommand, taskPath, - detectOutputFormat, + type CommandOutput, type Client, type Settings, type FlagsDef, @@ -12,7 +12,6 @@ import { BailianError, ExitCode, type DashScopeAsyncResponse, - type OutputFormat, type DashScopeTaskResponse, generateFilename, resolveBooleanFlag, @@ -29,7 +28,6 @@ import { import { poll } from "bailian-cli-runtime"; import { downloadFile } from "bailian-cli-runtime"; import { runConcurrent, downloadParallel, getConcurrency } from "bailian-cli-runtime"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { resolveImageSize } from "bailian-cli-runtime"; import { BOOL_FLAG_PROMPT_EXTEND_IMAGE_GENERATE, BOOL_FLAG_WATERMARK } from "bailian-cli-runtime"; import { @@ -194,13 +192,7 @@ export default defineCommand({ const resolvedModel = flags.model || settings.defaultImageModel; if (isAutoModel(resolvedModel)) { - await handleAutoMcpMode( - ctx.client, - ctx.localize, - settings, - flags, - detectOutputFormat(settings.output), - ); + await handleAutoMcpMode(ctx.client, ctx.output, ctx.localize, settings, flags); return; } @@ -249,24 +241,24 @@ export default defineCommand({ }, }; - const format = detectOutputFormat(settings.output); - if (settings.dryRun) { - emitResult( - { request: body, mode: route.useSync ? "sync" : "async", path: route.path }, - format, - ); + ctx.output.result({ + request: body, + mode: route.useSync ? "sync" : "async", + path: route.path, + }); return; } - if (!settings.quiet) { - process.stderr.write(`[Model: ${model}] [Mode: ${route.useSync ? "sync" : "async"}]\n`); - } + ctx.output.info({ + "en-US": `[Model: ${model}] [Mode: ${route.useSync ? "sync" : "async"}]`, + "zh-CN": `[模型:${model}] [模式:${route.useSync ? "同步" : "异步"}]`, + }); if (route.useSync) { - await handleSyncMode(ctx.client, settings, route.path, body, flags, format, concurrent); + await handleSyncMode(ctx.client, ctx.output, settings, route.path, body, flags, concurrent); } else { - await handleAsyncMode(ctx.client, settings, route.path, body, flags, format, concurrent); + await handleAsyncMode(ctx.client, ctx.output, settings, route.path, body, flags, concurrent); } }, }); @@ -275,10 +267,10 @@ export default defineCommand({ async function handleAutoMcpMode( client: Client, + commandOutput: CommandOutput, localize: Localize, settings: Settings, flags: GenerateFlags, - format: string, ): Promise { assertAutoMcpAvailable(client, localize); assertMcpCompatible("imageGen", flags, localize); @@ -293,27 +285,22 @@ async function handleAutoMcpMode( }); if (settings.dryRun) { - emitResult( - { - server: AIGC_MCP_SERVER.image, - url: client.url(bailianMcpPath(AIGC_MCP_SERVER.image)), - tool: AIGC_MCP_TOOL.imageGen, - arguments: args, - mode: "mcp", - concurrent, - }, - format as OutputFormat, - ); + commandOutput.result({ + server: AIGC_MCP_SERVER.image, + url: client.url(bailianMcpPath(AIGC_MCP_SERVER.image)), + tool: AIGC_MCP_TOOL.imageGen, + arguments: args, + mode: "mcp", + concurrent, + }); return; } - if (!settings.quiet) { - process.stderr.write("[Model: auto] [Mode: mcp]\n"); - } + commandOutput.info({ "en-US": "[Model: auto] [Mode: mcp]", "zh-CN": "[模型:auto] [模式:mcp]" }); // One shared MCP session; --concurrent fans out parallel tools/call on it. await withAigcMcpSession(client, AIGC_MCP_SERVER.image, localize, async (callTool) => { - const perRequest = await runConcurrent(concurrent, settings, async () => { + const perRequest = await runConcurrent(concurrent, commandOutput, async () => { const inner = await callTool(AIGC_MCP_TOOL.imageGen, args); return parseAigcImageResults(inner); }); @@ -323,7 +310,7 @@ async function handleAutoMcpMode( throw new BailianError("Generation completed but no images returned.", ExitCode.GENERAL); } - await saveImages(imageUrls, flags, settings, format); + await saveImages(imageUrls, flags, settings, commandOutput); }); } @@ -331,14 +318,14 @@ async function handleAutoMcpMode( async function handleSyncMode( client: Client, + commandOutput: CommandOutput, settings: Settings, path: string, body: DashScopeImageRequest, flags: GenerateFlags, - format: string, concurrent: number, ): Promise { - const results = await runConcurrent(concurrent, settings, () => + const results = await runConcurrent(concurrent, commandOutput, () => client.requestJson({ path, method: "POST", body }), ); @@ -352,23 +339,23 @@ async function handleSyncMode( throw new BailianError("Generation completed but no images returned.", ExitCode.GENERAL); } - await saveImages(imageUrls, flags, settings, format); + await saveImages(imageUrls, flags, settings, commandOutput); } // ---- Async mode: wan2.6-t2i / wan2.6-image / legacy text2image ---- async function handleAsyncMode( client: Client, + commandOutput: CommandOutput, settings: Settings, path: string, body: DashScopeImageRequest, flags: GenerateFlags, - format: string, concurrent: number, ): Promise { const responses = await runConcurrent( concurrent, - settings, + commandOutput, () => client.requestJson({ path, @@ -382,7 +369,7 @@ async function handleAsyncMode( // --async: return all task IDs immediately if (flags.async) { - emitResult({ task_ids: taskIds }, format as OutputFormat); + commandOutput.result({ task_ids: taskIds }); return; } @@ -391,7 +378,8 @@ async function handleAsyncMode( const pollPromises = taskIds.map((taskId) => { const pollUrl = client.url(taskPath(taskId)); - return poll(client, settings, { + return poll(client, { + output: commandOutput, url: pollUrl, intervalSec: pollInterval, timeoutSec: settings.timeout, @@ -432,7 +420,7 @@ async function handleAsyncMode( imageUrls, flags, settings, - format, + commandOutput, taskIds.length === 1 ? taskIds[0] : undefined, taskIds, ); @@ -444,7 +432,7 @@ async function saveImages( imageUrls: string[], flags: GenerateFlags, settings: Settings, - format: string, + commandOutput: CommandOutput, taskId?: string, taskIds?: string[], ): Promise { @@ -465,18 +453,16 @@ async function saveImages( }) : [{ url: imageUrls[0], destPath: join(outDir, `${prefix}.png`) }]; - const results = await downloadParallel(items, downloadFile, { quiet: settings.quiet }); + const results = await downloadParallel(items, downloadFile, { + quiet: settings.quiet || settings.output === "json", + }); - if (settings.quiet) { - emitBare(results.join("\n")); - } else { - const output: Record = { - urls: imageUrls, - saved: results, - total: imageUrls.length, - }; - if (taskId) output.task_id = taskId; - if (taskIds && taskIds.length > 1) output.task_ids = taskIds; - emitResult(output, format as OutputFormat); - } + const output: Record = { + urls: imageUrls, + saved: results, + total: imageUrls.length, + }; + if (taskId) output.task_id = taskId; + if (taskIds && taskIds.length > 1) output.task_ids = taskIds; + commandOutput.result(output); } diff --git a/packages/commands/src/commands/knowledge/billing.ts b/packages/commands/src/commands/knowledge/billing.ts index b7ef2412c..d5587a840 100644 --- a/packages/commands/src/commands/knowledge/billing.ts +++ b/packages/commands/src/commands/knowledge/billing.ts @@ -24,7 +24,7 @@ export const knowledgeCreationRisk: CommandRisk = { }, }; -type NoticeContext = Pick; +type NoticeContext = Pick; export function localizedKnowledgeBillingNotice(context: NoticeContext) { return { ...knowledgeBillingNotice, message: context.localize(knowledgeBillingNotice.message) }; @@ -32,11 +32,11 @@ export function localizedKnowledgeBillingNotice(context: NoticeContext) { export function writeKnowledgeBillingNotice(context: NoticeContext): void { const notice = localizedKnowledgeBillingNotice(context); - process.stderr.write( - context.settings.output === "json" - ? `${JSON.stringify({ warning: notice })}\n\n` - : `${notice.message}\n${notice.url}\n`, - ); + context.output.warning({ + code: notice.code, + message: `${notice.message}\n${notice.url}`, + details: { remainingAllowance: notice.remainingAllowance, url: notice.url }, + }); } /** Emit immediately after creation, before any fallible follow-up such as waiting for import. */ @@ -70,18 +70,18 @@ export function reportCreatedKnowledgeBase( "en-US": `Knowledge base ${indexId} was created and remains billable even if a later step fails. To stop its running-time charges, delete this knowledge base after confirming that its data is no longer needed.`, "zh-CN": `知识库 ${indexId} 已创建,后续步骤失败也不会停止其计费。如不再需要其中数据,请确认后删除该知识库以停止规格计费。`, }); - process.stderr.write( - context.settings.output === "json" - ? `${JSON.stringify({ warning: { code: "KNOWLEDGE_RESOURCE_CREATED", message, resource } })}\n\n` - : `${message}\n`, - ); + context.output.warning({ + code: "KNOWLEDGE_RESOURCE_CREATED", + message, + details: { resource }, + }); if (command && context.settings.output !== "json") { const instruction = [command.bin, ...command.path, ...command.args] .map((word) => /^[a-zA-Z0-9_./:@=-]+$/.test(word) ? word : `'${word.replaceAll("'", "'\\''")}'`, ) .join(" "); - process.stderr.write(`${instruction}\n`); + context.output.info(instruction); } return resource; } diff --git a/packages/commands/src/commands/knowledge/category-add.ts b/packages/commands/src/commands/knowledge/category-add.ts index 0bfb90941..e4d103b89 100644 --- a/packages/commands/src/commands/knowledge/category-add.ts +++ b/packages/commands/src/commands/knowledge/category-add.ts @@ -2,11 +2,9 @@ import { defineCommand, ragEndpoint, RAG_PATHS, - detectOutputFormat, type FlagsDef, type RagAddCategoryResponse, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts"; const CATEGORY_ADD_FLAGS = { @@ -54,7 +52,7 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const workspaceId = resolveWorkspaceId(ctx); - const format = detectOutputFormat(settings.output); + const format = settings.output; // categoryType fixed to UNSTRUCTURED (the only valid value for knowledge-base creation today) const body = { @@ -66,7 +64,7 @@ export default defineCommand({ const endpoint = ragEndpoint(workspaceId, RAG_PATHS.addCategory); if (settings.dryRun) { - emitResult({ endpoint, request: body }, format); + ctx.output.result({ endpoint, request: body }); return; } @@ -77,14 +75,10 @@ export default defineCommand({ }); const categoryId = response.data?.categoryId; - if (settings.quiet) { - emitBare(categoryId ?? ""); - return; - } - if (format === "text") { - emitBare(`created: ${categoryId ?? "-"} (${flags.name})`); - return; - } - emitResult(response, format); + + ctx.output.result( + response, + format === "text" ? { text: `created: ${categoryId ?? "-"} (${flags.name})` } : undefined, + ); }, }); diff --git a/packages/commands/src/commands/knowledge/category-delete.ts b/packages/commands/src/commands/knowledge/category-delete.ts index cc208d240..2972198d3 100644 --- a/packages/commands/src/commands/knowledge/category-delete.ts +++ b/packages/commands/src/commands/knowledge/category-delete.ts @@ -2,11 +2,9 @@ import { defineCommand, ragEndpoint, RAG_PATHS, - detectOutputFormat, type FlagsDef, type RagConnectorResponse, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts"; const CATEGORY_DELETE_FLAGS = { @@ -42,13 +40,13 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const workspaceId = resolveWorkspaceId(ctx); - const format = detectOutputFormat(settings.output); + const format = settings.output; const body = { categoryId: flags.categoryId }; const endpoint = ragEndpoint(workspaceId, RAG_PATHS.deleteCategory); if (settings.dryRun) { - emitResult({ endpoint, request: body }, format); + ctx.output.result({ endpoint, request: body }); return; } @@ -60,11 +58,9 @@ export default defineCommand({ body, }); - if (settings.quiet) return; - if (format === "text") { - emitBare(`deleted: ${flags.categoryId}`); - return; - } - emitResult(response, format); + ctx.output.result( + response, + format === "text" ? { text: `deleted: ${flags.categoryId}` } : undefined, + ); }, }); diff --git a/packages/commands/src/commands/knowledge/category-list.ts b/packages/commands/src/commands/knowledge/category-list.ts index 047bb6575..afe46830f 100644 --- a/packages/commands/src/commands/knowledge/category-list.ts +++ b/packages/commands/src/commands/knowledge/category-list.ts @@ -2,11 +2,9 @@ import { defineCommand, ragEndpoint, RAG_PATHS, - detectOutputFormat, type FlagsDef, type RagListCategoryResponse, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { resolveWorkspaceId, truncateLine, WORKSPACE_FLAG } from "./shared.ts"; const CATEGORY_LIST_FLAGS = { @@ -66,7 +64,7 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const workspaceId = resolveWorkspaceId(ctx); - const format = detectOutputFormat(settings.output); + const format = settings.output; // type fixed to UNSTRUCTURED, not exposed as a flag (the only valid value today); note: maxResult is singular const body = { @@ -80,7 +78,7 @@ export default defineCommand({ const endpoint = ragEndpoint(workspaceId, RAG_PATHS.listCategory); if (settings.dryRun) { - emitResult({ endpoint, request: body }, format); + ctx.output.result({ endpoint, request: body }); return; } @@ -91,23 +89,25 @@ export default defineCommand({ }); const categories = response.data?.categoryList ?? []; - if (settings.quiet) { - for (const category of categories) emitBare(category.categoryId ?? ""); - return; - } - if (format === "text") { + + ctx.output.result(response, format === "text" ? { text: renderText() } : undefined); + function renderText(): string { + const lines: string[] = []; + if (categories.length === 0) { - emitBare("No categories found."); + lines.push("No categories found."); } else { for (const category of categories) { const defaultMark = category.isDefault ? " [default]" : ""; - emitBare(truncateLine(`${category.categoryId} ${category.categoryName}${defaultMark}`)); + lines.push( + truncateLine(`${category.categoryId} ${category.categoryName}${defaultMark}`), + ); } } const nextToken = response.data?.nextToken; - if (nextToken) emitBare(`next: --next-token ${nextToken}`); - } else { - emitResult(response, format); + if (nextToken) lines.push(`next: --next-token ${nextToken}`); + + return lines.join("\n"); } }, }); diff --git a/packages/commands/src/commands/knowledge/chat.ts b/packages/commands/src/commands/knowledge/chat.ts index 06ba5dd45..87ee9ada0 100644 --- a/packages/commands/src/commands/knowledge/chat.ts +++ b/packages/commands/src/commands/knowledge/chat.ts @@ -5,7 +5,6 @@ import { defineCommand, knowledgeChatEndpoint, parseSSE, - detectOutputFormat, BailianError, ExitCode, type FlagsDef, @@ -14,7 +13,7 @@ import { type KnowledgeChatMessage, type KnowledgeChatRequest, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; + import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts"; const CHAT_FLAGS = { @@ -340,7 +339,7 @@ export default defineCommand({ const workspaceId = resolveWorkspaceId(ctx); - const format = detectOutputFormat(settings.output); + const format = settings.output; // API only supports SSE; streamOutput controls whether to print tokens in real-time const streamOutput = !settings.quiet && format === "text" && !!process.stdout.isTTY; @@ -378,7 +377,7 @@ export default defineCommand({ const url = knowledgeChatEndpoint(workspaceId); if (settings.dryRun) { - emitResult({ endpoint: url, request: body }, format); + ctx.output.result({ endpoint: url, request: body }); return; } @@ -392,7 +391,11 @@ export default defineCommand({ const accumulator = createChatAccumulator(ctx.localize); let displayedStage = ""; for await (const event of parseSSE(res)) { - if (settings.verbose) process.stderr.write(`[event] ${event.event ?? "message"}\n`); + ctx.output.debug({ + code: "KNOWLEDGE_CHAT_EVENT", + message: { "en-US": "Knowledge chat event", "zh-CN": "知识库对话事件" }, + details: { event: event.event ?? "message" }, + }); const fragments = accumulator.accept(event); if (streamOutput) { for (const fragment of fragments) { @@ -413,28 +416,37 @@ export default defineCommand({ if (event.data === "[DONE]") break; } const result = accumulator.finish(); - if (settings.quiet) emitBare(result.answer); - else if (format !== "text") emitResult(result, format); - else if (!streamOutput) emitBare(result.answer); - else { - process.stdout.write("\n"); - if (result.docs.length) emitBare(ctx.localize({ "en-US": "Sources:", "zh-CN": "来源:" })); - for (const doc of result.docs) { - if (doc === null || typeof doc !== "object" || Array.isArray(doc)) continue; - const record = doc as Record; - const metadata = - record.metadata && typeof record.metadata === "object" && !Array.isArray(record.metadata) - ? (record.metadata as Record) - : record; - emitBare( - JSON.stringify({ - doc_id: metadata.doc_id, - title: metadata.title, - citation: metadata._citation_index, - }), - ); - for (const line of mediaSummary(metadata)) emitBare(line); - } + let text: string | undefined; + if (format === "text") { + if (process.stdout.isTTY) { + const footer: string[] = []; + if (result.docs.length) + footer.push(ctx.localize({ "en-US": "Sources:", "zh-CN": "来源:" })); + for (const doc of result.docs) { + if (doc === null || typeof doc !== "object" || Array.isArray(doc)) continue; + const record = doc as Record; + const metadata = + record.metadata && + typeof record.metadata === "object" && + !Array.isArray(record.metadata) + ? (record.metadata as Record) + : record; + footer.push( + JSON.stringify({ + doc_id: metadata.doc_id, + title: metadata.title, + citation: metadata._citation_index, + }), + ); + footer.push(...mediaSummary(metadata)); + } + text = streamOutput + ? footer.length + ? "\n" + footer.join("\n") + : "" + : [result.answer, ...footer].join("\n"); + } else text = result.answer; } + ctx.output.result(result, text === undefined ? undefined : { text }); }, }); diff --git a/packages/commands/src/commands/knowledge/chunk-add.ts b/packages/commands/src/commands/knowledge/chunk-add.ts index 5c47a8a9e..a40209c93 100644 --- a/packages/commands/src/commands/knowledge/chunk-add.ts +++ b/packages/commands/src/commands/knowledge/chunk-add.ts @@ -3,13 +3,11 @@ import { defineCommand, ragEndpoint, RAG_PATHS, - detectOutputFormat, BailianError, ExitCode, type FlagsDef, type RagMutationResponse, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts"; import { readUtf8TextFile } from "./upload-support.ts"; @@ -180,7 +178,7 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const workspaceId = resolveWorkspaceId(ctx); - const format = detectOutputFormat(settings.output); + const format = settings.output; // dry-run also reads the file and parses --field (rehearsal semantics) let field: Record; @@ -209,7 +207,7 @@ export default defineCommand({ const endpoint = ragEndpoint(workspaceId, RAG_PATHS.chunkCreate); if (settings.dryRun) { - emitResult({ endpoint, request: body }, format); + ctx.output.result({ endpoint, request: body }); return; } @@ -219,13 +217,22 @@ export default defineCommand({ body, }); - // The response carries no chunk_id — quiet mode exits 0 silently on success - if (settings.quiet) return; - if (format === "text") { - emitBare(`chunk created (pipeline: ${flags.indexId})`); - emitBare("List chunks to find the new chunk id."); - return; - } - emitResult(response, format); + ctx.output.result( + response, + format === "text" + ? { + text: [ + ctx.localize({ + "en-US": `chunk created (pipeline: ${flags.indexId})`, + "zh-CN": `切片已创建 (pipeline: ${flags.indexId})`, + }), + ctx.localize({ + "en-US": "List chunks to find the new chunk id.", + "zh-CN": "请列出切片以获取新切片 ID。", + }), + ].join("\n"), + } + : undefined, + ); }, }); diff --git a/packages/commands/src/commands/knowledge/chunk-delete.ts b/packages/commands/src/commands/knowledge/chunk-delete.ts index cbdd0ed57..87d76bdab 100644 --- a/packages/commands/src/commands/knowledge/chunk-delete.ts +++ b/packages/commands/src/commands/knowledge/chunk-delete.ts @@ -3,12 +3,11 @@ import { defineCommand, ragEndpoint, RAG_PATHS, - detectOutputFormat, BailianError, type FlagsDef, type RagMutationResponse, } from "bailian-cli-core"; -import { emitResult, emitBare, normalizeError } from "bailian-cli-runtime"; +import { normalizeError } from "bailian-cli-runtime"; import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts"; const CHUNK_DELETE_FLAGS = { @@ -75,21 +74,18 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const workspaceId = resolveWorkspaceId(ctx); - const format = detectOutputFormat(settings.output); + const format = settings.output; const batches = splitIntoBatches(flags.chunkId); const endpoint = ragEndpoint(workspaceId, RAG_PATHS.chunkDelete); if (settings.dryRun) { - emitResult( - { - endpoint, - batches: batches.map((batchIds) => ({ - request: { pipelineId: flags.indexId, chunkIds: batchIds }, - })), - }, - format, - ); + ctx.output.result({ + endpoint, + batches: batches.map((batchIds) => ({ + request: { pipelineId: flags.indexId, chunkIds: batchIds }, + })), + }); return; } @@ -106,7 +102,7 @@ export default defineCommand({ deletedCount += batchIds.length; completedBatches.push(batchIds); } catch (error) { - const original = normalizeError(error, ctx.identity.binName); + const original = normalizeError(error, ctx.identity.binName, ctx.localize); const failedBatch = completedBatches.length + 1; const progress = [ ctx.localize({ @@ -133,11 +129,16 @@ export default defineCommand({ } } - if (settings.quiet) return; - if (format === "text") { - emitBare(`deleted: ${deletedCount} chunk(s) in ${batches.length} batch(es)`); - return; - } - emitResult({ deleted_count: deletedCount, batches: batches.length }, format); + ctx.output.result( + { deleted_count: deletedCount, batches: batches.length }, + format === "text" + ? { + text: ctx.localize({ + "en-US": `deleted: ${deletedCount} chunk(s) in ${batches.length} batch(es)`, + "zh-CN": `已删除: ${deletedCount} 个切片,共 ${batches.length} 个批次`, + }), + } + : undefined, + ); }, }); diff --git a/packages/commands/src/commands/knowledge/chunk-list.ts b/packages/commands/src/commands/knowledge/chunk-list.ts index 5d6595f75..26a56b782 100644 --- a/packages/commands/src/commands/knowledge/chunk-list.ts +++ b/packages/commands/src/commands/knowledge/chunk-list.ts @@ -4,11 +4,9 @@ import { defineCommand, ragEndpoint, RAG_PATHS, - detectOutputFormat, type FlagsDef, type RagChunkListResponse, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { resolveWorkspaceId, PAGE_FLAGS, WORKSPACE_FLAG } from "./shared.ts"; const CHUNK_LIST_FLAGS = { @@ -66,7 +64,7 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const workspaceId = resolveWorkspaceId(ctx); - const format = detectOutputFormat(settings.output); + const format = settings.output; // Gotcha: this endpoint's pagination keys are pageNum/pageSize (camelCase, in the body) const body = { @@ -78,7 +76,7 @@ export default defineCommand({ const endpoint = ragEndpoint(workspaceId, RAG_PATHS.chunkList); if (settings.dryRun) { - emitResult({ endpoint, request: body }, format); + ctx.output.result({ endpoint, request: body }); return; } @@ -88,38 +86,50 @@ export default defineCommand({ body, }); - const nodes = response.data?.nodes ?? []; - if (settings.quiet) { - // chunk ids only, for piping into chunk update/delete - for (const node of nodes) emitBare(node.metadata?._id ?? ""); - return; - } + let text: string | undefined; if (format === "text") { + const nodes = response.data?.nodes ?? []; + const lines: string[] = []; if (nodes.length === 0) { - emitBare("No chunks found."); + lines.push(ctx.localize({ "en-US": "No chunks found.", "zh-CN": "未找到切片。" })); } else { for (const node of nodes) { const metadata = node.metadata ?? {}; const statusPart = metadata._chunk_status_message - ? ` status: ${metadata._chunk_status_message}` + ? ctx.localize({ + "en-US": ` status: ${metadata._chunk_status_message}`, + "zh-CN": ` 状态: ${metadata._chunk_status_message}`, + }) : ""; const excludedPart = - metadata.is_displayed_chunk_content === false ? " [excluded from retrieval]" : ""; - emitBare( - `[chunk] ${metadata._id ?? "?"} (doc: ${metadata.doc_name ?? "?"}, doc_id: ${metadata.doc_id ?? "?"})${statusPart}${excludedPart}`, + metadata.is_displayed_chunk_content === false + ? ctx.localize({ "en-US": " [excluded from retrieval]", "zh-CN": " [不参与检索]" }) + : ""; + lines.push( + ctx.localize({ + "en-US": `[chunk] ${metadata._id ?? "?"} (doc: ${metadata.doc_name ?? "?"}, doc_id: ${metadata.doc_id ?? "?"})${statusPart}${excludedPart}`, + "zh-CN": `[切片] ${metadata._id ?? "?"} (文档: ${metadata.doc_name ?? "?"}, doc_id: ${metadata.doc_id ?? "?"})${statusPart}${excludedPart}`, + }), ); const contentText = firstNonEmptyText( metadata.content, node.text, metadata.clip_description, ); - emitBare(` ${contentText.length > 200 ? `${contentText.slice(0, 200)}…` : contentText}`); - for (const line of mediaSummary(metadata)) emitBare(` ${line}`); + lines.push( + ` ${contentText.length > 200 ? `${contentText.slice(0, 200)}…` : contentText}`, + ); + for (const line of mediaSummary(metadata)) lines.push(` ${line}`); } } - emitBare(`total: ${response.data?.total ?? nodes.length}`); - } else { - emitResult(response, format); + lines.push( + ctx.localize({ + "en-US": `total: ${response.data?.total ?? nodes.length}`, + "zh-CN": `总数: ${response.data?.total ?? nodes.length}`, + }), + ); + text = lines.join("\n"); } + ctx.output.result(response, text === undefined ? undefined : { text }); }, }); diff --git a/packages/commands/src/commands/knowledge/chunk-update.ts b/packages/commands/src/commands/knowledge/chunk-update.ts index 3fc53c6d3..15589f363 100644 --- a/packages/commands/src/commands/knowledge/chunk-update.ts +++ b/packages/commands/src/commands/knowledge/chunk-update.ts @@ -2,7 +2,6 @@ import { defineCommand, ragEndpoint, RAG_PATHS, - detectOutputFormat, BailianError, ExitCode, type Client, @@ -10,7 +9,6 @@ import { type RagChunkListResponse, type RagMutationResponse, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts"; import { readUtf8TextFile } from "./upload-support.ts"; @@ -162,7 +160,7 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const workspaceId = resolveWorkspaceId(ctx); - const format = detectOutputFormat(settings.output); + const format = settings.output; // dry-run also reads the file and validates (rehearsal semantics); the read-back // request is only made outside dry-run and when no new content is given @@ -200,7 +198,7 @@ export default defineCommand({ const endpoint = ragEndpoint(workspaceId, RAG_PATHS.chunkUpdate); if (settings.dryRun) { - emitResult({ endpoint, request: body }, format); + ctx.output.result({ endpoint, request: body }); return; } @@ -210,11 +208,16 @@ export default defineCommand({ body, }); - if (settings.quiet) return; - if (format === "text") { - emitBare(`updated: ${flags.chunkId}`); - return; - } - emitResult(response, format); + ctx.output.result( + response, + format === "text" + ? { + text: ctx.localize({ + "en-US": `updated: ${flags.chunkId}`, + "zh-CN": `已更新: ${flags.chunkId}`, + }), + } + : undefined, + ); }, }); diff --git a/packages/commands/src/commands/knowledge/collection-create.ts b/packages/commands/src/commands/knowledge/collection-create.ts index 159080c5c..158ae981b 100644 --- a/packages/commands/src/commands/knowledge/collection-create.ts +++ b/packages/commands/src/commands/knowledge/collection-create.ts @@ -2,11 +2,9 @@ import { defineCommand, ragEndpoint, RAG_PATHS, - detectOutputFormat, type FlagsDef, type RagAddConnectorResponse, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts"; const COLLECTION_CREATE_FLAGS = { @@ -102,7 +100,7 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const workspaceId = resolveWorkspaceId(ctx); - const format = detectOutputFormat(settings.output); + const format = settings.output; const storeType = (flags.storeType ?? "platform").toUpperCase(); // The server contract still uses connector* fields; only the CLI-facing term is collection. @@ -122,7 +120,7 @@ export default defineCommand({ const endpoint = ragEndpoint(workspaceId, RAG_PATHS.addConnector); if (settings.dryRun) { - emitResult({ endpoint, request: body }, format); + ctx.output.result({ endpoint, request: body }); return; } @@ -133,14 +131,12 @@ export default defineCommand({ }); const collectionId = response.data?.connectorId; - if (settings.quiet) { - emitBare(collectionId ?? ""); - return; - } - if (format === "text") { - emitBare(`created: ${collectionId ?? "-"} (${flags.name}, ${storeType})`); - return; - } - emitResult(response, format); + + ctx.output.result( + response, + format === "text" + ? { text: `created: ${collectionId ?? "-"} (${flags.name}, ${storeType})` } + : undefined, + ); }, }); diff --git a/packages/commands/src/commands/knowledge/collection-get.ts b/packages/commands/src/commands/knowledge/collection-get.ts index 928f2343e..9d80684c0 100644 --- a/packages/commands/src/commands/knowledge/collection-get.ts +++ b/packages/commands/src/commands/knowledge/collection-get.ts @@ -2,11 +2,9 @@ import { defineCommand, ragEndpoint, RAG_PATHS, - detectOutputFormat, type FlagsDef, type RagGetConnectorResponse, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts"; const COLLECTION_GET_FLAGS = { @@ -43,7 +41,7 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const workspaceId = resolveWorkspaceId(ctx); - const format = detectOutputFormat(settings.output); + const format = settings.output; // The server contract still uses connector* fields; only the CLI-facing term is collection const body = { @@ -53,7 +51,7 @@ export default defineCommand({ const endpoint = ragEndpoint(workspaceId, RAG_PATHS.getConnector); if (settings.dryRun) { - emitResult({ endpoint, request: body }, format); + ctx.output.result({ endpoint, request: body }); return; } @@ -64,18 +62,17 @@ export default defineCommand({ }); const collection = response.data; - if (settings.quiet) { - emitBare(collection?.connectorId ?? ""); - return; - } - if (format === "text") { - emitBare(`id: ${collection?.connectorId ?? "-"}`); - emitBare(`name: ${collection?.connectorName ?? "-"}`); - emitBare(`description: ${collection?.description ?? "-"}`); + + ctx.output.result(response, format === "text" ? { text: renderText() } : undefined); + function renderText(): string { + const lines: string[] = []; + + lines.push(`id: ${collection?.connectorId ?? "-"}`); + lines.push(`name: ${collection?.connectorName ?? "-"}`); + lines.push(`description: ${collection?.description ?? "-"}`); // getConnector does not return fileConnectorConfig (storeType/regionId/bucketName); // these fields are only available on the create request body. - return; + return lines.join("\n"); } - emitResult(response, format); }, }); diff --git a/packages/commands/src/commands/knowledge/doc-delete.ts b/packages/commands/src/commands/knowledge/doc-delete.ts index f8edf7072..fb49ba0a3 100644 --- a/packages/commands/src/commands/knowledge/doc-delete.ts +++ b/packages/commands/src/commands/knowledge/doc-delete.ts @@ -3,11 +3,9 @@ import { defineCommand, ragEndpoint, RAG_PATHS, - detectOutputFormat, type FlagsDef, type RagDeleteFileResponse, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts"; const DOC_DELETE_FLAGS = { @@ -52,9 +50,9 @@ export default defineCommand({ }, { "en-US": - "Use the doc_id from `knowledge doc list --quiet`, not the fileId from `knowledge doc upload`. For documents created via `knowledge create --doc-id`, the doc_id equals the fileId; for documents imported via `knowledge doc upload --index-id`, the doc_id may include a workspace suffix.", + "Use doc_id from the document-list JSON response (--output json), not fileId from document upload. For documents created via `knowledge create --doc-id`, the doc_id equals the fileId; for documents imported via `knowledge doc upload --index-id`, the doc_id may include a workspace suffix.", "zh-CN": - "请使用 `knowledge doc list --quiet` 返回的 doc_id,而不是 `knowledge doc upload` 返回的 fileId。通过 `knowledge create --doc-id` 创建的文档,其 doc_id 等于 fileId;通过 `knowledge doc upload --index-id` 导入的文档,其 doc_id 可能带有 Workspace 后缀。", + "请使用文档列表命令 JSON 响应(--output json)中的 doc_id,而不是文档上传返回的 fileId。通过 `knowledge create --doc-id` 创建的文档,其 doc_id 等于 fileId;通过 `knowledge doc upload --index-id` 导入的文档,其 doc_id 可能带有 Workspace 后缀。", }, { "en-US": @@ -81,14 +79,14 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const workspaceId = resolveWorkspaceId(ctx); - const format = detectOutputFormat(settings.output); + const format = settings.output; // snake_case: body { index_id, doc_ids } const body = { index_id: flags.indexId, doc_ids: flags.docId }; const endpoint = ragEndpoint(workspaceId, RAG_PATHS.indexDeleteFile); if (settings.dryRun) { - emitResult({ endpoint, request: body }, format); + ctx.output.result({ endpoint, request: body }); return; } @@ -98,22 +96,31 @@ export default defineCommand({ body, }); - // Output follows the server's data.deleted list + // Output follows the server's data.deleted list. const deleted = response.data?.deleted ?? []; - if (settings.quiet) { - for (const docId of deleted) emitBare(docId); - return; - } + let text: string | undefined; if (format === "text") { - emitBare(`deleted: ${deleted.length} document(s)`); - for (const docId of deleted) emitBare(` ${docId}`); - if (deleted.length !== flags.docId.length) { - process.stderr.write( - `Warning: requested ${flags.docId.length} deletion(s) but the server reported ${deleted.length}.\n`, - ); - } - return; + const lines = [ + ctx.localize({ + "en-US": `deleted: ${deleted.length} document(s)`, + "zh-CN": `已删除: ${deleted.length} 个文档`, + }), + ]; + for (const docId of deleted) lines.push(` ${docId}`); + text = lines.join("\n"); + } + ctx.output.result(response, text === undefined ? undefined : { text }); + if (deleted.length !== flags.docId.length) { + ctx.output.warning({ + code: "KNOWLEDGE_DOC_DELETE_PARTIAL", + message: { + "en-US": `requested ${flags.docId.length} deletion(s) but the server reported ${deleted.length}.`, + "zh-CN": `请求删除 ${flags.docId.length} 个文档,但服务端报告已删除 ${deleted.length} 个。`, + }, + ...(format === "json" + ? { details: { requested_count: flags.docId.length, deleted_count: deleted.length } } + : {}), + }); } - emitResult(response, format); }, }); diff --git a/packages/commands/src/commands/knowledge/doc-import-oss.ts b/packages/commands/src/commands/knowledge/doc-import-oss.ts index 55db9cd99..dd44d24e7 100644 --- a/packages/commands/src/commands/knowledge/doc-import-oss.ts +++ b/packages/commands/src/commands/knowledge/doc-import-oss.ts @@ -4,11 +4,9 @@ import { defineCommand, ragEndpoint, RAG_PATHS, - detectOutputFormat, type FlagsDef, type RagOssImportResponse, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts"; import { PARSER_FLAGS, readParserOptions } from "./parser-config.ts"; @@ -112,7 +110,7 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const workspaceId = resolveWorkspaceId(ctx); - const format = detectOutputFormat(settings.output); + const format = settings.output; const parserOptions = readParserOptions(flags, ctx.localize); const body = { @@ -131,7 +129,7 @@ export default defineCommand({ const endpoint = ragEndpoint(workspaceId, RAG_PATHS.addFilesFromAuthorizedOss); if (settings.dryRun) { - emitResult({ endpoint, request: body }, format); + ctx.output.result({ endpoint, request: body }); return; } @@ -141,23 +139,23 @@ export default defineCommand({ body, }); - // Live-verified shape: results come back as addFileResultList (the docs' flat - // fileIds field is not returned); per-file status is SUCCESS on success - const results = response.data?.addFileResultList ?? []; - const fileIds = results - .map((result) => result.fileId) - .filter((fileId): fileId is string => !!fileId); - if (settings.quiet) { - for (const fileId of fileIds) emitBare(fileId); - return; - } + let text: string | undefined; if (format === "text") { - emitBare(`imported: ${fileIds.length} file(s)`); - for (const result of results) { - emitBare(` ${result.fileId ?? "-"} ${result.status ?? "-"} ${result.ossKey ?? ""}`); - } - return; + // The API returns per-file results, including failed or missing-ID records. + const results = response.data?.addFileResultList ?? []; + const fileIds = results + .map((result) => result.fileId) + .filter((fileId): fileId is string => !!fileId); + const lines = [ + ctx.localize({ + "en-US": `imported: ${fileIds.length} file(s)`, + "zh-CN": `已导入: ${fileIds.length} 个文件`, + }), + ]; + for (const result of results) + lines.push(` ${result.fileId ?? "-"} ${result.status ?? "-"} ${result.ossKey ?? ""}`); + text = lines.join("\n"); } - emitResult(response, format); + ctx.output.result(response, text === undefined ? undefined : { text }); }, }); diff --git a/packages/commands/src/commands/knowledge/doc-import.ts b/packages/commands/src/commands/knowledge/doc-import.ts index 5810f76b5..6542f10b8 100644 --- a/packages/commands/src/commands/knowledge/doc-import.ts +++ b/packages/commands/src/commands/knowledge/doc-import.ts @@ -2,13 +2,11 @@ import { defineCommand, ragEndpoint, RAG_PATHS, - detectOutputFormat, BailianError, ExitCode, type FlagsDef, type RagJobCreateResponse, } from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; import { WORKSPACE_FLAG, resolveWorkspaceId, @@ -170,9 +168,9 @@ export default defineCommand({ ? { sourceType: "DATA_CENTER_FILE", docIds: flags.docId } : { sourceType: "DATA_CENTER_CATEGORY", categoryIds: flags.categoryId }), }; - const format = detectOutputFormat(settings.output); + const format = settings.output; if (settings.dryRun) { - emitResult({ endpoint, request }, format); + ctx.output.result({ endpoint, request }); return; } const response = await ctx.client.requestJson({ @@ -193,6 +191,7 @@ export default defineCommand({ if (flags.wait) { try { status = await pollImportJob(ctx.client, settings, { + output: ctx.output, statusUrl: importJobStatusUrl(workspaceId, flags.indexId, ingestionId).toString(), intervalSec: flags.pollInterval ?? 5, }); @@ -214,15 +213,26 @@ export default defineCommand({ ); } } - if (settings.quiet) { - emitBare(ingestionId); - return; - } - if (format === "text") { - emitBare(`ingestionId: ${ingestionId}`); - if (status) emitBare(`status: ${status.data?.ingestion_status ?? "UNKNOWN"}`); - return; - } - emitResult({ ...response, ...(status ? { status } : {}) }, format); + ctx.output.result( + { ...response, ...(status ? { status } : {}) }, + format === "text" + ? { + text: [ + ctx.localize({ + "en-US": `ingestionId: ${ingestionId}`, + "zh-CN": `导入任务: ${ingestionId}`, + }), + ...(status + ? [ + ctx.localize({ + "en-US": `status: ${status.data?.ingestion_status ?? "UNKNOWN"}`, + "zh-CN": `状态: ${status.data?.ingestion_status ?? "UNKNOWN"}`, + }), + ] + : []), + ].join("\n"), + } + : undefined, + ); }, }); diff --git a/packages/commands/src/commands/knowledge/doc-list.ts b/packages/commands/src/commands/knowledge/doc-list.ts index 84e3d5e95..73a3a7099 100644 --- a/packages/commands/src/commands/knowledge/doc-list.ts +++ b/packages/commands/src/commands/knowledge/doc-list.ts @@ -2,12 +2,11 @@ import { defineCommand, ragEndpoint, RAG_PATHS, - detectOutputFormat, type FlagsDef, type RagIndexFilesResponse, type RagIndexFileDetailsResponse, } from "bailian-cli-core"; -import { emitResult, emitBare, ansi } from "bailian-cli-runtime"; +import { ansi } from "bailian-cli-runtime"; import { resolveWorkspaceId, truncateLine, PAGE_FLAGS, WORKSPACE_FLAG } from "./shared.ts"; const DOC_LIST_FLAGS = { @@ -73,7 +72,7 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const workspaceId = resolveWorkspaceId(ctx); - const format = detectOutputFormat(settings.output); + const format = settings.output; // Gotcha: this endpoint's page parameter is page_num (not page_number) const url = new URL(ragEndpoint(workspaceId, RAG_PATHS.indexFiles)); @@ -92,7 +91,7 @@ export default defineCommand({ : null; if (settings.dryRun) { - emitResult({ endpoint, request }, format); + ctx.output.result({ endpoint, request }); return; } @@ -104,21 +103,19 @@ export default defineCommand({ ...(request ? { body: request } : {}), }); - const rows = response.data?.rows ?? []; - if (settings.quiet) { - for (const row of rows) emitBare(row.doc_id ?? ""); - return; - } + let text: string | undefined; if (format === "text") { + const rows = response.data?.rows ?? []; const styles = ansi(process.stdout); + const lines: string[] = []; if (rows.length === 0) { - emitBare("No documents found."); + lines.push(ctx.localize({ "en-US": "No documents found.", "zh-CN": "未找到文档。" })); } else { for (const row of rows) { const line = truncateLine( [row.doc_id, row.status, row.doc_name, row.doc_type ?? "-", row.size ?? "-"].join(" "), ); - emitBare(row.status === "FAILED" ? styles.red(line) : line); + lines.push(row.status === "FAILED" ? styles.red(line) : line); if (flags.details) { for (const key of [ "chunkSize", @@ -127,16 +124,21 @@ export default defineCommand({ "chunkMode", "enableHeaders", ]) { - emitBare( + lines.push( ` ${key}: ${row[key] === undefined || row[key] === null ? "-" : JSON.stringify(row[key])}`, ); } } } } - emitBare(`total: ${response.data?.total_count ?? rows.length}`); - } else { - emitResult(response, format); + lines.push( + ctx.localize({ + "en-US": `total: ${response.data?.total_count ?? rows.length}`, + "zh-CN": `总数: ${response.data?.total_count ?? rows.length}`, + }), + ); + text = lines.join("\n"); } + ctx.output.result(response, text === undefined ? undefined : { text }); }, }); diff --git a/packages/commands/src/commands/knowledge/doc-status.ts b/packages/commands/src/commands/knowledge/doc-status.ts index 02c6286f7..1eeaea8e1 100644 --- a/packages/commands/src/commands/knowledge/doc-status.ts +++ b/packages/commands/src/commands/knowledge/doc-status.ts @@ -2,13 +2,13 @@ import { defineCommand, ragEndpoint, RAG_PATHS, - detectOutputFormat, BailianError, ExitCode, type FlagsDef, type RagIndexJobStatusResponse, + type LocalizedText, } from "bailian-cli-core"; -import { emitResult, emitBare, ansi } from "bailian-cli-runtime"; +import { ansi } from "bailian-cli-runtime"; import { resolveWorkspaceId, PAGE_FLAGS, @@ -53,14 +53,23 @@ const DOC_STATUS_FLAGS = { ...WORKSPACE_FLAG, } satisfies FlagsDef; -function printStatus(response: RagIndexJobStatusResponse): void { +function statusText( + response: RagIndexJobStatusResponse, + localize: (text: LocalizedText) => string, +): string { const styles = ansi(process.stdout); - emitBare(`status: ${response.data?.ingestion_status ?? "UNKNOWN"}`); + const lines = [ + localize({ + "en-US": `status: ${response.data?.ingestion_status ?? "UNKNOWN"}`, + "zh-CN": `状态: ${response.data?.ingestion_status ?? "UNKNOWN"}`, + }), + ]; for (const doc of response.data?.rows ?? []) { const docState = doc.code ?? doc.status ?? "?"; const line = ` ${doc.doc_id ?? "?"} ${docState} ${doc.doc_name ?? ""}`; - emitBare(docState.includes("FAILED") ? styles.red(line) : line); + lines.push(docState.includes("FAILED") ? styles.red(line) : line); } + return lines.join("\n"); } export default defineCommand({ @@ -95,7 +104,7 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const workspaceId = resolveWorkspaceId(ctx); - const format = detectOutputFormat(settings.output); + const format = settings.output; // Both required flags are enforced by the parser up front; parameters go in // the query string (they are ignored in the body) @@ -111,7 +120,7 @@ export default defineCommand({ const endpoint = url.toString(); if (settings.dryRun) { - emitResult({ endpoint, request: null }, format); + ctx.output.result({ endpoint, request: null }); return; } @@ -120,6 +129,7 @@ export default defineCommand({ // Reuse the shared polling (timeout → TIMEOUT(5)); failure detection happens // uniformly after return, based on per-document status response = await pollImportJob(ctx.client, settings, { + output: ctx.output, statusUrl: endpoint, intervalSec: flags.pollInterval ?? 5, }); @@ -138,14 +148,9 @@ export default defineCommand({ ); } - if (settings.quiet) { - emitBare(response.data?.ingestion_status ?? "UNKNOWN"); - return; - } - if (format === "text") { - printStatus(response); - } else { - emitResult(response, format); - } + ctx.output.result( + response, + format === "text" ? { text: statusText(response, ctx.localize) } : undefined, + ); }, }); diff --git a/packages/commands/src/commands/knowledge/doc-sync.ts b/packages/commands/src/commands/knowledge/doc-sync.ts index b62d2ac1f..da669f286 100644 --- a/packages/commands/src/commands/knowledge/doc-sync.ts +++ b/packages/commands/src/commands/knowledge/doc-sync.ts @@ -1,11 +1,4 @@ -import { - BailianError, - ExitCode, - defineCommand, - detectOutputFormat, - type FlagsDef, -} from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; +import { BailianError, ExitCode, defineCommand, type FlagsDef } from "bailian-cli-core"; import { WORKSPACE_FLAG, resolveWorkspaceId } from "./shared.ts"; import { prepareSync, type SyncPrepared } from "./sync/prepare.ts"; import { executeSyncPlan } from "./sync/execute.ts"; @@ -157,6 +150,7 @@ export default defineCommand({ ExitCode.GENERAL, ); const ports = createSyncExecutionPorts({ + output: context.output, client: context.client, target: prepared.target, settings: context.settings, @@ -186,9 +180,14 @@ export default defineCommand({ const checkpoint = await readStateFile(prepared.stateFile, context.localize).catch( () => undefined, ); - process.stderr.write( - `${JSON.stringify({ code: "KNOWLEDGE_SYNC_INCOMPLETE", stateFile: prepared.stateFile, checkpoint })}\n`, - ); + context.output.warning({ + code: "KNOWLEDGE_SYNC_INCOMPLETE", + message: { + "en-US": "Synchronization did not complete; use the checkpoint for recovery.", + "zh-CN": "同步未完成;请使用恢复记录继续处理。", + }, + details: { stateFile: prepared.stateFile, checkpoint }, + }); throw error; } const result = { @@ -205,21 +204,25 @@ export default defineCommand({ requiresReplan: prepared.plan.requiresReplan, result: completed, }; - const format = detectOutputFormat(context.settings.output); - if (format === "json") emitResult(result, format); - else { - emitBare( - context.localize( - prepared.plan.requiresReplan - ? { - "en-US": "Recovery completed. Preview remaining changes again before continuing.", - "zh-CN": "恢复完成。请重新预览剩余变更后继续。", - } - : { "en-US": "Synchronization completed.", "zh-CN": "同步完成。" }, - ), - ); - emitBare(JSON.stringify(result.summary)); - emitBare(`stateFile: ${prepared.stateFile}`); - } + context.output.result( + result, + context.settings.output === "text" + ? { + text: [ + context.localize( + prepared.plan.requiresReplan + ? { + "en-US": + "Recovery completed. Preview remaining changes again before continuing.", + "zh-CN": "恢复完成。请重新预览剩余变更后继续。", + } + : { "en-US": "Synchronization completed.", "zh-CN": "同步完成。" }, + ), + JSON.stringify(result.summary), + `stateFile: ${prepared.stateFile}`, + ].join("\n"), + } + : undefined, + ); }, }); diff --git a/packages/commands/src/commands/knowledge/doc-tag.ts b/packages/commands/src/commands/knowledge/doc-tag.ts index 87f4fc569..128a004a0 100644 --- a/packages/commands/src/commands/knowledge/doc-tag.ts +++ b/packages/commands/src/commands/knowledge/doc-tag.ts @@ -3,11 +3,9 @@ import { defineCommand, ragEndpoint, RAG_PATHS, - detectOutputFormat, type FlagsDef, type RagBatchUpdateTagResponse, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts"; const DOC_TAG_FLAGS = { @@ -84,7 +82,7 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const workspaceId = resolveWorkspaceId(ctx); - const format = detectOutputFormat(settings.output); + const format = settings.output; const body = { fileInfos: flags.docId.map((fileId) => ({ fileId, tags: flags.tag })), @@ -93,7 +91,7 @@ export default defineCommand({ const endpoint = ragEndpoint(workspaceId, RAG_PATHS.batchUpdateFileTag); if (settings.dryRun) { - emitResult({ endpoint, request: body }, format); + ctx.output.result({ endpoint, request: body }); return; } @@ -103,11 +101,16 @@ export default defineCommand({ body, }); - if (settings.quiet) return; - if (format === "text") { - emitBare(`tagged: ${flags.docId.length} file(s) with [${flags.tag.join(", ")}]`); - return; - } - emitResult(response, format); + ctx.output.result( + response, + format === "text" + ? { + text: ctx.localize({ + "en-US": `tagged: ${flags.docId.length} file(s) with [${flags.tag.join(", ")}]`, + "zh-CN": `已为 ${flags.docId.length} 个文件设置标签 [${flags.tag.join(", ")}]`, + }), + } + : undefined, + ); }, }); diff --git a/packages/commands/src/commands/knowledge/doc-upload.ts b/packages/commands/src/commands/knowledge/doc-upload.ts index e85514b58..162247e3d 100644 --- a/packages/commands/src/commands/knowledge/doc-upload.ts +++ b/packages/commands/src/commands/knowledge/doc-upload.ts @@ -5,12 +5,10 @@ import { defineCommand, ragEndpoint, RAG_PATHS, - detectOutputFormat, BailianError, ExitCode, type FlagsDef, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { resolveWorkspaceId, WORKSPACE_FLAG, @@ -158,7 +156,7 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const workspaceId = resolveWorkspaceId(ctx); - const format = detectOutputFormat(settings.output); + const format = settings.output; const parserOptions = { parser: "AUTO_SELECT", ...readParserOptions(flags, ctx.localize) }; // Expand directories into individual file paths; unsupported extensions are @@ -186,7 +184,14 @@ export default defineCommand({ // file problems early); exceeding a soft limit only warns const checkedFiles = expandedFiles.map((filePath) => { const checked = checkUploadFile(filePath, ctx.localize); - if (checked.warning) process.stderr.write(`Warning: ${checked.warning}\n`); + if (checked.warning) + ctx.output.warning({ + code: "KNOWLEDGE_UPLOAD_SIZE_WARNING", + message: checked.warning, + ...(format === "json" + ? { details: { file: filePath, size_bytes: checked.sizeBytes } } + : {}), + }); return { filePath, sizeBytes: checked.sizeBytes }; }); @@ -235,7 +240,7 @@ export default defineCommand({ } as unknown, }); } - emitResult({ steps, skipped }, format); + ctx.output.result({ steps, skipped }); return; } @@ -289,6 +294,7 @@ export default defineCommand({ ingestionId = job.data?.ingestionId; if (flags.wait && ingestionId) { const statusResponse = await pollImportJob(ctx.client, settings, { + output: ctx.output, statusUrl: importJobStatusUrl(workspaceId, flags.indexId, ingestionId).toString(), intervalSec: flags.pollInterval ?? 5, }); @@ -310,40 +316,51 @@ export default defineCommand({ } } - if (settings.quiet) { - for (const item of uploaded) emitBare(item.fileId); - return; - } + // This orchestration has a stable aggregate result rather than one raw response. + const result = { + files: uploaded.map((item) => ({ path: item.path, fileId: item.fileId })), + skipped, + ...(flags.indexId ? { index_id: flags.indexId } : {}), + ...(ingestionId ? { ingestion_id: ingestionId } : {}), + ...(finalStatus ? { final_status: finalStatus } : {}), + }; + let text: string | undefined; if (format === "text") { - for (const item of uploaded) { - emitBare(`${basename(item.path)} ${item.fileId} registered`); - } - if (ingestionId) emitBare(`job: ${ingestionId}`); - if (finalStatus) emitBare(`status: ${finalStatus}`); - // Summary line: always show counts; list skipped files only with --verbose - const summaryParts = [`Uploaded ${uploaded.length} file${uploaded.length !== 1 ? "s" : ""}`]; - if (skipped.length > 0) { - summaryParts.push(`skipped ${skipped.length} unsupported`); - } - emitBare(`\n${summaryParts.join(", ")}.`); + const lines = uploaded.map((item) => + ctx.localize({ + "en-US": `${basename(item.path)} ${item.fileId} registered`, + "zh-CN": `${basename(item.path)} ${item.fileId} 已登记`, + }), + ); + if (ingestionId) + lines.push( + ctx.localize({ "en-US": `job: ${ingestionId}`, "zh-CN": `任务: ${ingestionId}` }), + ); + if (finalStatus) + lines.push( + ctx.localize({ "en-US": `status: ${finalStatus}`, "zh-CN": `状态: ${finalStatus}` }), + ); + const summaryParts = [ + ctx.localize({ + "en-US": `Uploaded ${uploaded.length} file${uploaded.length !== 1 ? "s" : ""}`, + "zh-CN": `已上传 ${uploaded.length} 个文件`, + }), + ]; + if (skipped.length > 0) + summaryParts.push( + ctx.localize({ + "en-US": `skipped ${skipped.length} unsupported`, + "zh-CN": `跳过 ${skipped.length} 个不支持的文件`, + }), + ); + lines.push(`\n${summaryParts.join(", ")}.`); + // Preserve the established verbose text projection; JSON always retains all skipped paths. if (settings.verbose && skipped.length > 0) { - emitBare("Skipped files:"); - for (const skippedPath of skipped) { - emitBare(` ${basename(skippedPath)}`); - } + lines.push(ctx.localize({ "en-US": "Skipped files:", "zh-CN": "已跳过文件:" })); + for (const skippedPath of skipped) lines.push(` ${basename(skippedPath)}`); } - return; + text = lines.join("\n"); } - // An orchestration command has no single response to pass through — emit a custom stable shape - emitResult( - { - files: uploaded.map((item) => ({ path: item.path, fileId: item.fileId })), - skipped, - ...(flags.indexId ? { index_id: flags.indexId } : {}), - ...(ingestionId ? { ingestion_id: ingestionId } : {}), - ...(finalStatus ? { final_status: finalStatus } : {}), - }, - format, - ); + ctx.output.result(result, text === undefined ? undefined : { text }); }, }); diff --git a/packages/commands/src/commands/knowledge/file-delete.ts b/packages/commands/src/commands/knowledge/file-delete.ts index 7242d3879..ac410daae 100644 --- a/packages/commands/src/commands/knowledge/file-delete.ts +++ b/packages/commands/src/commands/knowledge/file-delete.ts @@ -2,11 +2,9 @@ import { defineCommand, ragEndpoint, RAG_PATHS, - detectOutputFormat, type FlagsDef, type RagConnectorResponse, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts"; const FILE_DELETE_FLAGS = { @@ -54,13 +52,13 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const workspaceId = resolveWorkspaceId(ctx); - const format = detectOutputFormat(settings.output); + const format = settings.output; const body = { fileId: flags.fileId }; const endpoint = ragEndpoint(workspaceId, RAG_PATHS.deleteFile); if (settings.dryRun) { - emitResult({ endpoint, request: body }, format); + ctx.output.result({ endpoint, request: body }); return; } @@ -72,11 +70,9 @@ export default defineCommand({ body, }); - if (settings.quiet) return; - if (format === "text") { - emitBare(`deleted: ${flags.fileId}`); - return; - } - emitResult(response, format); + ctx.output.result( + response, + format === "text" ? { text: `deleted: ${flags.fileId}` } : undefined, + ); }, }); diff --git a/packages/commands/src/commands/knowledge/file-get.ts b/packages/commands/src/commands/knowledge/file-get.ts index 81bd750ff..129b397b0 100644 --- a/packages/commands/src/commands/knowledge/file-get.ts +++ b/packages/commands/src/commands/knowledge/file-get.ts @@ -2,11 +2,9 @@ import { defineCommand, ragEndpoint, RAG_PATHS, - detectOutputFormat, type FlagsDef, type RagDescribeFileResponse, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts"; const FILE_GET_FLAGS = { @@ -31,13 +29,13 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const workspaceId = resolveWorkspaceId(ctx); - const format = detectOutputFormat(settings.output); + const format = settings.output; const body = { fileId: flags.fileId }; const endpoint = ragEndpoint(workspaceId, RAG_PATHS.describeFile); if (settings.dryRun) { - emitResult({ endpoint, request: body }, format); + ctx.output.result({ endpoint, request: body }); return; } @@ -48,23 +46,23 @@ export default defineCommand({ }); const file = response.data; - if (settings.quiet) { - emitBare(file?.fileId ?? ""); - return; - } - if (format !== "text") { - emitResult(response, format); - return; + + ctx.output.result(response, format === "text" ? { text: renderText() } : undefined); + function renderText(): string { + const lines: string[] = []; + + lines.push(`id: ${file?.fileId ?? "-"}`); + lines.push(`name: ${file?.fileName ?? "-"}`); + lines.push(`type: ${file?.fileType ?? "-"}`); + lines.push(`size: ${file?.sizeBytes ?? "-"}`); + lines.push(`status: ${file?.status ?? "-"}`); + lines.push(`parser: ${file?.parser ?? "-"}`); + lines.push(`category: ${file?.category ?? "-"}`); + lines.push(`uploaded: ${file?.uploadTime ?? "-"}`); + const tags = Array.isArray(file?.tags) ? file.tags.join(", ") : (file?.tags ?? "-"); + lines.push(`tags: ${tags || "-"}`); + + return lines.join("\n"); } - emitBare(`id: ${file?.fileId ?? "-"}`); - emitBare(`name: ${file?.fileName ?? "-"}`); - emitBare(`type: ${file?.fileType ?? "-"}`); - emitBare(`size: ${file?.sizeBytes ?? "-"}`); - emitBare(`status: ${file?.status ?? "-"}`); - emitBare(`parser: ${file?.parser ?? "-"}`); - emitBare(`category: ${file?.category ?? "-"}`); - emitBare(`uploaded: ${file?.uploadTime ?? "-"}`); - const tags = Array.isArray(file?.tags) ? file.tags.join(", ") : (file?.tags ?? "-"); - emitBare(`tags: ${tags || "-"}`); }, }); diff --git a/packages/commands/src/commands/knowledge/file-list.ts b/packages/commands/src/commands/knowledge/file-list.ts index 9f9c764d4..c23e5760b 100644 --- a/packages/commands/src/commands/knowledge/file-list.ts +++ b/packages/commands/src/commands/knowledge/file-list.ts @@ -2,11 +2,9 @@ import { defineCommand, ragEndpoint, RAG_PATHS, - detectOutputFormat, type FlagsDef, type RagListFileResponse, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { resolveWorkspaceId, truncateLine, WORKSPACE_FLAG } from "./shared.ts"; const FILE_LIST_FLAGS = { @@ -85,7 +83,7 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const workspaceId = resolveWorkspaceId(ctx); - const format = detectOutputFormat(settings.output); + const format = settings.output; const body = { categoryId: flags.categoryId, @@ -97,7 +95,7 @@ export default defineCommand({ const endpoint = ragEndpoint(workspaceId, RAG_PATHS.listFile); if (settings.dryRun) { - emitResult({ endpoint, request: body }, format); + ctx.output.result({ endpoint, request: body }); return; } @@ -108,16 +106,16 @@ export default defineCommand({ }); const files = response.data?.fileList ?? []; - if (settings.quiet) { - for (const file of files) emitBare(file.fileId ?? ""); - return; - } - if (format === "text") { + + ctx.output.result(response, format === "text" ? { text: renderText() } : undefined); + function renderText(): string { + const lines: string[] = []; + if (files.length === 0) { - emitBare("No files found."); + lines.push("No files found."); } else { for (const file of files) { - emitBare( + lines.push( truncateLine( [file.fileId, file.status ?? "-", file.fileName, file.sizeBytes ?? "-"].join(" "), ), @@ -125,9 +123,9 @@ export default defineCommand({ } } const nextToken = response.data?.nextToken; - if (nextToken) emitBare(`next: --next-token ${nextToken}`); - } else { - emitResult(response, format); + if (nextToken) lines.push(`next: --next-token ${nextToken}`); + + return lines.join("\n"); } }, }); diff --git a/packages/commands/src/commands/knowledge/init-workflow.ts b/packages/commands/src/commands/knowledge/init-workflow.ts index 26d7d0a2e..5b625e7ae 100644 --- a/packages/commands/src/commands/knowledge/init-workflow.ts +++ b/packages/commands/src/commands/knowledge/init-workflow.ts @@ -1,7 +1,7 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { BailianError, ExitCode, type Settings } from "bailian-cli-core"; +import { BailianError, ExitCode, type CommandOutput, type Settings } from "bailian-cli-core"; import type { InitOptions, InitPrepared } from "./init-prepare.ts"; import type { InitState } from "./init-state.ts"; import { INIT_SAMPLE, matchesInitSample } from "./init-sample.ts"; @@ -33,6 +33,7 @@ export interface InitRunOptions extends InitOptions { prepared: InitPrepared; settings: Settings; pollInterval: number; + output: CommandOutput; report: (resource: InitResource) => void; } @@ -141,6 +142,7 @@ export async function runKnowledgeInit(options: InitRunOptions) { // reuse, verify current retrieval rather than depending on old job retention. if (state.ingestionId && !state.agentId) { const response = await pollImportJob(client, settings, { + output: options.output, statusUrl: importJobStatusUrl(workspaceId, state.indexId, state.ingestionId).toString(), intervalSec: options.pollInterval, }); diff --git a/packages/commands/src/commands/knowledge/init.ts b/packages/commands/src/commands/knowledge/init.ts index 00d3881c1..50a040bef 100644 --- a/packages/commands/src/commands/knowledge/init.ts +++ b/packages/commands/src/commands/knowledge/init.ts @@ -3,11 +3,9 @@ import { BailianError, ExitCode, defineCommand, - detectOutputFormat, type CommandContext, type FlagsDef, } from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; import { knowledgeCreationRisk, localizedKnowledgeBillingNotice } from "./billing.ts"; import { prepareKnowledgeInit, type InitPrepared } from "./init-prepare.ts"; import { runKnowledgeInit, type InitResource } from "./init-workflow.ts"; @@ -149,8 +147,9 @@ export default defineCommand({ ExitCode.GENERAL, ); const workspaceId = resolveWorkspaceId(context); - const format = detectOutputFormat(context.settings.output); + const format = context.settings.output; const result = await runKnowledgeInit({ + output: context.output, client: context.client, workspaceId, name: context.flags.name ?? "cli-demo", @@ -171,15 +170,17 @@ export default defineCommand({ "en-US": `${resource.created ? "Created" : "Reused"} ${resource.kind}: ${resource.id}`, "zh-CN": `${resource.created ? "已创建" : "已复用"} ${resource.kind}:${resource.id}`, }); - if (format === "json") - process.stderr.write( - `${JSON.stringify({ code: "KNOWLEDGE_INIT_RESOURCE", message, resource, cleanup })}\n`, - ); - else { - process.stderr.write(`${message}\n`); - for (const action of cleanup) - process.stderr.write(`${renderAction(context.identity.binName, action)}\n`); - } + context.output.warning({ + code: "KNOWLEDGE_INIT_RESOURCE", + message: + format === "text" + ? [ + message, + ...cleanup.map((action) => renderAction(context.identity.binName, action)), + ].join("\n") + : message, + ...(format === "json" ? { details: { resource, cleanup } } : {}), + }); }, }); const cleanup = [...result.resources] @@ -206,49 +207,54 @@ export default defineCommand({ } : undefined; const notices = [localizedKnowledgeBillingNotice(context)]; - if (format === "json") { - emitResult({ ...result, cleanup, nextSearch, notices }, format); - return; - } - emitBare( - context.localize({ - "en-US": "Initialization succeeded: the sample was retrieved.", - "zh-CN": "初始化成功:已检索到样例内容。", - }), - ); - emitBare( - `indexId: ${result.indexId}\nagentId: ${result.agentId}\nstateFile: ${result.stateFile}`, - ); - const sampleNode = result.sampleSearch.response.data.nodes.find((node) => - matchesInitSample({ data: { nodes: [node] } }), - ); - const content = sampleNode?.metadata?.content || sampleNode?.text || ""; - const excerpt = content.length > 400 ? `${content.slice(0, 400)}…` : content; - emitBare(context.localize({ "en-US": "Sample search", "zh-CN": "样例检索" })); - emitBare( - context.localize({ - "en-US": `Query: ${result.sampleSearch.query}`, - "zh-CN": `查询:${result.sampleSearch.query}`, - }), - ); - if (sampleNode?.metadata?.doc_name) - emitBare( + let text: string | undefined; + if (format === "text") { + const lines: string[] = []; + lines.push( context.localize({ - "en-US": `Document: ${sampleNode.metadata.doc_name}`, - "zh-CN": `文档:${sampleNode.metadata.doc_name}`, + "en-US": "Initialization succeeded: the sample was retrieved.", + "zh-CN": "初始化成功:已检索到样例内容。", }), ); - emitBare(context.localize({ "en-US": `Content: ${excerpt}`, "zh-CN": `内容:${excerpt}` })); - if (nextSearch) emitBare(renderAction(context.identity.binName, nextSearch)); - emitBare(notices[0]!.message); - if (cleanup.length) - emitBare( + lines.push( + `indexId: ${result.indexId}\nagentId: ${result.agentId}\nstateFile: ${result.stateFile}`, + ); + const sampleNode = result.sampleSearch.response.data.nodes.find((node) => + matchesInitSample({ data: { nodes: [node] } }), + ); + const content = sampleNode?.metadata?.content || sampleNode?.text || ""; + const excerpt = content.length > 400 ? `${content.slice(0, 400)}…` : content; + lines.push(context.localize({ "en-US": "Sample search", "zh-CN": "样例检索" })); + lines.push( context.localize({ - "en-US": - "Cleanup commands for resources created in this run (confirm deletion separately):", - "zh-CN": "本次新建资源的清理命令(删除需另行确认):", + "en-US": `Query: ${result.sampleSearch.query}`, + "zh-CN": `查询:${result.sampleSearch.query}`, }), ); - for (const action of cleanup) emitBare(renderAction(context.identity.binName, action)); + if (sampleNode?.metadata?.doc_name) + lines.push( + context.localize({ + "en-US": `Document: ${sampleNode.metadata.doc_name}`, + "zh-CN": `文档:${sampleNode.metadata.doc_name}`, + }), + ); + lines.push(context.localize({ "en-US": `Content: ${excerpt}`, "zh-CN": `内容:${excerpt}` })); + if (nextSearch) lines.push(renderAction(context.identity.binName, nextSearch)); + lines.push(notices[0]!.message); + if (cleanup.length) + lines.push( + context.localize({ + "en-US": + "Cleanup commands for resources created in this run (confirm deletion separately):", + "zh-CN": "本次新建资源的清理命令(删除需另行确认):", + }), + ); + for (const action of cleanup) lines.push(renderAction(context.identity.binName, action)); + text = lines.join("\n"); + } + context.output.result( + { ...result, cleanup, nextSearch, notices }, + text === undefined ? undefined : { text }, + ); }, }); diff --git a/packages/commands/src/commands/knowledge/kb-create.ts b/packages/commands/src/commands/knowledge/kb-create.ts index e2a03b642..6ef8ff3ac 100644 --- a/packages/commands/src/commands/knowledge/kb-create.ts +++ b/packages/commands/src/commands/knowledge/kb-create.ts @@ -3,12 +3,10 @@ import { defineCommand, ragEndpoint, RAG_PATHS, - detectOutputFormat, BailianError, ExitCode, type FlagsDef, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { knowledgeBillingNotice, localizedKnowledgeBillingNotice, @@ -240,7 +238,7 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const workspaceId = resolveWorkspaceId(ctx); - const format = detectOutputFormat(settings.output); + const format = settings.output; // Fixed values, not exposed as flags in this version: structureType unstructured, sinkType BUILT_IN. // Note: the public docs' example uses sinkType DEFAULT, but BUILT_IN is what works against the live API. @@ -280,10 +278,11 @@ export default defineCommand({ const endpoint = ragEndpoint(workspaceId, RAG_PATHS.indexCreateV2); if (settings.dryRun) { - emitResult( - { endpoint, request: body, notices: [localizedKnowledgeBillingNotice(ctx)] }, - format, - ); + ctx.output.result({ + endpoint, + request: body, + notices: [localizedKnowledgeBillingNotice(ctx)], + }); return; } @@ -296,6 +295,7 @@ export default defineCommand({ let finalStatus: string | undefined; if (flags.wait && pipelineId && ingestionId) { const statusResponse = await pollImportJob(ctx.client, settings, { + output: ctx.output, statusUrl: importJobStatusUrl(workspaceId, pipelineId, ingestionId).toString(), intervalSec: flags.pollInterval ?? 5, }); @@ -312,25 +312,23 @@ export default defineCommand({ } } - if (settings.quiet) { - emitBare(pipelineId ?? ""); - return; - } - if (format === "text") { - emitBare(`index_id: ${pipelineId ?? "-"}`); - if (ingestionId) emitBare(`ingestion_id: ${ingestionId}`); - if (finalStatus) emitBare(`status: ${finalStatus}`); - emitBare("Next: check the import job status, then search against this knowledge base."); - return; - } - emitResult( + ctx.output.result( { ...response, ...(finalStatus ? { final_status: finalStatus } : {}), resources, notices: [localizedKnowledgeBillingNotice(ctx)], }, - format, + format === "text" ? { text: renderText() } : undefined, ); + function renderText(): string { + const lines: string[] = []; + + lines.push(`index_id: ${pipelineId ?? "-"}`); + if (ingestionId) lines.push(`ingestion_id: ${ingestionId}`); + if (finalStatus) lines.push(`status: ${finalStatus}`); + lines.push("Next: check the import job status, then search against this knowledge base."); + return lines.join("\n"); + } }, }); diff --git a/packages/commands/src/commands/knowledge/kb-delete.ts b/packages/commands/src/commands/knowledge/kb-delete.ts index 03bc90375..ea1f460a5 100644 --- a/packages/commands/src/commands/knowledge/kb-delete.ts +++ b/packages/commands/src/commands/knowledge/kb-delete.ts @@ -2,11 +2,9 @@ import { defineCommand, ragEndpoint, RAG_PATHS, - detectOutputFormat, type FlagsDef, type RagMutationResponse, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts"; const KB_DELETE_FLAGS = { @@ -50,14 +48,14 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const workspaceId = resolveWorkspaceId(ctx); - const format = detectOutputFormat(settings.output); + const format = settings.output; // This endpoint is back to snake_case: body { index_id } const body = { index_id: flags.indexId }; const endpoint = ragEndpoint(workspaceId, RAG_PATHS.indexDelete); if (settings.dryRun) { - emitResult({ endpoint, request: body }, format); + ctx.output.result({ endpoint, request: body }); return; } @@ -67,11 +65,9 @@ export default defineCommand({ body, }); - if (settings.quiet) return; - if (format === "text") { - emitBare(`deleted: ${flags.indexId}`); - return; - } - emitResult(response, format); + ctx.output.result( + response, + format === "text" ? { text: `deleted: ${flags.indexId}` } : undefined, + ); }, }); diff --git a/packages/commands/src/commands/knowledge/kb-info.ts b/packages/commands/src/commands/knowledge/kb-info.ts index 6f1cf502d..684fa308f 100644 --- a/packages/commands/src/commands/knowledge/kb-info.ts +++ b/packages/commands/src/commands/knowledge/kb-info.ts @@ -2,7 +2,6 @@ import { defineCommand, ragEndpoint, RAG_PATHS, - detectOutputFormat, BailianError, ExitCode, type Client, @@ -10,7 +9,6 @@ import { type RagIndexListResponse, type RagIndexRow, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts"; const KB_INFO_FLAGS = { @@ -79,57 +77,52 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const workspaceId = resolveWorkspaceId(ctx); - const format = detectOutputFormat(settings.output); + const format = settings.output; if (settings.dryRun) { - emitResult( - { - endpoint: indexDetailUrl(workspaceId, flags.indexId), - request: null, - }, - format, - ); + ctx.output.result({ + endpoint: indexDetailUrl(workspaceId, flags.indexId), + request: null, + }); return; } const row = await fetchIndexDetail(ctx.client, workspaceId, flags.indexId); - if (settings.quiet) { - emitBare(row.id ?? ""); - return; - } - if (format !== "text") { - emitResult(row, format); - return; - } + ctx.output.result(row, format === "text" ? { text: renderText() } : undefined); + function renderText(): string { + const lines: string[] = []; + + // Grouped by diagnostic concern; the immutable annotation on Indexing tells + // users which settings require recreating the knowledge base + lines.push("Basic:"); + lines.push(formatField("id", row.id)); + lines.push(formatField("name", row.name)); + lines.push(formatField("description", row.description)); + lines.push(formatField("dataType", row.dataType)); + lines.push(formatField("knowledgeType", row.knowledgeType)); + lines.push(formatField("knowledgeScene", row.knowledgeScene)); + lines.push("Indexing: [immutable — recreate required to change]"); + lines.push(formatField("embeddingModelName", row.embeddingModelName)); + lines.push(formatField("multimodalEmbeddingModelName", row.multimodalEmbeddingModelName)); + lines.push(formatField("embeddingDimension", row.embeddingDimension)); + lines.push(formatField("chunkSize", row.chunkSize)); + lines.push(formatField("overlapSize", row.overlapSize)); + lines.push(formatField("chunkMode", row.chunkMode)); + lines.push(formatField("separator", row.separator)); + lines.push("Retrieval:"); + lines.push(formatField("rerankModelName", row.rerankModelName)); + lines.push(formatField("rerankMinScore", row.rerankMinScore)); + lines.push(formatField("rerankTopN", row.rerankTopN)); + lines.push(formatField("rerankMode", row.rerankMode)); + lines.push(formatField("enableRewrite", row.enableRewrite)); + lines.push(formatField("denseSimilarityTopK", row.denseSimilarityTopK)); + lines.push(formatField("sparseSimilarityTopK", row.sparseSimilarityTopK)); + lines.push("Data:"); + lines.push(formatField("sourceType", row.sourceType)); + lines.push(formatField("connectorId", row.connectorId)); - // Grouped by diagnostic concern; the immutable annotation on Indexing tells - // users which settings require recreating the knowledge base - emitBare("Basic:"); - emitBare(formatField("id", row.id)); - emitBare(formatField("name", row.name)); - emitBare(formatField("description", row.description)); - emitBare(formatField("dataType", row.dataType)); - emitBare(formatField("knowledgeType", row.knowledgeType)); - emitBare(formatField("knowledgeScene", row.knowledgeScene)); - emitBare("Indexing: [immutable — recreate required to change]"); - emitBare(formatField("embeddingModelName", row.embeddingModelName)); - emitBare(formatField("multimodalEmbeddingModelName", row.multimodalEmbeddingModelName)); - emitBare(formatField("embeddingDimension", row.embeddingDimension)); - emitBare(formatField("chunkSize", row.chunkSize)); - emitBare(formatField("overlapSize", row.overlapSize)); - emitBare(formatField("chunkMode", row.chunkMode)); - emitBare(formatField("separator", row.separator)); - emitBare("Retrieval:"); - emitBare(formatField("rerankModelName", row.rerankModelName)); - emitBare(formatField("rerankMinScore", row.rerankMinScore)); - emitBare(formatField("rerankTopN", row.rerankTopN)); - emitBare(formatField("rerankMode", row.rerankMode)); - emitBare(formatField("enableRewrite", row.enableRewrite)); - emitBare(formatField("denseSimilarityTopK", row.denseSimilarityTopK)); - emitBare(formatField("sparseSimilarityTopK", row.sparseSimilarityTopK)); - emitBare("Data:"); - emitBare(formatField("sourceType", row.sourceType)); - emitBare(formatField("connectorId", row.connectorId)); + return lines.join("\n"); + } }, }); diff --git a/packages/commands/src/commands/knowledge/kb-list.ts b/packages/commands/src/commands/knowledge/kb-list.ts index da3a39c01..c9a1baeac 100644 --- a/packages/commands/src/commands/knowledge/kb-list.ts +++ b/packages/commands/src/commands/knowledge/kb-list.ts @@ -2,11 +2,9 @@ import { defineCommand, ragEndpoint, RAG_PATHS, - detectOutputFormat, type FlagsDef, type RagIndexListResponse, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { resolveWorkspaceId, truncateLine, PAGE_FLAGS, WORKSPACE_FLAG } from "./shared.ts"; const KB_LIST_FLAGS = { @@ -58,7 +56,7 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const workspaceId = resolveWorkspaceId(ctx); - const format = detectOutputFormat(settings.output); + const format = settings.output; // Pagination and filter parameters must go in the query string — the server ignores them in the body const url = new URL(ragEndpoint(workspaceId, RAG_PATHS.indexList)); @@ -68,7 +66,7 @@ export default defineCommand({ const endpoint = url.toString(); if (settings.dryRun) { - emitResult({ endpoint, request: null }, format); + ctx.output.result({ endpoint, request: null }); return; } @@ -78,16 +76,16 @@ export default defineCommand({ }); const rows = response.data?.rows ?? []; - if (settings.quiet) { - for (const row of rows) emitBare(row.id); - return; - } - if (format === "text") { + + ctx.output.result(response, format === "text" ? { text: renderText() } : undefined); + function renderText(): string { + const lines: string[] = []; + if (rows.length === 0) { - emitBare("No knowledge bases found."); + lines.push("No knowledge bases found."); } else { for (const row of rows) { - emitBare( + lines.push( truncateLine( [ row.id, @@ -101,9 +99,9 @@ export default defineCommand({ ); } } - emitBare(`total: ${response.data?.total_count ?? rows.length}`); - } else { - emitResult(response, format); + lines.push(`total: ${response.data?.total_count ?? rows.length}`); + + return lines.join("\n"); } }, }); diff --git a/packages/commands/src/commands/knowledge/kb-stats.ts b/packages/commands/src/commands/knowledge/kb-stats.ts index ff9f7e344..6bb5b9ef8 100644 --- a/packages/commands/src/commands/knowledge/kb-stats.ts +++ b/packages/commands/src/commands/knowledge/kb-stats.ts @@ -2,13 +2,11 @@ import { defineCommand, ragEndpoint, RAG_PATHS, - detectOutputFormat, BailianError, ExitCode, type FlagsDef, type RagMonitorResponse, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts"; const KB_STATS_FLAGS = { @@ -88,7 +86,7 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const workspaceId = resolveWorkspaceId(ctx); - const format = detectOutputFormat(settings.output); + const format = settings.output; const nowSeconds = Math.floor(Date.now() / 1000); const startTimestamp = flags.start @@ -114,7 +112,7 @@ export default defineCommand({ const endpoint = ragEndpoint(workspaceId, RAG_PATHS.indexMonitor); if (settings.dryRun) { - emitResult({ endpoint, request: body }, format); + ctx.output.result({ endpoint, request: body }); return; } @@ -124,21 +122,26 @@ export default defineCommand({ body, }); - if (format !== "text") { - emitResult(response, format); - return; - } - if (clampedEnd) { - emitBare("note: end time was in the future, clamped to now."); + if (clampedEnd) + ctx.output.info({ + "en-US": "note: end time was in the future, clamped to now.", + "zh-CN": "提示:结束时间晚于当前时间,已截断为当前时间。", + }); + ctx.output.result(response, format === "text" ? { text: renderText() } : undefined); + function renderText(): string { + const lines: string[] = []; + + // Shape verified against the live API: the monitor fields are objects, not arrays + const storage = response.data?.storageMonitorData; + const qps = response.data?.qpsMonitorData; + lines.push(`plan: ${response.data?.pipelineCommercialType ?? "-"}`); + lines.push( + `storage: ${storage?.indexStorageUsage ?? "-"} / ${storage?.indexStorageLimit ?? "-"}`, + ); + lines.push(`peak qps: ${qps?.peakQps ?? "-"}`); + lines.push(`qps windows: ${qps?.monitorData?.length ?? 0} data point(s)`); + + return lines.join("\n"); } - // Shape verified against the live API: the monitor fields are objects, not arrays - const storage = response.data?.storageMonitorData; - const qps = response.data?.qpsMonitorData; - emitBare(`plan: ${response.data?.pipelineCommercialType ?? "-"}`); - emitBare( - `storage: ${storage?.indexStorageUsage ?? "-"} / ${storage?.indexStorageLimit ?? "-"}`, - ); - emitBare(`peak qps: ${qps?.peakQps ?? "-"}`); - emitBare(`qps windows: ${qps?.monitorData?.length ?? 0} data point(s)`); }, }); diff --git a/packages/commands/src/commands/knowledge/kb-update.ts b/packages/commands/src/commands/knowledge/kb-update.ts index b498ea0fc..f2115a9ec 100644 --- a/packages/commands/src/commands/knowledge/kb-update.ts +++ b/packages/commands/src/commands/knowledge/kb-update.ts @@ -2,11 +2,9 @@ import { defineCommand, ragEndpoint, RAG_PATHS, - detectOutputFormat, type FlagsDef, type RagMutationResponse, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts"; const KB_UPDATE_FLAGS = { @@ -84,7 +82,7 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const workspaceId = resolveWorkspaceId(ctx); - const format = detectOutputFormat(settings.output); + const format = settings.output; // Gotcha: this endpoint names the knowledge base ID parameter `id` (not index_id/indexId) const body = { @@ -96,7 +94,7 @@ export default defineCommand({ const endpoint = ragEndpoint(workspaceId, RAG_PATHS.indexUpdate); if (settings.dryRun) { - emitResult({ endpoint, request: body }, format); + ctx.output.result({ endpoint, request: body }); return; } @@ -106,11 +104,9 @@ export default defineCommand({ body, }); - if (settings.quiet) return; - if (format === "text") { - emitBare(`updated: ${flags.indexId}`); - return; - } - emitResult(response, format); + ctx.output.result( + response, + format === "text" ? { text: `updated: ${flags.indexId}` } : undefined, + ); }, }); diff --git a/packages/commands/src/commands/knowledge/search.ts b/packages/commands/src/commands/knowledge/search.ts index f4e1566f8..3c35665e6 100644 --- a/packages/commands/src/commands/knowledge/search.ts +++ b/packages/commands/src/commands/knowledge/search.ts @@ -6,11 +6,9 @@ import { ExitCode, defineCommand, knowledgeSearchEndpoint, - detectOutputFormat, type FlagsDef, type KnowledgeSearchRequest, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts"; const SEARCH_FLAGS = { @@ -120,7 +118,7 @@ export default defineCommand({ const workspaceId = resolveWorkspaceId(ctx); - const format = detectOutputFormat(settings.output); + const format = settings.output; const body: KnowledgeSearchRequest = { query: flags.query ?? "", @@ -181,28 +179,34 @@ export default defineCommand({ const url = knowledgeSearchEndpoint(workspaceId); if (settings.dryRun) { - emitResult({ endpoint: url, request: body }, format); + ctx.output.result({ endpoint: url, request: body }); return; } const response = await searchKnowledge(ctx.client, workspaceId, body); - const nodes = response.data?.nodes || []; - if (settings.quiet || format === "text") { + let text: string | undefined; + if (format === "text") { + const nodes = response.data?.nodes || []; + const lines: string[] = []; if (nodes.length === 0) { - emitBare("No results found."); + lines.push(ctx.localize({ "en-US": "No results found.", "zh-CN": "未找到结果。" })); } else { for (const [nodeIndex, node] of nodes.entries()) { - emitBare(`[${nodeIndex + 1}] (score: ${node.score.toFixed(4)})`); - emitBare( + lines.push( + ctx.localize({ + "en-US": `[${nodeIndex + 1}] (score: ${node.score.toFixed(4)})`, + "zh-CN": `[${nodeIndex + 1}] (得分: ${node.score.toFixed(4)})`, + }), + ); + lines.push( firstNonEmptyText(node.text, node.metadata?.content, node.metadata?.clip_description), ); - for (const line of mediaSummary(node.metadata ?? {})) emitBare(line); - emitBare(""); + lines.push(...mediaSummary(node.metadata ?? {}), ""); } } - } else { - emitResult(response, format); + text = lines.join("\n"); } + ctx.output.result(response, text === undefined ? undefined : { text }); }, }); diff --git a/packages/commands/src/commands/knowledge/service-copy.ts b/packages/commands/src/commands/knowledge/service-copy.ts index e4b994d4c..869dc59a6 100644 --- a/packages/commands/src/commands/knowledge/service-copy.ts +++ b/packages/commands/src/commands/knowledge/service-copy.ts @@ -2,11 +2,9 @@ import { defineCommand, ragEndpoint, RAG_PATHS, - detectOutputFormat, type FlagsDef, type RagAgentMutationResponse, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { agentMutationField, resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts"; const SERVICE_COPY_FLAGS = { @@ -45,13 +43,13 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const workspaceId = resolveWorkspaceId(ctx); - const format = detectOutputFormat(settings.output); + const format = settings.output; const body = { agent_id: flags.agentId }; const endpoint = ragEndpoint(workspaceId, RAG_PATHS.agentCopy); if (settings.dryRun) { - emitResult({ endpoint, request: body }, format); + ctx.output.result({ endpoint, request: body }); return; } @@ -61,20 +59,23 @@ export default defineCommand({ body, }); - const newAgentId = agentMutationField(response, "agent_id"); - if (settings.quiet) { - emitBare(newAgentId ?? ""); - return; - } - if (format === "text") { - emitBare( - `new agent_id: ${newAgentId ?? "-"} (name: ${agentMutationField(response, "agent_name") ?? "-"}, status: ${agentMutationField(response, "agent_status") ?? "draft"})`, - ); - emitBare( - "Test the draft with --agent-version beta on search/chat, then deploy it to publish.", - ); - return; - } - emitResult(response, format); + ctx.output.result( + response, + format === "text" + ? { + text: [ + ctx.localize({ + "en-US": `new agent_id: ${agentMutationField(response, "agent_id") ?? "-"} (name: ${agentMutationField(response, "agent_name") ?? "-"}, status: ${agentMutationField(response, "agent_status") ?? "draft"})`, + "zh-CN": `新 agent_id: ${agentMutationField(response, "agent_id") ?? "-"} (名称: ${agentMutationField(response, "agent_name") ?? "-"}, 状态: ${agentMutationField(response, "agent_status") ?? "draft"})`, + }), + ctx.localize({ + "en-US": + "Test the draft with --agent-version beta on search/chat, then deploy it to publish.", + "zh-CN": "在 search/chat 中使用 --agent-version beta 测试草稿,然后部署发布。", + }), + ].join("\n"), + } + : undefined, + ); }, }); diff --git a/packages/commands/src/commands/knowledge/service-create.ts b/packages/commands/src/commands/knowledge/service-create.ts index 7756f8a1b..f116a7741 100644 --- a/packages/commands/src/commands/knowledge/service-create.ts +++ b/packages/commands/src/commands/knowledge/service-create.ts @@ -1,14 +1,7 @@ import { validateNonemptyFlags } from "./nonempty-flags.ts"; import { createKnowledgeService } from "./operations/service.ts"; import { parseConfigFile } from "./service-update.ts"; -import { - defineCommand, - ragEndpoint, - RAG_PATHS, - detectOutputFormat, - type FlagsDef, -} from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; +import { defineCommand, ragEndpoint, RAG_PATHS, type FlagsDef } from "bailian-cli-core"; import { agentMutationField, resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts"; const SERVICE_CREATE_FLAGS = { @@ -117,7 +110,7 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const workspaceId = resolveWorkspaceId(ctx); - const format = detectOutputFormat(settings.output); + const format = settings.output; // agent_config is optional — omit to use server defaults; with --index-id build // the minimal kb_search_configs (name/desc etc. are backfilled by the system from @@ -127,7 +120,7 @@ export default defineCommand({ agent_scene: flags.scene, ...(flags.description ? { agent_desc: flags.description } : {}), ...(flags.configFile !== undefined - ? { agent_config: parseConfigFile(flags.configFile) } + ? { agent_config: parseConfigFile(flags.configFile, ctx.output) } : flags.indexId ? { agent_config: { kb_search_configs: [{ id: flags.indexId }] } } : {}), @@ -135,26 +128,29 @@ export default defineCommand({ const endpoint = ragEndpoint(workspaceId, RAG_PATHS.agentCreate); if (settings.dryRun) { - emitResult({ endpoint, request: body }, format); + ctx.output.result({ endpoint, request: body }); return; } const response = await createKnowledgeService(ctx.client, workspaceId, body); - const agentId = agentMutationField(response, "agent_id"); - if (settings.quiet) { - emitBare(agentId ?? ""); - return; - } - if (format === "text") { - emitBare( - `created: ${agentId ?? "-"} (status: ${agentMutationField(response, "agent_status") ?? "draft"}, version: ${agentMutationField(response, "agent_version") ?? "beta"})`, - ); - emitBare( - "Test the draft with --agent-version beta on search/chat, then deploy it to publish.", - ); - return; - } - emitResult(response, format); + ctx.output.result( + response, + format === "text" + ? { + text: [ + ctx.localize({ + "en-US": `created: ${agentMutationField(response, "agent_id") ?? "-"} (status: ${agentMutationField(response, "agent_status") ?? "draft"}, version: ${agentMutationField(response, "agent_version") ?? "beta"})`, + "zh-CN": `已创建: ${agentMutationField(response, "agent_id") ?? "-"} (状态: ${agentMutationField(response, "agent_status") ?? "draft"}, 版本: ${agentMutationField(response, "agent_version") ?? "beta"})`, + }), + ctx.localize({ + "en-US": + "Test the draft with --agent-version beta on search/chat, then deploy it to publish.", + "zh-CN": "在 search/chat 中使用 --agent-version beta 测试草稿,然后部署发布。", + }), + ].join("\n"), + } + : undefined, + ); }, }); diff --git a/packages/commands/src/commands/knowledge/service-delete.ts b/packages/commands/src/commands/knowledge/service-delete.ts index 027e2d617..47102f018 100644 --- a/packages/commands/src/commands/knowledge/service-delete.ts +++ b/packages/commands/src/commands/knowledge/service-delete.ts @@ -2,11 +2,9 @@ import { defineCommand, ragEndpoint, RAG_PATHS, - detectOutputFormat, type FlagsDef, type RagAgentMutationResponse, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { agentMutationField, resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts"; const SERVICE_DELETE_FLAGS = { @@ -54,13 +52,13 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const workspaceId = resolveWorkspaceId(ctx); - const format = detectOutputFormat(settings.output); + const format = settings.output; const body = { agent_id: flags.agentId }; const endpoint = ragEndpoint(workspaceId, RAG_PATHS.agentDelete); if (settings.dryRun) { - emitResult({ endpoint, request: body }, format); + ctx.output.result({ endpoint, request: body }); return; } @@ -70,13 +68,16 @@ export default defineCommand({ body, }); - if (settings.quiet) return; - if (format === "text") { - emitBare( - `deleted: ${flags.agentId} (status: ${agentMutationField(response, "agent_status") ?? "deleted"})`, - ); - return; - } - emitResult(response, format); + ctx.output.result( + response, + format === "text" + ? { + text: ctx.localize({ + "en-US": `deleted: ${flags.agentId} (status: ${agentMutationField(response, "agent_status") ?? "deleted"})`, + "zh-CN": `已删除: ${flags.agentId} (状态: ${agentMutationField(response, "agent_status") ?? "deleted"})`, + }), + } + : undefined, + ); }, }); diff --git a/packages/commands/src/commands/knowledge/service-deploy.ts b/packages/commands/src/commands/knowledge/service-deploy.ts index 38ced5736..25969e22e 100644 --- a/packages/commands/src/commands/knowledge/service-deploy.ts +++ b/packages/commands/src/commands/knowledge/service-deploy.ts @@ -2,11 +2,9 @@ import { defineCommand, ragEndpoint, RAG_PATHS, - detectOutputFormat, type FlagsDef, type RagAgentMutationResponse, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { agentMutationField, resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts"; const SERVICE_DEPLOY_FLAGS = { @@ -68,7 +66,7 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const workspaceId = resolveWorkspaceId(ctx); - const format = detectOutputFormat(settings.output); + const format = settings.output; const body = { agent_id: flags.agentId, @@ -77,7 +75,7 @@ export default defineCommand({ const endpoint = ragEndpoint(workspaceId, RAG_PATHS.agentDeploy); if (settings.dryRun) { - emitResult({ endpoint, request: body }, format); + ctx.output.result({ endpoint, request: body }); return; } @@ -87,15 +85,16 @@ export default defineCommand({ body, }); - const newVersion = agentMutationField(response, "agent_version"); - if (settings.quiet) { - emitBare(newVersion ?? ""); - return; - } - if (format === "text") { - emitBare(`deployed: ${flags.agentId} version ${newVersion ?? "-"}`); - return; - } - emitResult(response, format); + ctx.output.result( + response, + format === "text" + ? { + text: ctx.localize({ + "en-US": `deployed: ${flags.agentId} version ${agentMutationField(response, "agent_version") ?? "-"}`, + "zh-CN": `已部署: ${flags.agentId} 版本 ${agentMutationField(response, "agent_version") ?? "-"}`, + }), + } + : undefined, + ); }, }); diff --git a/packages/commands/src/commands/knowledge/service-get.ts b/packages/commands/src/commands/knowledge/service-get.ts index f0de4a0d9..78ee79eb7 100644 --- a/packages/commands/src/commands/knowledge/service-get.ts +++ b/packages/commands/src/commands/knowledge/service-get.ts @@ -3,12 +3,11 @@ import { defineCommand, ragEndpoint, RAG_PATHS, - detectOutputFormat, type FlagsDef, type RagAgentDetail, + type LocalizedText, type RagAgentGetResponse, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts"; const SERVICE_GET_FLAGS = { @@ -29,21 +28,58 @@ const SERVICE_GET_FLAGS = { ...WORKSPACE_FLAG, } satisfies FlagsDef; -function printDetail(detail: RagAgentDetail): void { - emitBare(`Version ${detail.agent_version ?? "?"}:`); - if (detail.agent_version_desc) emitBare(` desc: ${detail.agent_version_desc}`); - if (detail.publish_time !== undefined) emitBare(` published: ${detail.publish_time}`); +function detailLines(detail: RagAgentDetail, localize: (text: LocalizedText) => string): string[] { + const lines = [ + localize({ + "en-US": `Version ${detail.agent_version ?? "?"}:`, + "zh-CN": `版本 ${detail.agent_version ?? "?"}:`, + }), + ]; + if (detail.agent_version_desc) + lines.push( + localize({ + "en-US": ` desc: ${detail.agent_version_desc}`, + "zh-CN": ` 描述: ${detail.agent_version_desc}`, + }), + ); + if (detail.publish_time !== undefined) + lines.push( + localize({ + "en-US": ` published: ${detail.publish_time}`, + "zh-CN": ` 发布时间: ${detail.publish_time}`, + }), + ); const config = detail.agent_config; - if (!config) return; - if (config.agent_policy) emitBare(` policy: ${config.agent_policy}`); - if (config.agent_model) emitBare(` model: ${config.agent_model}`); + if (!config) return lines; + if (config.agent_policy) + lines.push( + localize({ + "en-US": ` policy: ${config.agent_policy}`, + "zh-CN": ` 策略: ${config.agent_policy}`, + }), + ); + if (config.agent_model) + lines.push( + localize({ + "en-US": ` model: ${config.agent_model}`, + "zh-CN": ` 模型: ${config.agent_model}`, + }), + ); if (config.hybrid_rerank !== undefined) - emitBare(` hybrid_rerank: ${JSON.stringify(config.hybrid_rerank)}`); - if (config.temperature !== undefined) emitBare(` temperature: ${config.temperature}`); + lines.push(` hybrid_rerank: ${JSON.stringify(config.hybrid_rerank)}`); + if (config.temperature !== undefined) + lines.push( + localize({ + "en-US": ` temperature: ${config.temperature}`, + "zh-CN": ` 温度: ${config.temperature}`, + }), + ); for (const kbConfig of config.kb_search_configs ?? []) { const kbId = typeof kbConfig.id === "string" ? kbConfig.id : "?"; const kbName = typeof kbConfig.name === "string" ? ` (${kbConfig.name})` : ""; - emitBare(` kb: ${kbId}${kbName}`); + lines.push( + localize({ "en-US": ` kb: ${kbId}${kbName}`, "zh-CN": ` 知识库: ${kbId}${kbName}` }), + ); for (const key of [ "rerank", "rerank_model", @@ -52,9 +88,10 @@ function printDetail(detail: RagAgentDetail): void { "dense_similarity_top_k", "sparse_similarity_top_k", ]) { - if (kbConfig[key] !== undefined) emitBare(` ${key}: ${JSON.stringify(kbConfig[key])}`); + if (kbConfig[key] !== undefined) lines.push(` ${key}: ${JSON.stringify(kbConfig[key])}`); } } + return lines; } export default defineCommand({ @@ -88,7 +125,7 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const workspaceId = resolveWorkspaceId(ctx); - const format = detectOutputFormat(settings.output); + const format = settings.output; const body = { agent_id: flags.agentId, @@ -97,7 +134,7 @@ export default defineCommand({ const endpoint = ragEndpoint(workspaceId, RAG_PATHS.agentGet); if (settings.dryRun) { - emitResult({ endpoint, request: body }, format); + ctx.output.result({ endpoint, request: body }); return; } @@ -107,23 +144,33 @@ export default defineCommand({ body, }); - const data = response.data; - if (settings.quiet) { - emitBare(data?.agent_id ?? ""); - return; - } - if (format !== "text") { - emitResult(response, format); - return; - } - emitBare("Basic:"); - emitBare(` id: ${data?.agent_id ?? "-"}`); - emitBare(` name: ${data?.agent_name ?? "-"}`); - emitBare(` desc: ${data?.agent_desc ?? "-"}`); - emitBare(` scene: ${data?.agent_scene ?? "-"}`); - emitBare(` status: ${data?.agent_status ?? "-"}`); - for (const detail of data?.agent_details ?? []) { - printDetail(detail); + let text: string | undefined; + if (format === "text") { + const data = response.data; + const lines = [ + ctx.localize({ "en-US": "Basic:", "zh-CN": "基本信息:" }), + ` id: ${data?.agent_id ?? "-"}`, + ctx.localize({ + "en-US": ` name: ${data?.agent_name ?? "-"}`, + "zh-CN": ` 名称: ${data?.agent_name ?? "-"}`, + }), + ctx.localize({ + "en-US": ` desc: ${data?.agent_desc ?? "-"}`, + "zh-CN": ` 描述: ${data?.agent_desc ?? "-"}`, + }), + ctx.localize({ + "en-US": ` scene: ${data?.agent_scene ?? "-"}`, + "zh-CN": ` 场景: ${data?.agent_scene ?? "-"}`, + }), + ctx.localize({ + "en-US": ` status: ${data?.agent_status ?? "-"}`, + "zh-CN": ` 状态: ${data?.agent_status ?? "-"}`, + }), + ]; + for (const detail of data?.agent_details ?? []) + lines.push(...detailLines(detail, ctx.localize)); + text = lines.join("\n"); } + ctx.output.result(response, text === undefined ? undefined : { text }); }, }); diff --git a/packages/commands/src/commands/knowledge/service-list.ts b/packages/commands/src/commands/knowledge/service-list.ts index a3c346a67..6801ef53d 100644 --- a/packages/commands/src/commands/knowledge/service-list.ts +++ b/packages/commands/src/commands/knowledge/service-list.ts @@ -2,11 +2,9 @@ import { defineCommand, ragEndpoint, RAG_PATHS, - detectOutputFormat, type FlagsDef, type RagAgentListResponse, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { resolveWorkspaceId, truncateLine, PAGE_FLAGS, WORKSPACE_FLAG } from "./shared.ts"; const SERVICE_LIST_FLAGS = { @@ -85,7 +83,7 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const workspaceId = resolveWorkspaceId(ctx); - const format = detectOutputFormat(settings.output); + const format = settings.output; // agent/list pagination goes in the body const body = { @@ -100,7 +98,7 @@ export default defineCommand({ const endpoint = ragEndpoint(workspaceId, RAG_PATHS.agentList); if (settings.dryRun) { - emitResult({ endpoint, request: body }, format); + ctx.output.result({ endpoint, request: body }); return; } @@ -110,21 +108,19 @@ export default defineCommand({ body, }); - const rows = response.data?.rows ?? []; - if (settings.quiet) { - for (const row of rows) emitBare(row.agent_id ?? ""); - return; - } + let text: string | undefined; if (format === "text") { + const rows = response.data?.rows ?? []; + const lines: string[] = []; if (rows.length === 0) { - emitBare("No services found."); + lines.push(ctx.localize({ "en-US": "No services found.", "zh-CN": "未找到服务。" })); } else { for (const row of rows) { const kbNames = (row.pipeline_list ?? []) .map((pipeline) => pipeline.pipeline_name) .filter(Boolean) .join(","); - emitBare( + lines.push( truncateLine( [ row.agent_id, @@ -139,15 +135,27 @@ export default defineCommand({ ); } } - emitBare(`total: ${response.data?.total_count ?? rows.length}`); - // Connect finding an ID with using it (capability wording, no hardcoded product path) - emitBare( - flags.scene === "search" - ? "Use an agent_id above with the knowledge search command." - : "Use an agent_id above with the knowledge chat command.", + lines.push( + ctx.localize({ + "en-US": `total: ${response.data?.total_count ?? rows.length}`, + "zh-CN": `总数: ${response.data?.total_count ?? rows.length}`, + }), + ); + lines.push( + ctx.localize( + flags.scene === "search" + ? { + "en-US": "Use an agent_id above with the knowledge search command.", + "zh-CN": "将上述 agent_id 用于知识库搜索命令。", + } + : { + "en-US": "Use an agent_id above with the knowledge chat command.", + "zh-CN": "将上述 agent_id 用于知识库对话命令。", + }, + ), ); - } else { - emitResult(response, format); + text = lines.join("\n"); } + ctx.output.result(response, text === undefined ? undefined : { text }); }, }); diff --git a/packages/commands/src/commands/knowledge/service-update.ts b/packages/commands/src/commands/knowledge/service-update.ts index 1bc877da7..30b2bb293 100644 --- a/packages/commands/src/commands/knowledge/service-update.ts +++ b/packages/commands/src/commands/knowledge/service-update.ts @@ -4,14 +4,13 @@ import { defineCommand, ragEndpoint, RAG_PATHS, - detectOutputFormat, BailianError, ExitCode, type Client, + type CommandOutput, type FlagsDef, type RagAgentConfig, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts"; const BOOL_CHOICES = ["true", "false"]; @@ -160,7 +159,10 @@ function collectScalarConfig(flags: UpdateFlags): Partial { } /** --config-file: read JSON + structural/enum/range validation; unknown fields warn but pass through */ -export function parseConfigFile(filePath: string): RagAgentConfig { +export function parseConfigFile( + filePath: string, + output: Pick, +): RagAgentConfig { let raw: string; try { raw = readFileSync(filePath, "utf-8"); @@ -182,7 +184,7 @@ export function parseConfigFile(filePath: string): RagAgentConfig { throw new BailianError("--config-file must contain a JSON object.", ExitCode.USAGE); } const config = parsed as RagAgentConfig; - validateConfigValues(config); + validateConfigValues(config, output); return config; } @@ -215,7 +217,10 @@ const KNOWN_CONFIG_KEYS = new Set([ ]); /** Enum/range validation + behavioral warnings, per the agent_config contract */ -export function validateConfigValues(config: RagAgentConfig): void { +export function validateConfigValues( + config: RagAgentConfig, + output: Pick, +): void { if (config.agent_policy !== undefined && !["turbo", "agentic"].includes(config.agent_policy)) { throw new BailianError("agent_policy must be turbo or agentic", ExitCode.USAGE); } @@ -237,15 +242,27 @@ export function validateConfigValues(config: RagAgentConfig): void { | { rerank_mode?: string; rerank_instruct?: string } | undefined; if (rerank?.rerank_instruct && rerank.rerank_mode !== "custom") { - process.stderr.write( - "Warning: rerank_instruct is only effective with rerank_mode=custom; the server clears it otherwise.\n", - ); + output.warning({ + code: "KNOWLEDGE_SERVICE_RERANK_INSTRUCT_IGNORED", + message: { + "en-US": + "rerank_instruct is only effective with rerank_mode=custom; the server clears it otherwise.", + "zh-CN": "rerank_instruct 仅在 rerank_mode=custom 时生效;否则服务端会将其清空。", + }, + }); } } // Unknown fields: warn but pass through (the server is the source of truth) for (const key of Object.keys(config)) { if (!KNOWN_CONFIG_KEYS.has(key)) { - process.stderr.write(`Warning: unknown agent_config field passed through: ${key}\n`); + output.warning({ + code: "KNOWLEDGE_SERVICE_UNKNOWN_CONFIG_FIELD", + message: { + "en-US": `unknown agent_config field passed through: ${key}`, + "zh-CN": `未知 agent_config 字段已原样传递: ${key}`, + }, + details: { field: key }, + }); } } } @@ -368,7 +385,7 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const workspaceId = resolveWorkspaceId(ctx); - const format = detectOutputFormat(settings.output); + const format = settings.output; const scalarConfig = collectScalarConfig(flags); const hasConfigChange = flags.configFile !== undefined || Object.keys(scalarConfig).length > 0; @@ -376,7 +393,7 @@ export default defineCommand({ let agentConfig: RagAgentConfig | undefined; if (flags.configFile !== undefined) { // dry-run also reads the file and validates (rehearsal semantics) - agentConfig = parseConfigFile(flags.configFile); + agentConfig = parseConfigFile(flags.configFile, ctx.output); } else if (Object.keys(scalarConfig).length > 0) { if (settings.dryRun) { // dry-run does not issue the read-merge request; show the scalar delta with a placeholder note @@ -386,7 +403,7 @@ export default defineCommand({ // then merge (hidden from the user) const currentConfig = await fetchBetaConfig(ctx.client, workspaceId, flags.agentId); agentConfig = { ...currentConfig, ...scalarConfig }; - validateConfigValues(agentConfig); + validateConfigValues(agentConfig, ctx.output); } } @@ -401,29 +418,39 @@ export default defineCommand({ const endpoint = ragEndpoint(workspaceId, RAG_PATHS.agentUpdate); if (settings.dryRun) { - emitResult( - { - endpoint, - request: body, - ...(hasConfigChange && flags.configFile === undefined - ? { note: "scalar config flags are merged into the current beta config at run time" } - : {}), - }, - format, - ); + ctx.output.result({ + endpoint, + request: body, + ...(hasConfigChange && flags.configFile === undefined + ? { note: "scalar config flags are merged into the current beta config at run time" } + : {}), + }); return; } const response = await updateKnowledgeService(ctx.client, workspaceId, body); - if (settings.quiet) return; - if (format === "text") { - emitBare(`updated: ${flags.agentId}`); - if (hasConfigChange) { - emitBare("Draft config changed — verify with --agent-version beta, then deploy."); - } - return; - } - emitResult(response, format); + ctx.output.result( + response, + format === "text" + ? { + text: [ + ctx.localize({ + "en-US": `updated: ${flags.agentId}`, + "zh-CN": `已更新: ${flags.agentId}`, + }), + ...(hasConfigChange + ? [ + ctx.localize({ + "en-US": + "Draft config changed — verify with --agent-version beta, then deploy.", + "zh-CN": "草稿配置已更改,请使用 --agent-version beta 验证,然后部署。", + }), + ] + : []), + ].join("\n"), + } + : undefined, + ); }, }); diff --git a/packages/commands/src/commands/knowledge/shared.ts b/packages/commands/src/commands/knowledge/shared.ts index 2dcd6cdbc..1daf1b7c0 100644 --- a/packages/commands/src/commands/knowledge/shared.ts +++ b/packages/commands/src/commands/knowledge/shared.ts @@ -4,6 +4,7 @@ import { ExitCode, ragEndpoint, RAG_PATHS, + type CommandOutput, type Client, type FlagsDef, type RagIndexJobDoc, @@ -117,10 +118,11 @@ export function importJobStatusUrl(workspaceId: string, indexId: string, jobId: export async function pollImportJob( client: Client, settings: Settings, - options: { statusUrl: string; intervalSec: number }, + options: { statusUrl: string; intervalSec: number; output: CommandOutput }, ): Promise { const deadline = Date.now() + settings.timeout * 1000; - return poll(client, settings, { + return poll(client, { + output: options.output, url: options.statusUrl, load: () => readImportJobPages(client, options.statusUrl, deadline), intervalSec: options.intervalSec, diff --git a/packages/commands/src/commands/knowledge/sync/ports.ts b/packages/commands/src/commands/knowledge/sync/ports.ts index 7a08b5ab3..c48ac399b 100644 --- a/packages/commands/src/commands/knowledge/sync/ports.ts +++ b/packages/commands/src/commands/knowledge/sync/ports.ts @@ -3,6 +3,7 @@ import { BailianError, ExitCode, type Client, + type CommandOutput, type LocalizedText, type Settings, } from "bailian-cli-core"; @@ -19,6 +20,7 @@ export function createSyncExecutionPorts(options: { target: { workspaceId: string; indexId: string; categoryId: string; scopeTag: string }; settings: Settings; pollInterval: number; + output: CommandOutput; localize: (text: LocalizedText) => string; verify: () => Promise; }): SyncExecutionPorts { @@ -65,6 +67,7 @@ export function createSyncExecutionPorts(options: { }, async waitReady(fileId, ingestionId, tags) { const job = await pollImportJob(client, settings, { + output: options.output, statusUrl: importJobStatusUrl(target.workspaceId, target.indexId, ingestionId).toString(), intervalSec: options.pollInterval, }); diff --git a/packages/commands/src/commands/knowledge/upload-support.ts b/packages/commands/src/commands/knowledge/upload-support.ts index 173feb2eb..874847455 100644 --- a/packages/commands/src/commands/knowledge/upload-support.ts +++ b/packages/commands/src/commands/knowledge/upload-support.ts @@ -205,7 +205,10 @@ export function checkUploadFile( } return { sizeBytes, - warning: `${basename(filePath)} exceeds the recommended 10 MB for ${extension}; the server may reject or truncate it.`, + warning: localize({ + "en-US": `${basename(filePath)} exceeds the recommended 10 MB for ${extension}; the server may reject or truncate it.`, + "zh-CN": `${basename(filePath)} 超过 ${extension} 的建议 10 MB 大小;服务端可能拒绝或截断该文件。`, + }), }; } return { sizeBytes }; diff --git a/packages/commands/src/commands/log/audit-disable.ts b/packages/commands/src/commands/log/audit-disable.ts index 492f1a909..54e0ef207 100644 --- a/packages/commands/src/commands/log/audit-disable.ts +++ b/packages/commands/src/commands/log/audit-disable.ts @@ -1,4 +1,3 @@ -import { detectOutputFormat } from "bailian-cli-core"; import { defineTelemetryCommand } from "../shared/telemetry.ts"; import { disableLogDelivery } from "./shared.ts"; @@ -18,13 +17,12 @@ export default defineTelemetryCommand({ ], exampleArgs: ["", "--dry-run", "--output json"], async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); await disableLogDelivery( { client: ctx.client, + output: ctx.output, settings: ctx.settings, binName: ctx.identity.binName, - format, }, "audit", true, diff --git a/packages/commands/src/commands/log/audit-enable.ts b/packages/commands/src/commands/log/audit-enable.ts index 225a09faa..b261dabd4 100644 --- a/packages/commands/src/commands/log/audit-enable.ts +++ b/packages/commands/src/commands/log/audit-enable.ts @@ -1,4 +1,3 @@ -import { detectOutputFormat } from "bailian-cli-core"; import { defineTelemetryCommand } from "../shared/telemetry.ts"; import { LOG_NO_WAIT_FLAG, enableLogDelivery } from "./shared.ts"; import { LOG_SERVICE_TYPES } from "./status.ts"; @@ -26,13 +25,12 @@ export default defineTelemetryCommand({ ], exampleArgs: ["", "--no-wait", "--output json"], async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); await enableLogDelivery( { client: ctx.client, + output: ctx.output, settings: ctx.settings, binName: ctx.identity.binName, - format, noWait: ctx.flags.noWait, }, "audit", diff --git a/packages/commands/src/commands/log/audit-get.ts b/packages/commands/src/commands/log/audit-get.ts index 5097037ff..2cad26ddb 100644 --- a/packages/commands/src/commands/log/audit-get.ts +++ b/packages/commands/src/commands/log/audit-get.ts @@ -1,5 +1,5 @@ -import { BailianError, ExitCode, detectOutputFormat } from "bailian-cli-core"; -import { ansi, emitResult } from "bailian-cli-runtime"; +import { BailianError, ExitCode } from "bailian-cli-core"; +import { ansi } from "bailian-cli-runtime"; import { defineTelemetryCommand, telemetryGatewayPreview, @@ -12,7 +12,7 @@ import { type ModelLogEntry, normalizeAuditLog, assertAuditWorkspace, - printLogDetail, + renderLogDetail, validateHoursFlag, validateRequestIdLength, } from "./shared.ts"; @@ -64,7 +64,7 @@ export default defineTelemetryCommand({ }, async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const format = settings.output; // A single request may sit far back; default window is 24h here. const { startTime, endTime } = resolveTimeRange(flags, 1); @@ -82,14 +82,11 @@ export default defineTelemetryCommand({ if (flags.model) reqDTO.models = [flags.model]; if (settings.dryRun) { - emitResult( - { - api: LIST_AUDIT_LOGS_API, - data: { reqDTO }, - ...telemetryGatewayPreview(settings), - }, - format, - ); + ctx.output.result({ + api: LIST_AUDIT_LOGS_API, + data: { reqDTO }, + ...telemetryGatewayPreview(settings), + }); return; } @@ -106,16 +103,19 @@ export default defineTelemetryCommand({ ); } - if (format === "json") { - emitResult(entry, format); - return; - } + let text: string | undefined; + if (format === "text") { + const textChunks: string[] = []; - printLogDetail(entry); - if (entry.originLog) { - process.stdout.write( - `\n${ansi(process.stdout).bold("Origin record:")}\n${JSON.stringify(entry.originLog, null, 2)}\n`, - ); + textChunks.push(renderLogDetail(entry) + "\n"); + if (entry.originLog) { + textChunks.push( + `\n${ansi(process.stdout).bold("Origin record:")}\n${JSON.stringify(entry.originLog, null, 2)}\n`, + ); + } + + text = textChunks.join("").replace(/\n$/, ""); } + ctx.output.result(entry, { text }); }, }); diff --git a/packages/commands/src/commands/log/audit-list.ts b/packages/commands/src/commands/log/audit-list.ts index e85169646..c70f57e31 100644 --- a/packages/commands/src/commands/log/audit-list.ts +++ b/packages/commands/src/commands/log/audit-list.ts @@ -1,5 +1,3 @@ -import { detectOutputFormat } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; import { parseCommaList } from "../shared/params.ts"; import { defineTelemetryCommand, @@ -16,7 +14,7 @@ import { LIST_AUDIT_LOGS_API, normalizeAuditLog, assertAuditWorkspace, - printLogTable, + renderLogTable, validateHoursFlag, validateStatusCodeTypes, } from "./shared.ts"; @@ -99,7 +97,7 @@ export default defineTelemetryCommand({ }, async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const format = settings.output; const { startTime, endTime } = resolveTimeRange(flags, 1 / 24); const reqDTO: Record = { @@ -116,14 +114,11 @@ export default defineTelemetryCommand({ }; if (settings.dryRun) { - emitResult( - { - api: LIST_AUDIT_LOGS_API, - data: { reqDTO }, - ...telemetryGatewayPreview(settings), - }, - format, - ); + ctx.output.result({ + api: LIST_AUDIT_LOGS_API, + data: { reqDTO }, + ...telemetryGatewayPreview(settings), + }); return; } @@ -133,21 +128,24 @@ export default defineTelemetryCommand({ assertAuditWorkspace(list, ctx); const nextToken = resp.nextToken as string | undefined; - if (format === "json") { - emitResult( - { - totalCount: resp.totalCount ?? 0, - nextToken, - list, - }, - format, - ); - return; - } + let text: string | undefined; + if (format === "text") { + const textChunks: string[] = []; - printLogTable(list); - if (nextToken) { - process.stdout.write(`Next page: --next-token ${nextToken}\n`); + textChunks.push(renderLogTable(list) + "\n"); + if (nextToken) { + textChunks.push(`Next page: --next-token ${nextToken}\n`); + } + + text = textChunks.join("").replace(/\n$/, ""); } + ctx.output.result( + { + totalCount: resp.totalCount ?? 0, + nextToken, + list, + }, + { text }, + ); }, }); diff --git a/packages/commands/src/commands/log/inference-disable.ts b/packages/commands/src/commands/log/inference-disable.ts index 0cbef1060..8e0feea8d 100644 --- a/packages/commands/src/commands/log/inference-disable.ts +++ b/packages/commands/src/commands/log/inference-disable.ts @@ -1,4 +1,3 @@ -import { detectOutputFormat } from "bailian-cli-core"; import { defineTelemetryCommand } from "../shared/telemetry.ts"; import { disableLogDelivery } from "./shared.ts"; @@ -18,13 +17,12 @@ export default defineTelemetryCommand({ ], exampleArgs: ["", "--dry-run", "--output json"], async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); await disableLogDelivery( { client: ctx.client, + output: ctx.output, settings: ctx.settings, binName: ctx.identity.binName, - format, }, "inference", ); diff --git a/packages/commands/src/commands/log/inference-enable.ts b/packages/commands/src/commands/log/inference-enable.ts index 7fff06b90..2a0b4776e 100644 --- a/packages/commands/src/commands/log/inference-enable.ts +++ b/packages/commands/src/commands/log/inference-enable.ts @@ -1,4 +1,3 @@ -import { detectOutputFormat } from "bailian-cli-core"; import { defineTelemetryCommand } from "../shared/telemetry.ts"; import { LOG_NO_WAIT_FLAG, enableLogDelivery, requireAuditLogEnabled } from "./shared.ts"; import { LOG_SERVICE_TYPES } from "./status.ts"; @@ -26,12 +25,11 @@ export default defineTelemetryCommand({ ], exampleArgs: ["", "--no-wait", "--output json"], async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); const enableContext = { client: ctx.client, + output: ctx.output, settings: ctx.settings, binName: ctx.identity.binName, - format, noWait: ctx.flags.noWait, }; diff --git a/packages/commands/src/commands/log/inference-get.ts b/packages/commands/src/commands/log/inference-get.ts index d0a18c1ff..0bb66d993 100644 --- a/packages/commands/src/commands/log/inference-get.ts +++ b/packages/commands/src/commands/log/inference-get.ts @@ -1,5 +1,5 @@ -import { BailianError, ExitCode, detectOutputFormat } from "bailian-cli-core"; -import { ansi, emitResult } from "bailian-cli-runtime"; +import { BailianError, ExitCode } from "bailian-cli-core"; +import { ansi } from "bailian-cli-runtime"; import { defineTelemetryCommand, telemetryGatewayPreview, @@ -10,7 +10,7 @@ import { } from "../shared/telemetry.ts"; import { type ModelLogEntry, - printLogDetail, + renderLogDetail, validateHoursFlag, validateRequestIdLength, } from "./shared.ts"; @@ -62,7 +62,7 @@ export default defineTelemetryCommand({ }, async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const format = settings.output; // A single request may sit far back; default window is 24h here. const { startTime, endTime } = resolveTimeRange(flags, 1); @@ -74,16 +74,13 @@ export default defineTelemetryCommand({ }; if (settings.dryRun) { - emitResult( - { - apis: [LIST_INFERENCE_LOGS_API, ORIGIN_LOG_API], - data: { - reqDTO: { ...baseReqDTO, startTime: String(startTime), endTime: String(endTime) }, - }, - ...telemetryGatewayPreview(settings), + ctx.output.result({ + apis: [LIST_INFERENCE_LOGS_API, ORIGIN_LOG_API], + data: { + reqDTO: { ...baseReqDTO, startTime: String(startTime), endTime: String(endTime) }, }, - format, - ); + ...telemetryGatewayPreview(settings), + }); return; } @@ -117,18 +114,21 @@ export default defineTelemetryCommand({ entry.request = entry.request ?? origin?.request; entry.response = entry.response ?? origin?.response; - if (format === "json") { - emitResult(entry, format); - return; - } + let text: string | undefined; + if (format === "text") { + const textChunks: string[] = []; - printLogDetail(entry); - if (!entry.request && !entry.response) { - process.stdout.write( - ansi(process.stdout).dim( - "\nRequest/response content is only available when inference log delivery was enabled at call time.\n", - ), - ); + textChunks.push(renderLogDetail(entry) + "\n"); + if (!entry.request && !entry.response) { + textChunks.push( + ansi(process.stdout).dim( + "\nRequest/response content is only available when inference log delivery was enabled at call time.", + ), + ); + } + + text = textChunks.join("").replace(/\n$/, ""); } + ctx.output.result(entry, { text }); }, }); diff --git a/packages/commands/src/commands/log/inference-list.ts b/packages/commands/src/commands/log/inference-list.ts index cb0e38949..4d5240ed8 100644 --- a/packages/commands/src/commands/log/inference-list.ts +++ b/packages/commands/src/commands/log/inference-list.ts @@ -1,6 +1,4 @@ -import { detectOutputFormat } from "bailian-cli-core"; import { defineTelemetryCommand, telemetryGatewayPreview } from "../shared/telemetry.ts"; -import { emitResult } from "bailian-cli-runtime"; import { parseCommaList } from "../shared/params.ts"; import { TELEMETRY_LOG_TIME_FLAGS, @@ -11,7 +9,7 @@ import { import { type ModelLogEntry, LIST_INFERENCE_LOGS_API, - printLogTable, + renderLogTable, validateHoursFlag, validateStatusCodeTypes, } from "./shared.ts"; @@ -108,7 +106,7 @@ export default defineTelemetryCommand({ }, async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const format = settings.output; const { startTime, endTime } = resolveTimeRange(flags, 1 / 24); // The inference log API takes single model/apikey values and string timestamps. @@ -129,14 +127,11 @@ export default defineTelemetryCommand({ }; if (settings.dryRun) { - emitResult( - { - api: LIST_INFERENCE_LOGS_API, - data: { reqDTO }, - ...telemetryGatewayPreview(settings), - }, - format, - ); + ctx.output.result({ + api: LIST_INFERENCE_LOGS_API, + data: { reqDTO }, + ...telemetryGatewayPreview(settings), + }); return; } @@ -144,17 +139,20 @@ export default defineTelemetryCommand({ const resp = await pollTelemetryData(ctx.client, LIST_INFERENCE_LOGS_API, reqDTO); const list = (resp.list as ModelLogEntry[]) ?? []; - if (format === "json") { - emitResult( - { - totalCount: resp.totalCount ?? 0, - list, - }, - format, - ); - return; - } + let text: string | undefined; + if (format === "text") { + const textChunks: string[] = []; + + textChunks.push(renderLogTable(list) + "\n"); - printLogTable(list); + text = textChunks.join("").replace(/\n$/, ""); + } + ctx.output.result( + { + totalCount: resp.totalCount ?? 0, + list, + }, + { text }, + ); }, }); diff --git a/packages/commands/src/commands/log/shared.ts b/packages/commands/src/commands/log/shared.ts index 43e24c012..e3ed20d44 100644 --- a/packages/commands/src/commands/log/shared.ts +++ b/packages/commands/src/commands/log/shared.ts @@ -1,13 +1,13 @@ import { BailianError, ExitCode, - detectOutputFormat, type Client, + type CommandOutput, type CommandContext, type FlagsDef, type LocalizedText, } from "bailian-cli-core"; -import { ansi, emitResult, renderBoxTable } from "bailian-cli-runtime"; +import { ansi, renderBoxTable } from "bailian-cli-runtime"; import { formatNumber, formatDateTime } from "../shared/format.ts"; import { parseCommaList } from "../shared/params.ts"; import { @@ -144,12 +144,14 @@ export function validateRequestIdLength(requestId: string): string | undefined { return undefined; } -export function printLogTable(list: ModelLogEntry[]): void { +export function renderLogTable(list: ModelLogEntry[]): string { + const textChunks: string[] = []; + const color = ansi(process.stdout); if (list.length === 0) { - process.stdout.write("No logs found in this range.\n"); - return; + textChunks.push("No logs found in this range.\n"); + return textChunks.join("").replace(/\n$/, ""); } const lines = renderBoxTable({ @@ -181,10 +183,14 @@ export function printLogTable(list: ModelLogEntry[]): void { return color.yellow(value); }, }); - for (const line of lines) process.stdout.write(line + "\n"); + for (const line of lines) textChunks.push(line + "\n"); + + return textChunks.join("").replace(/\n$/, ""); } -export function printLogDetail(entry: ModelLogEntry): void { +export function renderLogDetail(entry: ModelLogEntry): string { + const textChunks: string[] = []; + const color = ansi(process.stdout); const usage = parseLogUsage(entry.usage); @@ -210,14 +216,16 @@ export function printLogDetail(entry: ModelLogEntry): void { if (entry.errorMessage) rows.push(["Error Message", entry.errorMessage]); for (const [label, value] of rows) { - process.stdout.write(`${color.bold(label)}: ${value}\n`); + textChunks.push(`${color.bold(label)}: ${value}\n`); } if (entry.request) { - process.stdout.write(`\n${color.bold("Request:")}\n${entry.request}\n`); + textChunks.push(`\n${color.bold("Request:")}\n${entry.request}\n`); } if (entry.response) { - process.stdout.write(`\n${color.bold("Response:")}\n${entry.response}\n`); + textChunks.push(`\n${color.bold("Response:")}\n${entry.response}\n`); } + + return textChunks.join("").replace(/\n$/, ""); } // --------------------------------------------------------------------------- @@ -282,10 +290,10 @@ function groupReqDTO(settings: Settings, kind: LogKind): Record // --------------------------------------------------------------------------- export interface LogEnableContext { + output: CommandOutput; client: Client; settings: Settings; binName: string; - format: "json" | "text"; noWait?: boolean; } @@ -299,18 +307,15 @@ export async function enableLogDelivery( kind: LogKind, serviceType: string, ): Promise { - const { client, settings, format } = ctx; - const color = ansi(process.stderr); + const { client, settings } = ctx; + const format = settings.output; if (settings.dryRun) { - emitResult( - { - apis: [CREATE_SLR_API, INIT_STORE_API, ENABLE_GROUP_API], - data: { reqDTO: groupReqDTO(settings, kind) }, - ...telemetryGatewayPreview(settings), - }, - format, - ); + ctx.output.result({ + apis: [CREATE_SLR_API, INIT_STORE_API, ENABLE_GROUP_API], + data: { reqDTO: groupReqDTO(settings, kind) }, + ...telemetryGatewayPreview(settings), + }); return; } @@ -319,20 +324,27 @@ export async function enableLogDelivery( // Step 1: SLS service-linked role. await ensureTelemetrySlrAuthorized(client, "Log", settings.workspaceId, { pollRetries: 5, - onProgress: (message) => process.stderr.write(color.dim(message + "\n")), + onProgress: (message) => + ctx.output.progress({ "en-US": message, "zh-CN": "正在授权服务关联角色……" }), }); // Step 2: SLS store instance (async creation, poll until Ready). let serviceStatus = await getTelemetryServiceStatus(client, serviceType, settings.workspaceId); if (serviceStatus.instanceStatus === "NotExist") { - process.stderr.write(color.dim("Initializing the SLS store instance...\n")); + ctx.output.info({ + "en-US": "Initializing the SLS store instance...", + "zh-CN": "正在初始化 SLS 存储实例……", + }); await pollTelemetryData(client, INIT_STORE_API, { ...(settings.workspaceId ? { workspaceId: settings.workspaceId } : {}), serviceType, }); if (!ctx.noWait) { - process.stderr.write(color.dim("Waiting for the SLS instance to become ready...\n")); + ctx.output.info({ + "en-US": "Waiting for the SLS instance to become ready...", + "zh-CN": "正在等待 SLS 实例就绪……", + }); const deadline = Date.now() + WAIT_TIMEOUT_MS; while (Date.now() < deadline && serviceStatus.instanceStatus !== "Ready") { await new Promise((resolve) => setTimeout(resolve, WAIT_INTERVAL_MS)); @@ -344,17 +356,23 @@ export async function enableLogDelivery( // Step 3: flip the delivery switch for every model in the workspace. await client.console(ENABLE_GROUP_API, { reqDTO: groupReqDTO(settings, kind) }); - if (format === "json") { - emitResult({ enabled: true, kind, instanceStatus: serviceStatus.instanceStatus }, format); - return; - } + let text: string | undefined; + if (format === "text") { + const textChunks: string[] = []; - process.stdout.write(`${kind === "audit" ? "Audit" : "Inference"} log delivery enabled.\n`); - if (ctx.noWait) { - process.stdout.write( - `The SLS instance may still be initializing; check \`${ctx.binName} log status\` later.\n`, - ); + textChunks.push(`${kind === "audit" ? "Audit" : "Inference"} log delivery enabled.\n`); + if (ctx.noWait) { + textChunks.push( + `The SLS instance may still be initializing; check \`${ctx.binName} log status\` later.\n`, + ); + } + + text = textChunks.join("").replace(/\n$/, ""); } + ctx.output.result( + { enabled: true, kind, instanceStatus: serviceStatus.instanceStatus }, + { text }, + ); } /** @@ -366,17 +384,15 @@ export async function disableLogDelivery( kind: LogKind, guardInferenceOn?: boolean, ): Promise { - const { client, settings, format } = ctx; + const { client, settings } = ctx; + const format = settings.output; if (settings.dryRun) { - emitResult( - { - api: DISABLE_GROUP_API, - data: { reqDTO: groupReqDTO(settings, kind) }, - ...telemetryGatewayPreview(settings), - }, - format, - ); + ctx.output.result({ + api: DISABLE_GROUP_API, + data: { reqDTO: groupReqDTO(settings, kind) }, + ...telemetryGatewayPreview(settings), + }); return; } @@ -395,12 +411,15 @@ export async function disableLogDelivery( await client.console(DISABLE_GROUP_API, { reqDTO: groupReqDTO(settings, kind) }); - if (format === "json") { - emitResult({ disabled: true, kind }, format); - return; - } + let text: string | undefined; + if (format === "text") { + const textChunks: string[] = []; - process.stdout.write(`${kind === "audit" ? "Audit" : "Inference"} log delivery disabled.\n`); + textChunks.push(`${kind === "audit" ? "Audit" : "Inference"} log delivery disabled.\n`); + + text = textChunks.join("").replace(/\n$/, ""); + } + ctx.output.result({ disabled: true, kind }, { text }); } /** Shared --no-wait flag used by both enable commands. */ @@ -453,13 +472,13 @@ export function validateInferenceCountFlags(flags: LogCountFlags): LocalizedText /** 两类日志各用自己的列表 totalCount,避免独立计数接口与列表的口径错配。 */ export async function countLogs( - ctx: Pick, + ctx: Pick, kind: LogKind, flags: LogCountFlags, resolveRange: () => { startTime: number; endTime: number }, ): Promise { const { settings } = ctx; - const format = detectOutputFormat(settings.output); + const format = settings.output; const { startTime, endTime } = resolveRange(); const api = kind === "audit" ? LIST_AUDIT_LOGS_API : LIST_INFERENCE_LOGS_API; const baseReqDTO: Record = { @@ -497,14 +516,11 @@ export async function countLogs( } if (settings.dryRun) { - emitResult( - { - api, - data: reqDTOs.length === 1 ? { reqDTO: reqDTOs[0] } : { reqDTOs }, - ...telemetryGatewayPreview(settings), - }, - format, - ); + ctx.output.result({ + api, + data: reqDTOs.length === 1 ? { reqDTO: reqDTOs[0] } : { reqDTOs }, + ...telemetryGatewayPreview(settings), + }); return; } @@ -535,19 +551,22 @@ export async function countLogs( count += totalCount; } - if (format === "json") { - emitResult( - { - kind, - period: { start: formatDateTime(startTime), end: formatDateTime(endTime) }, - count, - }, - format, + let text: string | undefined; + if (format === "text") { + const textChunks: string[] = []; + + textChunks.push( + `${kind === "audit" ? "Audit" : "Inference"} logs: ${formatNumber(count)} (${formatDateTime(startTime)} ~ ${formatDateTime(endTime)})\n`, ); - return; - } - process.stdout.write( - `${kind === "audit" ? "Audit" : "Inference"} logs: ${formatNumber(count)} (${formatDateTime(startTime)} ~ ${formatDateTime(endTime)})\n`, + text = textChunks.join("").replace(/\n$/, ""); + } + ctx.output.result( + { + kind, + period: { start: formatDateTime(startTime), end: formatDateTime(endTime) }, + count, + }, + { text }, ); } diff --git a/packages/commands/src/commands/log/status.ts b/packages/commands/src/commands/log/status.ts index ca36d5044..f1c59a242 100644 --- a/packages/commands/src/commands/log/status.ts +++ b/packages/commands/src/commands/log/status.ts @@ -1,5 +1,4 @@ -import { detectOutputFormat } from "bailian-cli-core"; -import { ansi, emitResult, displayWidth, padEnd } from "bailian-cli-runtime"; +import { ansi, displayWidth, padEnd } from "bailian-cli-runtime"; import { defineTelemetryCommand, telemetryGatewayPreview, @@ -74,65 +73,65 @@ export default defineTelemetryCommand({ ], async run(ctx) { const { settings, identity } = ctx; - const format = detectOutputFormat(settings.output); + const format = settings.output; if (settings.dryRun) { - emitResult( - { - apis: [ - SLR_STATUS_API, - TELEMETRY_SERVICE_STATUS_API, - "zeldaEasy.bailian-telemetry.telemetryGroup.getTelemetryGroupStatus", - ], - data: { - reqDTO: { - workspaceId: settings.workspaceId, - slrType: "Log", - serviceTypes: Object.values(LOG_SERVICE_TYPES), - telemetryTypes: ["AuditLog", "InferenceLog"], - }, + ctx.output.result({ + apis: [ + SLR_STATUS_API, + TELEMETRY_SERVICE_STATUS_API, + "zeldaEasy.bailian-telemetry.telemetryGroup.getTelemetryGroupStatus", + ], + data: { + reqDTO: { + workspaceId: settings.workspaceId, + slrType: "Log", + serviceTypes: Object.values(LOG_SERVICE_TYPES), + telemetryTypes: ["AuditLog", "InferenceLog"], }, - ...telemetryGatewayPreview(settings), }, - format, - ); + ...telemetryGatewayPreview(settings), + }); return; } ensureTelemetryRegionSupported(settings); const status = await fetchLogServiceStatus(ctx.client, settings.workspaceId); - if (format === "json") { - emitResult(status, format); - return; - } + let text: string | undefined; + if (format === "text") { + const textChunks: string[] = []; - const color = ansi(process.stdout); - const rows: [string, string][] = [ - ["SLS Authorization (SLR)", status.slrAuthorized ? "Authorized" : "Not authorized"], - ["Audit Log (service)", statusText(status.audit)], - ["Audit Log (switch)", switchText(status.auditSwitch)], - ["Inference Log (service)", statusText(status.inference)], - ["Inference Log (switch)", switchText(status.inferenceSwitch)], - ]; - const instanceUrl = status.inference.instanceInfo?.instanceUrl; - if (instanceUrl) rows.push(["SLS Instance URL", instanceUrl]); + const color = ansi(process.stdout); + const rows: [string, string][] = [ + ["SLS Authorization (SLR)", status.slrAuthorized ? "Authorized" : "Not authorized"], + ["Audit Log (service)", statusText(status.audit)], + ["Audit Log (switch)", switchText(status.auditSwitch)], + ["Inference Log (service)", statusText(status.inference)], + ["Inference Log (switch)", switchText(status.inferenceSwitch)], + ]; + const instanceUrl = status.inference.instanceInfo?.instanceUrl; + if (instanceUrl) rows.push(["SLS Instance URL", instanceUrl]); - const maxLabel = Math.max(...rows.map(([label]) => displayWidth(label))); - for (const [label, value] of rows) { - process.stdout.write(`${color.bold(padEnd(label, maxLabel + 2))}${value}\n`); - } + const maxLabel = Math.max(...rows.map(([label]) => displayWidth(label))); + for (const [label, value] of rows) { + textChunks.push(`${color.bold(padEnd(label, maxLabel + 2))}${value}\n`); + } + + if (status.auditSwitch !== true) { + textChunks.push( + color.dim(`\nRun \`${identity.binName} log audit enable\` to enable audit log delivery.`), + ); + } else if (status.inferenceSwitch !== true) { + textChunks.push( + color.dim( + `\nRun \`${identity.binName} log inference enable\` to enable inference log delivery.`, + ), + ); + } - if (status.auditSwitch !== true) { - process.stdout.write( - color.dim(`\nRun \`${identity.binName} log audit enable\` to enable audit log delivery.\n`), - ); - } else if (status.inferenceSwitch !== true) { - process.stdout.write( - color.dim( - `\nRun \`${identity.binName} log inference enable\` to enable inference log delivery.\n`, - ), - ); + text = textChunks.join("").replace(/\n$/, ""); } + ctx.output.result(status, { text }); }, }); diff --git a/packages/commands/src/commands/log/trace-get.ts b/packages/commands/src/commands/log/trace-get.ts index 89ca21a24..1972c433c 100644 --- a/packages/commands/src/commands/log/trace-get.ts +++ b/packages/commands/src/commands/log/trace-get.ts @@ -1,6 +1,6 @@ -import { BailianError, ExitCode, detectOutputFormat } from "bailian-cli-core"; +import { BailianError, ExitCode } from "bailian-cli-core"; import { defineTelemetryCommand, telemetryGatewayPreview } from "../shared/telemetry.ts"; -import { ansi, emitResult } from "bailian-cli-runtime"; +import { ansi } from "bailian-cli-runtime"; import { formatNumber, formatDateTime } from "../shared/format.ts"; import { TELEMETRY_LOG_TIME_FLAGS, @@ -13,7 +13,9 @@ import { TRACE_STATUS_LABELS, type TraceEntry } from "./trace-list.ts"; const GET_TRACE_API = "zeldaEasy.bailian-telemetry.trace.getTraceWithOss"; -function printSpan(span: TraceEntry, depth: number): void { +function renderSpan(span: TraceEntry, depth: number): string { + const textChunks: string[] = []; + const color = ansi(process.stdout); const indent = " ".repeat(depth); const status = TRACE_STATUS_LABELS[span.statusCode ?? "0"] ?? "-"; @@ -22,15 +24,17 @@ function printSpan(span: TraceEntry, depth: number): void { const tokens = span.totalTokens != null ? `, ${formatNumber(span.totalTokens)} tokens` : ""; const statusText = status === "Error" ? color.red(status) : status; - process.stdout.write( + textChunks.push( `${indent}${span.spanName ?? "-"} ${color.dim(`[${statusText}, ${duration}${tokens}]`)}\n`, ); if (span.statusCode === "2" && span.statusMessage) { - process.stdout.write(`${indent} ${color.red(span.statusMessage)}\n`); + textChunks.push(`${indent} ${color.red(span.statusMessage)}\n`); } for (const child of span.children ?? []) { - printSpan(child, depth + 1); + textChunks.push(renderSpan(child, depth + 1) + "\n"); } + + return textChunks.join("").replace(/\n$/, ""); } export default defineTelemetryCommand({ @@ -67,7 +71,7 @@ export default defineTelemetryCommand({ ], async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const format = settings.output; const { startTime, endTime } = resolveTimeRange(flags, 1); const reqDTO = { @@ -79,14 +83,11 @@ export default defineTelemetryCommand({ }; if (settings.dryRun) { - emitResult( - { - api: GET_TRACE_API, - data: { reqDTO }, - ...telemetryGatewayPreview(settings), - }, - format, - ); + ctx.output.result({ + api: GET_TRACE_API, + data: { reqDTO }, + ...telemetryGatewayPreview(settings), + }); return; } @@ -101,15 +102,18 @@ export default defineTelemetryCommand({ ); } - if (format === "json") { - emitResult(trace, format); - return; - } + let text: string | undefined; + if (format === "text") { + const textChunks: string[] = []; + + const color = ansi(process.stdout); + textChunks.push( + `${color.bold("Trace")} ${trace.traceId} ${color.dim(formatDateTime(trace.startTime))}\n\n`, + ); + textChunks.push(renderSpan(trace, 0) + "\n"); - const color = ansi(process.stdout); - process.stdout.write( - `${color.bold("Trace")} ${trace.traceId} ${color.dim(formatDateTime(trace.startTime))}\n\n`, - ); - printSpan(trace, 0); + text = textChunks.join("").replace(/\n$/, ""); + } + ctx.output.result(trace, { text }); }, }); diff --git a/packages/commands/src/commands/log/trace-list.ts b/packages/commands/src/commands/log/trace-list.ts index 493b710e0..0971560b8 100644 --- a/packages/commands/src/commands/log/trace-list.ts +++ b/packages/commands/src/commands/log/trace-list.ts @@ -1,6 +1,5 @@ -import { detectOutputFormat } from "bailian-cli-core"; import { defineTelemetryCommand, telemetryGatewayPreview } from "../shared/telemetry.ts"; -import { ansi, emitResult, renderBoxTable } from "bailian-cli-runtime"; +import { ansi, renderBoxTable } from "bailian-cli-runtime"; import { formatNumber, formatDateTime } from "../shared/format.ts"; import { TELEMETRY_LOG_TIME_FLAGS, @@ -41,12 +40,14 @@ interface TraceListPayload { list?: TraceEntry[]; } -function printTraceTable(list: TraceEntry[]): void { +function renderTraceTable(list: TraceEntry[]): string { + const textChunks: string[] = []; + const color = ansi(process.stdout); if (list.length === 0) { - process.stdout.write("No traces found in this range.\n"); - return; + textChunks.push("No traces found in this range.\n"); + return textChunks.join("").replace(/\n$/, ""); } const lines = renderBoxTable({ @@ -69,7 +70,9 @@ function printTraceTable(list: TraceEntry[]): void { return undefined; }, }); - for (const line of lines) process.stdout.write(line + "\n"); + for (const line of lines) textChunks.push(line + "\n"); + + return textChunks.join("").replace(/\n$/, ""); } export default defineTelemetryCommand({ @@ -123,7 +126,7 @@ export default defineTelemetryCommand({ ], async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const format = settings.output; const { startTime, endTime } = resolveTimeRange(flags, 1 / 24); const reqDTO = { @@ -138,14 +141,11 @@ export default defineTelemetryCommand({ }; if (settings.dryRun) { - emitResult( - { - api: LIST_TRACES_API, - data: { reqDTO }, - ...telemetryGatewayPreview(settings), - }, - format, - ); + ctx.output.result({ + api: LIST_TRACES_API, + data: { reqDTO }, + ...telemetryGatewayPreview(settings), + }); return; } @@ -156,11 +156,14 @@ export default defineTelemetryCommand({ const list = Array.isArray(payload) ? payload : (payload?.list ?? []); const totalCount = Array.isArray(payload) ? undefined : payload?.totalCount; - if (format === "json") { - emitResult({ totalCount, list }, format); - return; - } + let text: string | undefined; + if (format === "text") { + const textChunks: string[] = []; + + textChunks.push(renderTraceTable(list) + "\n"); - printTraceTable(list); + text = textChunks.join("").replace(/\n$/, ""); + } + ctx.output.result({ totalCount, list }, { text }); }, }); diff --git a/packages/commands/src/commands/log/trace-stats.ts b/packages/commands/src/commands/log/trace-stats.ts index 3fb465dd8..4d0862344 100644 --- a/packages/commands/src/commands/log/trace-stats.ts +++ b/packages/commands/src/commands/log/trace-stats.ts @@ -1,6 +1,5 @@ -import { detectOutputFormat } from "bailian-cli-core"; import { defineTelemetryCommand, telemetryGatewayPreview } from "../shared/telemetry.ts"; -import { emitResult, renderBoxTable } from "bailian-cli-runtime"; +import { renderBoxTable } from "bailian-cli-runtime"; import { formatNumber, formatDateTime } from "../shared/format.ts"; import { parseCommaList } from "../shared/params.ts"; import { @@ -24,6 +23,32 @@ interface TraceStatistic { avgUserFirstTokenDuration?: string; } +function renderStatistics(items: TraceStatistic[]): string { + const textChunks: string[] = []; + + if (items.length === 0) { + textChunks.push("No trace statistics found in this range.\n"); + return textChunks.join("").replace(/\n$/, ""); + } + + const lines = renderBoxTable({ + headers: ["Resource", "Calls", "Total Tokens", "Avg Duration", "Avg First Token"], + rows: items.map((item) => [ + item.resourceId ?? "-", + formatNumber(Number(item.callCount ?? 0)), + formatNumber(Number(item.totalTokens ?? 0)), + item.avgDuration ? `${formatNumber(Math.round(Number(item.avgDuration)))} ms` : "-", + item.avgLlmFirstTokenDuration + ? `${formatNumber(Math.round(Number(item.avgLlmFirstTokenDuration)))} ms` + : "-", + ]), + align: ["left", "right", "right", "right", "right"], + }); + for (const line of lines) textChunks.push(line + "\n"); + + return textChunks.join("").replace(/\n$/, ""); +} + export default defineTelemetryCommand({ description: { "en-US": "Show per-resource trace statistics (calls, tokens, latency)", @@ -54,7 +79,7 @@ export default defineTelemetryCommand({ exampleArgs: ["", "--resource-id qwen3.6-plus --hours 24", "--resource-type app --output json"], async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const format = settings.output; const { startTime, endTime } = resolveTimeRange(flags, 1 / 24); const reqDTO = { @@ -68,14 +93,11 @@ export default defineTelemetryCommand({ }; if (settings.dryRun) { - emitResult( - { - api: TRACE_STATISTIC_API, - data: { reqDTO }, - ...telemetryGatewayPreview(settings), - }, - format, - ); + ctx.output.result({ + api: TRACE_STATISTIC_API, + data: { reqDTO }, + ...telemetryGatewayPreview(settings), + }); return; } @@ -84,35 +106,20 @@ export default defineTelemetryCommand({ // Keyed by resource id. const items = Object.values(resp) as TraceStatistic[]; - if (format === "json") { - emitResult( - { - period: { start: formatDateTime(startTime), end: formatDateTime(endTime) }, - list: items, - }, - format, - ); - return; - } + let text: string | undefined; + if (format === "text") { + const textChunks: string[] = []; - if (items.length === 0) { - process.stdout.write("No trace statistics found in this range.\n"); - return; - } + textChunks.push(renderStatistics(items) + "\n"); - const lines = renderBoxTable({ - headers: ["Resource", "Calls", "Total Tokens", "Avg Duration", "Avg First Token"], - rows: items.map((item) => [ - item.resourceId ?? "-", - formatNumber(Number(item.callCount ?? 0)), - formatNumber(Number(item.totalTokens ?? 0)), - item.avgDuration ? `${formatNumber(Math.round(Number(item.avgDuration)))} ms` : "-", - item.avgLlmFirstTokenDuration - ? `${formatNumber(Math.round(Number(item.avgLlmFirstTokenDuration)))} ms` - : "-", - ]), - align: ["left", "right", "right", "right", "right"], - }); - for (const line of lines) process.stdout.write(line + "\n"); + text = textChunks.join("").replace(/\n$/, ""); + } + ctx.output.result( + { + period: { start: formatDateTime(startTime), end: formatDateTime(endTime) }, + list: items, + }, + { text }, + ); }, }); diff --git a/packages/commands/src/commands/managed-agent/_engine/api-helpers.ts b/packages/commands/src/commands/managed-agent/_engine/api-helpers.ts index 9fe3bd829..7db961f87 100644 --- a/packages/commands/src/commands/managed-agent/_engine/api-helpers.ts +++ b/packages/commands/src/commands/managed-agent/_engine/api-helpers.ts @@ -1,5 +1,5 @@ -import type { FlagsDef } from "bailian-cli-core"; -import { emitBare, emitResult, formatTable } from "bailian-cli-runtime"; +import type { CommandContext, FlagsDef } from "bailian-cli-core"; +import { formatTable } from "bailian-cli-runtime"; import type { PagedResult } from "./pagination.ts"; export const API_TARGET_FLAGS = { @@ -141,20 +141,21 @@ export async function searchCursorPages( }; } -export function emitCollection(options: { - format: "json" | "text"; - key: string; - items: T[]; - headers: string[]; - rows: string[][]; - hasMore?: boolean; - nextPage?: string; - truncated?: boolean; - scannedPages?: number; - emptyMessage?: string; -}): void { +export function emitCollection( + ctx: Pick, + options: { + key: string; + items: T[]; + headers: string[]; + rows: string[][]; + hasMore?: boolean; + nextPage?: string; + truncated?: boolean; + scannedPages?: number; + emptyMessage?: string; + }, +): void { const { - format, key, items, headers, @@ -163,30 +164,38 @@ export function emitCollection(options: { nextPage, truncated, scannedPages, - emptyMessage = "No resources found.", + emptyMessage = ctx.localize({ "en-US": "No resources found.", "zh-CN": "没有找到资源。" }), } = options; - if (format === "json") { - emitResult( - { - [key]: items, - has_more: hasMore, - next_page: nextPage, - ...(truncated === undefined ? {} : { truncated }), - ...(scannedPages === undefined ? {} : { scanned_pages: scannedPages }), - }, - format, - ); - return; - } - if (items.length === 0) { - emitBare(emptyMessage); - return; - } - for (const line of formatTable(headers, rows)) emitBare(line); - emitBare(`\nTotal: ${items.length}`); - if (truncated) emitBare("Search stopped at --page-limit; more pages remain."); - else if (hasMore) - emitBare(`More results are available.${nextPage ? ` Next page: ${nextPage}` : ""}`); + const data = { + [key]: items, + has_more: hasMore, + next_page: nextPage, + ...(truncated === undefined ? {} : { truncated }), + ...(scannedPages === undefined ? {} : { scanned_pages: scannedPages }), + }; + const lines = + items.length === 0 + ? [emptyMessage] + : [ + ...formatTable(headers, rows), + ctx.localize({ "en-US": `\nTotal: ${items.length}`, "zh-CN": `\n总计:${items.length}` }), + ...(truncated + ? [ + ctx.localize({ + "en-US": "Search stopped at --page-limit; more pages remain.", + "zh-CN": "搜索已在 --page-limit 处停止,仍有更多分页。", + }), + ] + : hasMore + ? [ + ctx.localize({ + "en-US": `More results are available.${nextPage ? ` Next page: ${nextPage}` : ""}`, + "zh-CN": `还有更多结果。${nextPage ? ` 下一页:${nextPage}` : ""}`, + }), + ] + : []), + ]; + ctx.output.result(data, { text: lines.join("\n") }); } export function displayValue(value: unknown, maxLength = 40): string { diff --git a/packages/commands/src/commands/managed-agent/_engine/config-loader.ts b/packages/commands/src/commands/managed-agent/_engine/config-loader.ts index 89185df7d..7085bb45d 100644 --- a/packages/commands/src/commands/managed-agent/_engine/config-loader.ts +++ b/packages/commands/src/commands/managed-agent/_engine/config-loader.ts @@ -13,8 +13,9 @@ import { prepareProviderEnv, scrubCredentialEnv, } from "./credentials.ts"; +import { withStdoutProtected } from "./console-capture.ts"; import { loadFileState } from "./file-state-manager.ts"; -import { type HostContext, installSdkTransport } from "./transport.ts"; +import { type HostContext } from "./transport.ts"; export { CREDENTIALS_NOTE, OFFLINE_NOTE } from "./credentials.ts"; @@ -51,22 +52,24 @@ interface AgentConfigOptions { * (offline commands pass `credentials: "none"` to skip the check) */ export async function resolveAgentProjectConfig( - host: CredentialHost, + host: CredentialHost & HostContext, filePath: string, options: AgentConfigOptions = {}, ): Promise { - prepareProviderEnv(); - const resolved = await resolveProjectConfig(filePath, options); - normalizeInterpolatedProviderBlocks(resolved.config.providers); - injectProviderCredentials(resolved.config.providers, host, { - overrideBaseUrl: options.overrideBailianBaseUrl, + return withStdoutProtected(host, async () => { + prepareProviderEnv(); + const resolved = await resolveProjectConfig(filePath, options); + normalizeInterpolatedProviderBlocks(resolved.config.providers); + injectProviderCredentials(resolved.config.providers, host, { + overrideBaseUrl: options.overrideBailianBaseUrl, + }); + scrubCredentialEnv(); + assertBailianOnlyProviders(resolved.config.providers); + if ((options.credentials ?? "all") !== "none") { + assertProviderCredentials(resolved.config.providers); + } + return resolved; }); - scrubCredentialEnv(); - assertBailianOnlyProviders(resolved.config.providers); - if ((options.credentials ?? "all") !== "none") { - assertProviderCredentials(resolved.config.providers); - } - return resolved; } /** @@ -82,7 +85,6 @@ export async function buildAgentRuntime( filePath: string, options: AgentConfigOptions = {}, ): Promise { - installSdkTransport(host); const { config, configPath, projectName } = await resolveAgentProjectConfig( host, filePath, diff --git a/packages/commands/src/commands/managed-agent/_engine/console-capture.ts b/packages/commands/src/commands/managed-agent/_engine/console-capture.ts index aab32e194..77828d1f3 100644 --- a/packages/commands/src/commands/managed-agent/_engine/console-capture.ts +++ b/packages/commands/src/commands/managed-agent/_engine/console-capture.ts @@ -1,23 +1,45 @@ -/** - * Redirect `console.log` / `console.info` to stderr while `fn` runs. - * - * The OpenAgentPack SDK's provider adapters emit progress/debug logging via - * `console.log` (e.g. `[skill-upload]`), which would corrupt bl's stdout data - * channel in `--output json` mode. Wrapping SDK calls that may log keeps stdout - * a clean data channel. Restores the originals on completion. - */ -export async function withStdoutProtected(fn: () => Promise): Promise { - const originalLog = console.log; - const originalInfo = console.info; - const toStderr = (...args: unknown[]): void => { - process.stderr.write(`${args.map((arg) => String(arg)).join(" ")}\n`); - }; - console.log = toStderr; - console.info = toStderr; - try { - return await fn(); - } finally { - console.log = originalLog; - console.info = originalInfo; +import { sdkOutput, withSdkInvocation, writeSdkDiagnostic, type HostContext } from "./transport.ts"; + +type ConsoleMethod = "log" | "info" | "debug" | "warn" | "error"; +const methods: readonly ConsoleMethod[] = ["log", "info", "debug", "warn", "error"]; +const routers = new Map void>(); + +/** The SDK has no logger seam and logs complete file responses. Keep only safe summaries. */ +function installSdkConsoleRouting(): void { + for (const method of methods) { + if (console[method] === routers.get(method)) continue; + const previous = console[method]; + const route = (...arguments_: unknown[]): void => { + const output = sdkOutput(); + if (!output) { + previous.apply(console, arguments_); + return; + } + const first = arguments_[0]; + const component = + typeof first === "string" + ? (/^\[(agent-skill|skill-upload|agents)\]/u.exec(first)?.[1] ?? "sdk") + : "sdk"; + writeSdkDiagnostic(() => { + const diagnostic = { + code: "AGENT_SDK_DIAGNOSTIC", + message: { "en-US": "Managed Agent SDK diagnostic", "zh-CN": "托管 Agent SDK 诊断" }, + details: { component, method, argument_count: arguments_.length }, + }; + if (method === "warn" || method === "error") output.warning(diagnostic); + else output.debug(diagnostic); + }); + }; + routers.set(method, route); + console[method] = route; } } + +/** Context routing also covers SDK callbacks scheduled before this promise completes. */ +export function withStdoutProtected( + host: HostContext, + run: () => Promise, +): Promise { + installSdkConsoleRouting(); + return withSdkInvocation(host, run); +} diff --git a/packages/commands/src/commands/managed-agent/_engine/feedback.ts b/packages/commands/src/commands/managed-agent/_engine/feedback.ts index 1cf876d3b..baf6c39ba 100644 --- a/packages/commands/src/commands/managed-agent/_engine/feedback.ts +++ b/packages/commands/src/commands/managed-agent/_engine/feedback.ts @@ -1,10 +1,15 @@ import type { RuntimeFeedbackEvent } from "@openagentpack/sdk"; +import type { CommandOutput } from "bailian-cli-core"; +import { writeSdkDiagnostic } from "./transport.ts"; -/** - * Render SDK runtime feedback to stderr, keeping stdout a clean data channel. - * Used as the `onFeedback` sink for plan/apply so progress messages don't mix - * with structured output. - */ -export function renderAgentFeedback(event: RuntimeFeedbackEvent): void { - process.stderr.write(`${event.message}\n`); +export function renderAgentFeedback(output: CommandOutput, event: RuntimeFeedbackEvent): void { + writeSdkDiagnostic(() => { + if (event.level === "warning" || event.level === "error") { + output.warning({ + code: "AGENT_RUNTIME_FEEDBACK", + message: event.message, + details: { type: event.type, resource: event.resource }, + }); + } else output.info(event.message); + }); } diff --git a/packages/commands/src/commands/managed-agent/_engine/managed-api-context.ts b/packages/commands/src/commands/managed-agent/_engine/managed-api-context.ts index a5a168ab7..f80abf794 100644 --- a/packages/commands/src/commands/managed-agent/_engine/managed-api-context.ts +++ b/packages/commands/src/commands/managed-agent/_engine/managed-api-context.ts @@ -45,7 +45,7 @@ export function buildManagedApiContext(host: ManagedApiHost) { ); } const baseUrl = resolveManagedApiBaseUrl(host); - installSdkTransport(host); + installSdkTransport(); return createManagedApiContext({ providers: { bailian: { diff --git a/packages/commands/src/commands/managed-agent/_engine/playground-launcher.ts b/packages/commands/src/commands/managed-agent/_engine/playground-launcher.ts index 0d585d3df..bed01418c 100644 --- a/packages/commands/src/commands/managed-agent/_engine/playground-launcher.ts +++ b/packages/commands/src/commands/managed-agent/_engine/playground-launcher.ts @@ -5,8 +5,15 @@ import { existsSync, readFileSync } from "node:fs"; import { createRequire } from "node:module"; import { dirname, resolve } from "node:path"; import { promisify } from "node:util"; -import { BailianError, type Client, ExitCode, type Settings } from "bailian-cli-core"; -import { emitBare } from "bailian-cli-runtime"; +import { + BailianError, + type Client, + ExitCode, + UsageError, + type CommandOutput, + type LocalizedText, + type Settings, +} from "bailian-cli-core"; const PLAYGROUND_PACKAGE = "@openagentpack/playground"; const execFileAsync = promisify(execFile); @@ -22,6 +29,8 @@ export interface PlaygroundLaunchOptions { surface: "preview" | "workbench"; client: Client; settings: Settings; + output: CommandOutput; + localize: (message: LocalizedText) => string; } interface Launcher { @@ -50,6 +59,18 @@ export interface PlaygroundBrowserTarget { export async function launchManagedAgentPlayground( options: PlaygroundLaunchOptions, ): Promise { + if (options.settings.output === "json") { + throw new UsageError( + options.localize({ + "en-US": "Managed Agent Workbench requires text output for its interactive process.", + "zh-CN": "托管 Agent Workbench 的交互进程需要 text 输出模式。", + }), + options.localize({ + "en-US": "Use --output text to launch Workbench, or --dry-run for a JSON preview.", + "zh-CN": "使用 --output text 启动 Workbench,或使用 --dry-run 获取 JSON 预览。", + }), + ); + } assertSupportedNodeVersion(); const port = options.port ?? DEFAULT_PORT; if (!Number.isInteger(port) || port <= 0 || port > 65_535) { @@ -66,11 +87,19 @@ export async function launchManagedAgentPlayground( existing.projectId === projectId && (launcher.version === undefined || existing.version === launcher.version); if (reusable) { - emitBare(`Workbench already running at http://localhost:${port} (pid ${existing.pid}).`); - await openPlaygroundSurface(port, options.surface, options.agent, options.open); + options.output.result( + { url: `http://localhost:${port}`, port, surface: options.surface, pid: existing.pid }, + { + text: options.localize({ + "en-US": `Workbench already running at http://localhost:${port} (pid ${existing.pid}).`, + "zh-CN": `Workbench 已在 http://localhost:${port} 运行(pid ${existing.pid})。`, + }), + }, + ); + await openPlaygroundSurface(port, options.surface, options.agent, options.open, options); return; } - const released = await replaceExistingPlayground(existing, port); + const released = await replaceExistingPlayground(existing, port, options); if (!released) { throw new BailianError( `Could not stop the existing Workbench process (pid ${existing.pid}) on port ${port}.`, @@ -82,8 +111,13 @@ export async function launchManagedAgentPlayground( const environment = buildPlaygroundEnvironment(options, port, sourcePath); if (launcher.fetched) { - emitBare(`Fetching ${PLAYGROUND_PACKAGE} (first run may take a moment)...`); + options.output.info({ + "en-US": `Fetching ${PLAYGROUND_PACKAGE} (first run may take a moment)...`, + "zh-CN": `正在获取 ${PLAYGROUND_PACKAGE}(首次运行可能需要片刻)...`, + }); } + // This native interactive sidecar keeps its terminal protocol in text mode. + // JSON was rejected before launcher resolution or any process/browser action. const child = spawn(launcher.command, launcher.args, { env: environment, stdio: ["inherit", "pipe", "inherit"], @@ -97,8 +131,16 @@ export async function launchManagedAgentPlayground( ExitCode.GENERAL, ); } - emitBare(`Workbench ready at http://localhost:${readyPort}`); - await openPlaygroundSurface(readyPort, options.surface, options.agent, options.open); + options.output.result( + { url: `http://localhost:${readyPort}`, port: readyPort, surface: options.surface }, + { + text: options.localize({ + "en-US": `Workbench ready at http://localhost:${readyPort}`, + "zh-CN": `Workbench 已就绪:http://localhost:${readyPort}`, + }), + }, + ); + await openPlaygroundSurface(readyPort, options.surface, options.agent, options.open, options); const exitCode = await waitForChildExit(child); if (exitCode !== 0) { throw new BailianError(`Workbench exited with code ${exitCode}.`, ExitCode.GENERAL); @@ -349,8 +391,12 @@ async function probeExistingPlayground(port: number): Promise, ): Promise { - emitBare(`Replacing Workbench v${existing.version} (pid ${existing.pid}) on port ${port}...`); + options.output.info({ + "en-US": `Replacing Workbench v${existing.version} (pid ${existing.pid}) on port ${port}...`, + "zh-CN": `正在替换端口 ${port} 上的 Workbench v${existing.version}(pid ${existing.pid})...`, + }); try { process.kill(existing.pid, "SIGTERM"); } catch { @@ -368,14 +414,23 @@ async function openPlaygroundSurface( surface: "preview" | "workbench", requestedAgent: string | undefined, shouldOpen: boolean, + options: Pick, ): Promise { if (!shouldOpen) return; const target = surface === "workbench" ? { url: `http://localhost:${port}` } : await resolvePlaygroundBrowserTarget(port, requestedAgent); - if (target.warning) emitBare(`Warning: ${target.warning}`); - openBrowser(target.url); + if (target.warning) + options.output.warning({ + message: { + "en-US": target.warning, + "zh-CN": requestedAgent?.trim() + ? `未找到 Agent '${requestedAgent.trim()}'。将打开项目 Workbench。` + : "此项目声明了多个 Agent。将打开 Workbench;使用 --agent 重新运行以打开 Preview。", + }, + }); + openBrowser(target.url, options); } async function resolvePlaygroundBrowserTarget( @@ -398,7 +453,7 @@ async function resolvePlaygroundBrowserTarget( } } -function openBrowser(url: string): void { +function openBrowser(url: string, options: Pick): void { const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open"; const args = process.platform === "win32" ? ["", url] : [url]; @@ -409,7 +464,12 @@ function openBrowser(url: string): void { shell: process.platform === "win32", }).unref(); } catch { - emitBare(`Could not open a browser automatically. Visit ${url}`); + options.output.warning({ + message: { + "en-US": `Could not open a browser automatically. Visit ${url}`, + "zh-CN": `无法自动打开浏览器。请访问 ${url}`, + }, + }); } } diff --git a/packages/commands/src/commands/managed-agent/_engine/scoped-create.ts b/packages/commands/src/commands/managed-agent/_engine/scoped-create.ts index 692d5ab47..4cd06f454 100644 --- a/packages/commands/src/commands/managed-agent/_engine/scoped-create.ts +++ b/packages/commands/src/commands/managed-agent/_engine/scoped-create.ts @@ -12,8 +12,7 @@ import { resolveProjectConfigFromObject, syncProjectResourcesWithStateBackend, } from "@openagentpack/sdk"; -import { BailianError, type CommandContext, detectOutputFormat, ExitCode } from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; +import { BailianError, type CommandContext, ExitCode } from "bailian-cli-core"; import { parseDocument } from "yaml"; import { formatResourceLabel } from "./address-utils.ts"; import { resolveAgentProjectConfig } from "./config-loader.ts"; @@ -21,9 +20,13 @@ import { assertProviderCredentials } from "./credentials.ts"; import { withStdoutProtected } from "./console-capture.ts"; import { retainAgentError, withAgentErrors } from "./errors.ts"; import { createFileStateScope } from "./file-state-manager.ts"; +import { renderAgentFeedback } from "./feedback.ts"; import { installSdkTransport } from "./transport.ts"; -export type ScopedCreateHost = Pick; +export type ScopedCreateHost = Pick< + CommandContext, + "client" | "identity" | "settings" | "output" | "localize" +>; export type ScopedCreateGroup = | "agents" | "environments" @@ -160,7 +163,7 @@ export async function loadScopedCreateProject( } const provider = "bailian"; if (!host.settings.dryRun) assertProviderCredentials(loaded.config.providers, [provider]); - installSdkTransport(host); + installSdkTransport(); const stateBackend = new LocalFileStateBackend({ configPath: loaded.configPath }); return { ...loaded, @@ -199,7 +202,6 @@ export async function resolveCandidateDeclaration(options: { export async function runScopedTopLevelCreate(input: ScopedTopLevelCreateInput): Promise { const { project, host } = input; - const format = detectOutputFormat(host.settings.output); const keySelection = await project.stateBackend.read(project.stateScope, (state) => selectResourceKey({ displayName: input.displayName, @@ -243,13 +245,14 @@ export async function runScopedTopLevelCreate(input: ScopedTopLevelCreateInput): if (host.settings.dryRun || !input.yes) { const planned = await withAgentErrors(() => - withStdoutProtected(() => + withStdoutProtected(host, () => planProjectWithStateBackend(backendInput, { provider: project.provider, scope: { roots: [root] }, mode: "create-only", refresh: !host.settings.dryRun, - quiet: format === "json", + quiet: false, + onFeedback: (event) => renderAgentFeedback(host.output, event), }), ), ); @@ -271,16 +274,25 @@ export async function runScopedTopLevelCreate(input: ScopedTopLevelCreateInput): actions: planned.plan.actions, diagnostics: planned.plan.diagnostics, }; - if (format === "json") emitResult(result, format); - else { - emitBare(`Generated YAML key: ${resourceKey}`); - renderPlan(planned.plan.actions, planned.plan.diagnostics); - emitBare( - host.settings.dryRun - ? "Dry run: YAML, State, and remote resources were not changed." - : "Preview only: re-run with --yes to write YAML and create this resource.", - ); - } + const text = [ + host.localize({ + "en-US": `Generated YAML key: ${resourceKey}`, + "zh-CN": `生成的 YAML key:${resourceKey}`, + }), + renderPlan(planned.plan.actions, planned.plan.diagnostics), + host.settings.dryRun + ? host.localize({ + "en-US": "Dry run: YAML, State, and remote resources were not changed.", + "zh-CN": "Dry run:YAML、State 和远端资源均未修改。", + }) + : host.localize({ + "en-US": "Preview only: re-run with --yes to write YAML and create this resource.", + "zh-CN": "仅预览:使用 --yes 重新运行以写入 YAML 并创建此资源。", + }), + ] + .filter((line) => line !== "") + .join("\n"); + host.output.result(result, { text }); return; } @@ -288,13 +300,14 @@ export async function runScopedTopLevelCreate(input: ScopedTopLevelCreateInput): let run: ResourceSyncRun; try { run = await withAgentErrors(() => - withStdoutProtected(() => + withStdoutProtected(host, () => syncProjectResourcesWithStateBackend(backendInput, { provider: project.provider, scope: { roots: [root] }, mode: "create-only", refresh: true, - quiet: format === "json", + quiet: false, + onFeedback: (event) => renderAgentFeedback(host.output, event), policy: "block", }), ), @@ -328,11 +341,20 @@ export async function runScopedTopLevelCreate(input: ScopedTopLevelCreateInput): results, error: failed?.error, }; - if (format === "json") emitResult(result, format); - else { - emitBare(`Wrote ${project.configPath} with ${input.resourceType} key '${resourceKey}'.`); - if (!failed) emitBare(`Created ${input.resourceType} '${input.displayName}'.`); - } + const text = [ + host.localize({ + "en-US": `Wrote ${project.configPath} with ${input.resourceType} key '${resourceKey}'.`, + "zh-CN": `已写入 ${project.configPath},${input.resourceType} key 为 '${resourceKey}'。`, + }), + ]; + if (!failed) + text.push( + host.localize({ + "en-US": `Created ${input.resourceType} '${input.displayName}'.`, + "zh-CN": `已创建 ${input.resourceType} '${input.displayName}'。`, + }), + ); + host.output.result(result, { text: text.join("\n") }); if (failed) { throw new BailianError( failed.error ?? `Scoped ${input.resourceType} create failed.`, @@ -411,12 +433,13 @@ export async function parseJsonInputs( function renderPlan( actions: Array<{ action: string; address: ResourceAddress }>, diagnostics: Array<{ severity: string; code: string; message: string }>, -): void { - for (const diagnostic of diagnostics) { - emitBare(`[${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`); - } +): string { + const lines = diagnostics.map( + (diagnostic) => `[${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`, + ); for (const action of actions.filter((entry) => entry.action !== "no-op")) { const icon = action.action === "create" ? "+" : action.action === "update" ? "~" : "-"; - emitBare(` ${icon} ${formatResourceLabel(action.address)}`); + lines.push(` ${icon} ${formatResourceLabel(action.address)}`); } + return lines.join("\n"); } diff --git a/packages/commands/src/commands/managed-agent/_engine/session-render.ts b/packages/commands/src/commands/managed-agent/_engine/session-render.ts index 0f3f61925..4f642fb33 100644 --- a/packages/commands/src/commands/managed-agent/_engine/session-render.ts +++ b/packages/commands/src/commands/managed-agent/_engine/session-render.ts @@ -4,16 +4,16 @@ import { type ProviderSessionEvent, } from "@openagentpack/sdk"; import { sanitizeSessionEvents } from "@openagentpack/sdk/session-events"; -import { BailianError, ExitCode } from "bailian-cli-core"; +import { BailianError, ExitCode, type CommandContext } from "bailian-cli-core"; /** Skip user echo + thinking noise in live rendering (mirrors OpenAgentPack CLI). */ function shouldRenderLiveEvent(event: ProviderSessionEvent): boolean { return event.type !== "thinking" && !(event.type === "message" && event.role === "user"); } -function renderTerminalStatus(status: string, json: boolean): void { - if (json) return; - process.stderr.write(`\n[session ${status}]\n`); +type RenderHost = Pick; +function renderTerminalStatus(ctx: RenderHost, status: string): void { + ctx.output.info({ "en-US": `\n[session ${status}]`, "zh-CN": `\n[会话 ${status}]` }); } function findLastSessionError(events: readonly ProviderSessionEvent[]): string | undefined { @@ -24,9 +24,12 @@ function findLastSessionError(events: readonly ProviderSessionEvent[]): string | return undefined; } -function throwIfSessionFailed(status: string | undefined, message?: string): void { +function throwIfSessionFailed(ctx: RenderHost, status: string | undefined, message?: string): void { if (status !== "failed") return; - throw new BailianError(message ?? "Session failed.", ExitCode.GENERAL); + throw new BailianError( + message ?? ctx.localize({ "en-US": "Session failed.", "zh-CN": "会话失败。" }), + ExitCode.GENERAL, + ); } /** @@ -49,73 +52,85 @@ export interface SessionRenderContext { * prefixes the envelope with the session identity. */ export async function streamAndRenderEvents( + ctx: RenderHost, events: AsyncIterable, - json: boolean, context: SessionRenderContext = {}, ): Promise { const collected: ProviderSessionEvent[] = []; let terminalStatus: string | undefined; let errorMessage: string | undefined; - for await (const event of events) { - if (json) collected.push(event); - else renderEvent(event); - if (event.type === "error" && event.content?.trim()) errorMessage = event.content; - if (event.type === "status" && isTerminalSessionStatus(event.status)) { - terminalStatus = event.status; - renderTerminalStatus(event.status ?? "", json); - break; + try { + for await (const event of events) { + collected.push(event); + renderEvent(ctx, event); + if (event.type === "error" && event.content?.trim()) errorMessage = event.content; + if (event.type === "status" && isTerminalSessionStatus(event.status)) { + terminalStatus = event.status; + renderTerminalStatus(ctx, event.status ?? ""); + break; + } } + } catch (error) { + if (context.session_id) + ctx.output.warning({ + code: "AGENT_SESSION_INTERRUPTED", + message: { + "en-US": `Session ${context.session_id} was created; reading its events was interrupted.`, + "zh-CN": `会话 ${context.session_id} 已创建,读取事件时中断。`, + }, + details: context, + }); + throw error; } - if (json) { - process.stdout.write( - `${JSON.stringify({ ...context, events: sanitizeSessionEvents(collected) }, null, 2)}\n`, - ); - } - throwIfSessionFailed(terminalStatus, errorMessage); + ctx.output.result({ ...context, events: sanitizeSessionEvents(collected) }, { text: "" }); + throwIfSessionFailed(ctx, terminalStatus, errorMessage); } /** Assistant text → stdout (data channel); everything else → stderr (diagnostics). */ -function renderEvent(event: ProviderSessionEvent): void { +function renderEvent(ctx: RenderHost, event: ProviderSessionEvent): void { if (!shouldRenderLiveEvent(event)) return; if (event.type === "message" && event.content) { - process.stdout.write(event.content); + if (ctx.settings.output === "text") process.stdout.write(event.content); } else if (event.type === "tool_use") { - process.stderr.write(`\n[tool] ${event.tool_name}\n`); + ctx.output.info({ + "en-US": `\n[tool] ${event.tool_name}`, + "zh-CN": `\n[工具] ${event.tool_name}`, + }); } else if (event.type === "tool_result" && event.content) { const preview = event.content.length > 200 ? `${event.content.slice(0, 200)}...` : event.content; - process.stderr.write(`${preview}\n`); + ctx.output.info(preview); } else if (event.type === "status") { - if (event.status === "running") process.stderr.write("\n[session running]\n"); + if (event.status === "running") renderTerminalStatus(ctx, "running"); } else if (event.type === "error") { - process.stderr.write(`\n[error] ${event.content ?? "unknown error"}\n`); + ctx.output.warning({ + code: "AGENT_SESSION_EVENT_ERROR", + message: event.content ?? { + "en-US": "Unknown session error", + "zh-CN": "未知会话错误", + }, + }); } } /** Render a polled (non-streaming) collected result. `context` prefixes the JSON envelope. */ export function renderCollectedEvents( + ctx: RenderHost, result: CollectedSessionEvents, - json: boolean, context: SessionRenderContext = {}, ): void { - if (json) { - process.stdout.write( - `${JSON.stringify( - { - ...context, - events: sanitizeSessionEvents(result.result.events), - has_more: result.result.has_more, - next_page: result.result.next_page, - }, - null, - 2, - )}\n`, - ); - } else { - for (const event of result.result.events) renderEvent(event); - renderTerminalStatus(result.terminalStatus, json); - } - throwIfSessionFailed(result.terminalStatus, findLastSessionError(result.result.events)); + for (const event of result.result.events) renderEvent(ctx, event); + renderTerminalStatus(ctx, result.terminalStatus); + ctx.output.result( + { + ...context, + events: sanitizeSessionEvents(result.result.events), + has_more: result.result.has_more, + next_page: result.result.next_page, + }, + { text: "" }, + ); + throwIfSessionFailed(ctx, result.terminalStatus, findLastSessionError(result.result.events)); } /** Split a comma-separated --memory-stores value. */ diff --git a/packages/commands/src/commands/managed-agent/_engine/transport.ts b/packages/commands/src/commands/managed-agent/_engine/transport.ts index 1d99e5ced..fefc7fdf4 100644 --- a/packages/commands/src/commands/managed-agent/_engine/transport.ts +++ b/packages/commands/src/commands/managed-agent/_engine/transport.ts @@ -1,29 +1,57 @@ +import { AsyncLocalStorage } from "node:async_hooks"; import * as sdk from "@openagentpack/sdk"; import { createInstrumentedFetch, + httpTransport, + type CommandOutput, type FetchImplementation, type Identity, type Settings, } from "bailian-cli-core"; -/** The slice of CommandContext the transport wrapper needs (UA identity + verbose). */ +/** Invocation-owned SDK diagnostics and transport; never retain the first CLI host. */ export interface HostContext { identity: Identity; settings: Settings; + output: CommandOutput; } -let installed = false; +interface SdkInvocation { + output: CommandOutput; + fetch: FetchImplementation; +} +const invocations = new AsyncLocalStorage(); + +export function sdkOutput(): CommandOutput | undefined { + return invocations.getStore()?.output; +} + +/** Third-party diagnostics must never turn a successful SDK operation into a failure. */ +export function writeSdkDiagnostic(write: () => void): void { + try { + write(); + } catch { + /* The business operation owns success and failure. */ + } +} + +const routedFetch: FetchImplementation = (input, init) => + (invocations.getStore()?.fetch ?? httpTransport.fetch)(input, init); + +export function installSdkTransport(): void { + sdk.setDefaultFetch(routedFetch); +} -/** - * Route the SDK's provider-client requests through the CLI's instrumented - * fetch (UA, host-gated tracking headers, --verbose logging). Feature-detected: - * `setDefaultFetch` landed after @openagentpack/sdk 0.1.0 — on older versions - * this is a silent no-op and the SDK keeps using the global fetch as before. - */ -export function installSdkTransport(host: HostContext): void { - if (installed) return; - const setDefaultFetch = (sdk as Record).setDefaultFetch; - if (typeof setDefaultFetch !== "function") return; - (setDefaultFetch as (fetchImpl: FetchImplementation) => void)(createInstrumentedFetch(host)); - installed = true; +export function withSdkInvocation( + host: HostContext, + run: () => Promise, +): Promise { + installSdkTransport(); + const output = host.output; + const fetch = createInstrumentedFetch({ + identity: host.identity, + settings: host.settings, + debug: (message) => writeSdkDiagnostic(() => output.debug(message)), + }); + return invocations.run({ output, fetch }, run); } diff --git a/packages/commands/src/commands/managed-agent/agent/create.ts b/packages/commands/src/commands/managed-agent/agent/create.ts index 479269794..7bea70414 100644 --- a/packages/commands/src/commands/managed-agent/agent/create.ts +++ b/packages/commands/src/commands/managed-agent/agent/create.ts @@ -13,19 +13,13 @@ import { type SkillDecl, syncProjectResourcesWithStateBackend, } from "@openagentpack/sdk"; -import { - BailianError, - defineCommand, - detectOutputFormat, - ExitCode, - type FlagsDef, -} from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; +import { BailianError, defineCommand, ExitCode, type FlagsDef } from "bailian-cli-core"; import { parseDocument } from "yaml"; import { formatResourceLabel } from "../_engine/address-utils.ts"; import { CREDENTIALS_NOTE } from "../_engine/config-loader.ts"; import { withStdoutProtected } from "../_engine/console-capture.ts"; import { retainAgentError, withAgentErrors } from "../_engine/errors.ts"; +import { renderAgentFeedback } from "../_engine/feedback.ts"; import { type LoadedScopedCreateProject, loadScopedCreateProject, @@ -455,7 +449,6 @@ export default defineCommand({ }, async run(ctx) { const { flags, settings } = ctx; - const format = detectOutputFormat(settings.output); const file = flags.file ?? "agents.yaml"; const project = await loadScopedCreateProject(ctx, file); const provider = project.provider; @@ -547,12 +540,13 @@ export default defineCommand({ if (settings.dryRun || !flags.yes) { const planned = await withAgentErrors(() => - withStdoutProtected(() => + withStdoutProtected(ctx, () => planProjectWithStateBackend(backendInput, { provider, scope: { roots }, refresh: !settings.dryRun, - quiet: format === "json", + quiet: false, + onFeedback: (event) => renderAgentFeedback(ctx.output, event), mode: "create-only", }), ), @@ -583,26 +577,38 @@ export default defineCommand({ actions: planned.plan.actions, diagnostics: planned.plan.diagnostics, }; - if (format === "json") { - emitResult(result, format); - } else { - for (const skill of localSkills) { - emitBare(`Generated Skill YAML key: ${skill.key}`); - } - emitBare(`Generated Agent YAML key: ${agentKey}`); - for (const diagnostic of planned.plan.diagnostics) { - emitBare(`[${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`); - } - for (const action of planned.plan.actions.filter((entry) => entry.action !== "no-op")) { - const icon = action.action === "create" ? "+" : action.action === "update" ? "~" : "-"; - emitBare(` ${icon} ${formatResourceLabel(action.address)}`); - } - emitBare( - settings.dryRun - ? "Dry run: YAML, State, and remote resources were not changed." - : "Preview only: re-run with --yes to write YAML and create this Agent.", - ); - } + ctx.output.result(result, { + text: + ctx.settings.output === "text" + ? (() => { + const lines: string[] = []; + { + for (const skill of localSkills) { + lines.push(`Generated Skill YAML key: ${skill.key}`); + } + lines.push(`Generated Agent YAML key: ${agentKey}`); + for (const diagnostic of planned.plan.diagnostics) { + lines.push( + `[${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`, + ); + } + for (const action of planned.plan.actions.filter( + (entry) => entry.action !== "no-op", + )) { + const icon = + action.action === "create" ? "+" : action.action === "update" ? "~" : "-"; + lines.push(` ${icon} ${formatResourceLabel(action.address)}`); + } + lines.push( + settings.dryRun + ? "Dry run: YAML, State, and remote resources were not changed." + : "Preview only: re-run with --yes to write YAML and create this Agent.", + ); + } + return lines.join("\n"); + })() + : undefined, + }); return; } @@ -610,12 +616,13 @@ export default defineCommand({ let run: ResourceSyncRun; try { run = await withAgentErrors(() => - withStdoutProtected(() => + withStdoutProtected(ctx, () => syncProjectResourcesWithStateBackend(backendInput, { provider, scope: { roots }, refresh: true, - quiet: format === "json", + quiet: false, + onFeedback: (event) => renderAgentFeedback(ctx.output, event), mode: "create-only", policy: "block", }), @@ -666,14 +673,22 @@ export default defineCommand({ results, error: failed?.error, }; - if (format === "json") emitResult(result, format); - else { - emitBare( - `Wrote ${project.configPath} with Agent key '${agentKey}'${localSkills.length ? ` and ${localSkills.length} local Skill declaration(s)` : ""}.`, - ); - if (status === "completed") emitBare(`Created Agent '${flags.name.trim()}'.`); - else emitBare(`Scoped create ${status}: ${failed?.error ?? "unknown error"}`); - } + ctx.output.result(result, { + text: + ctx.settings.output === "text" + ? (() => { + const lines: string[] = []; + { + lines.push( + `Wrote ${project.configPath} with Agent key '${agentKey}'${localSkills.length ? ` and ${localSkills.length} local Skill declaration(s)` : ""}.`, + ); + if (status === "completed") lines.push(`Created Agent '${flags.name.trim()}'.`); + else lines.push(`Scoped create ${status}: ${failed?.error ?? "unknown error"}`); + } + return lines.join("\n"); + })() + : undefined, + }); if (failed) { throw new BailianError( failed.error ?? "Scoped Agent create failed.", diff --git a/packages/commands/src/commands/managed-agent/agent/get.ts b/packages/commands/src/commands/managed-agent/agent/get.ts index e7dc50971..e14ffce5e 100644 --- a/packages/commands/src/commands/managed-agent/agent/get.ts +++ b/packages/commands/src/commands/managed-agent/agent/get.ts @@ -1,6 +1,5 @@ import { getRemoteAgent } from "@openagentpack/sdk"; -import { defineCommand, detectOutputFormat } from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; +import { defineCommand } from "bailian-cli-core"; import { displayValue } from "../_engine/api-helpers.ts"; import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts"; import { withStdoutProtected } from "../_engine/console-capture.ts"; @@ -20,9 +19,8 @@ export default defineCommand({ ? "--agent-version must be a positive integer." : undefined, async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); const agent = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); return getRemoteAgent(runtime, ctx.flags.agentId, { provider: "bailian", @@ -30,16 +28,21 @@ export default defineCommand({ }); }), ); - if (format === "json") { - emitResult(agent, format); - return; - } - emitBare(`ID: ${agent.id}`); - emitBare(`Name: ${displayValue(agent.name)}`); - emitBare(`Description: ${displayValue(agent.description, 120)}`); - emitBare(`Version: ${displayValue(agent.version)}`); - emitBare(`Type: ${displayValue(agent.type)}`); - emitBare(`Created: ${displayValue(agent.created_at)}`); - emitBare(`Updated: ${displayValue(agent.updated_at)}`); + ctx.output.result(agent, { + text: + ctx.settings.output === "text" + ? (() => { + const lines: string[] = []; + lines.push(`ID: ${agent.id}`); + lines.push(`Name: ${displayValue(agent.name)}`); + lines.push(`Description: ${displayValue(agent.description, 120)}`); + lines.push(`Version: ${displayValue(agent.version)}`); + lines.push(`Type: ${displayValue(agent.type)}`); + lines.push(`Created: ${displayValue(agent.created_at)}`); + lines.push(`Updated: ${displayValue(agent.updated_at)}`); + return lines.join("\n"); + })() + : undefined, + }); }, }); diff --git a/packages/commands/src/commands/managed-agent/agent/list.ts b/packages/commands/src/commands/managed-agent/agent/list.ts index 0bda7cd98..c6631e68b 100644 --- a/packages/commands/src/commands/managed-agent/agent/list.ts +++ b/packages/commands/src/commands/managed-agent/agent/list.ts @@ -1,5 +1,5 @@ import { listRemoteAgents } from "@openagentpack/sdk"; -import { defineCommand, detectOutputFormat } from "bailian-cli-core"; +import { defineCommand } from "bailian-cli-core"; import { emitCollection, validateLimitAndPageLimit } from "../_engine/api-helpers.ts"; import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts"; import { withStdoutProtected } from "../_engine/console-capture.ts"; @@ -16,9 +16,8 @@ export default defineCommand({ notes: CREDENTIALS_NOTE, validate: validateLimitAndPageLimit, async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); const result = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); return fetchAllPages( async (page) => { @@ -39,8 +38,7 @@ export default defineCommand({ ); }), ); - emitCollection({ - format, + emitCollection(ctx, { key: "agents", items: result.items, headers: ["ID", "NAME", "VERSION", "TYPE", "UPDATED"], diff --git a/packages/commands/src/commands/managed-agent/agent/search.ts b/packages/commands/src/commands/managed-agent/agent/search.ts index ff7f2436b..27edbfb39 100644 --- a/packages/commands/src/commands/managed-agent/agent/search.ts +++ b/packages/commands/src/commands/managed-agent/agent/search.ts @@ -1,5 +1,5 @@ import { listRemoteAgents } from "@openagentpack/sdk"; -import { defineCommand, detectOutputFormat } from "bailian-cli-core"; +import { defineCommand } from "bailian-cli-core"; import { emitCollection, matchesQuery, @@ -20,9 +20,8 @@ export default defineCommand({ notes: CREDENTIALS_NOTE, validate: validateLimitAndPageLimit, async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); const result = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); return searchCursorPages( async (page) => { @@ -43,8 +42,7 @@ export default defineCommand({ ); }), ); - emitCollection({ - format, + emitCollection(ctx, { key: "agents", items: result.items, headers: ["ID", "NAME", "VERSION", "TYPE", "UPDATED"], diff --git a/packages/commands/src/commands/managed-agent/agent/versions.ts b/packages/commands/src/commands/managed-agent/agent/versions.ts index 974bbf06a..b08c97ddc 100644 --- a/packages/commands/src/commands/managed-agent/agent/versions.ts +++ b/packages/commands/src/commands/managed-agent/agent/versions.ts @@ -1,5 +1,5 @@ import { listRemoteAgentVersions } from "@openagentpack/sdk"; -import { defineCommand, detectOutputFormat } from "bailian-cli-core"; +import { defineCommand } from "bailian-cli-core"; import { emitCollection, validateLimitAndPageLimit } from "../_engine/api-helpers.ts"; import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts"; import { withStdoutProtected } from "../_engine/console-capture.ts"; @@ -16,9 +16,8 @@ export default defineCommand({ notes: CREDENTIALS_NOTE, validate: validateLimitAndPageLimit, async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); const result = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); return fetchAllPages( async (page) => { @@ -38,8 +37,7 @@ export default defineCommand({ ); }), ); - emitCollection({ - format, + emitCollection(ctx, { key: "versions", items: result.items, headers: ["ID", "NAME", "VERSION", "TYPE", "UPDATED"], diff --git a/packages/commands/src/commands/managed-agent/apply.ts b/packages/commands/src/commands/managed-agent/apply.ts index 4d4a940ae..7a775d971 100644 --- a/packages/commands/src/commands/managed-agent/apply.ts +++ b/packages/commands/src/commands/managed-agent/apply.ts @@ -1,11 +1,4 @@ -import { - BailianError, - defineCommand, - detectOutputFormat, - ExitCode, - type FlagsDef, -} from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; +import { BailianError, defineCommand, ExitCode, type FlagsDef } from "bailian-cli-core"; import { executePlannedProject, planProjectContext } from "@openagentpack/sdk"; import { formatResourceLabel } from "./_engine/address-utils.ts"; import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts"; @@ -59,50 +52,50 @@ export default defineCommand({ notes: CREDENTIALS_NOTE, async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const file = flags.file ?? "agents.yaml"; if (settings.dryRun) { - emitResult( - { - would_apply: { - provider: "bailian", - refresh: !flags.noRefresh, - concurrency: flags.concurrency, - }, - config_file: file, - hint: "Run `managed-agent plan` to preview the exact resource changes.", + ctx.output.result({ + would_apply: { + provider: "bailian", + refresh: !flags.noRefresh, + concurrency: flags.concurrency, }, - format, - ); + config_file: file, + hint: "Run `managed-agent plan` to preview the exact resource changes.", + }); return; } const planned = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, file); return planProjectContext(runtime, { provider: "bailian", refresh: !flags.noRefresh, - quiet: true, - onFeedback: renderAgentFeedback, + quiet: false, + onFeedback: (event) => renderAgentFeedback(ctx.output, event), }); }), ); const plan = planned.plan; - // In --output json, stdout must stay a single-JSON data channel: diagnostics - // and the action preview are progress info → stderr; text mode keeps stdout. - const emitProgress = (line: string): void => { - if (format === "json") process.stderr.write(`${line}\n`); - else emitBare(line); - }; + const emitProgress = (line: string): void => ctx.output.info(line); const errorDiagnostics = plan.diagnostics.filter( (diagnostic) => diagnostic.severity === "error", ); if (errorDiagnostics.length > 0) { for (const diagnostic of errorDiagnostics) { - emitProgress(`[error] ${diagnostic.code}: ${diagnostic.message}`); + ctx.output.warning({ + message: `[error] ${diagnostic.code}: ${diagnostic.message}`, + code: "AGENT_PLAN_DIAGNOSTIC", + details: { + severity: diagnostic.severity, + code: diagnostic.code, + resource: diagnostic.resource, + }, + }); } throw new BailianError( formatAgentDiagnosticFailure( @@ -115,9 +108,15 @@ export default defineCommand({ const actionable = plan.actions.filter((action) => action.action !== "no-op"); if (actionable.length === 0) { - if (format === "json") - emitResult({ succeeded: 0, failed: 0, skipped: 0, results: [] }, format); - else emitBare("No changes. Infrastructure is up-to-date."); + ctx.output.result( + { succeeded: 0, failed: 0, skipped: 0, results: [] }, + { + text: + ctx.settings.output === "text" + ? "No changes. Infrastructure is up-to-date." + : undefined, + }, + ); return; } @@ -127,9 +126,9 @@ export default defineCommand({ } const result = await withAgentErrors(() => - withStdoutProtected(() => + withStdoutProtected(ctx, () => executePlannedProject(planned, { - onFeedback: renderAgentFeedback, + onFeedback: (event) => renderAgentFeedback(ctx.output, event), policy: "force", concurrency: flags.concurrency, }), @@ -141,11 +140,15 @@ export default defineCommand({ const failed = failedResults.length; const skipped = result.results.filter((entry) => entry.status === "skipped").length; - if (format === "json") { - emitResult({ succeeded, failed, skipped, results: result.results }, format); - } else { - emitBare(`\nApply finished: ${succeeded} succeeded, ${failed} failed, ${skipped} skipped.`); - } + ctx.output.result( + { succeeded, failed, skipped, results: result.results }, + { + text: + ctx.settings.output === "text" + ? `\nApply finished: ${succeeded} succeeded, ${failed} failed, ${skipped} skipped.` + : undefined, + }, + ); if (failed > 0) { const firstFailure = failedResults.find((entry) => entry.error); diff --git a/packages/commands/src/commands/managed-agent/deployment/_set-paused.ts b/packages/commands/src/commands/managed-agent/deployment/_set-paused.ts index 97e926dd3..c05194976 100644 --- a/packages/commands/src/commands/managed-agent/deployment/_set-paused.ts +++ b/packages/commands/src/commands/managed-agent/deployment/_set-paused.ts @@ -1,6 +1,5 @@ import { setRemoteDeploymentPaused } from "@openagentpack/sdk"; -import { defineCommand, detectOutputFormat } from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; +import { defineCommand } from "bailian-cli-core"; import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts"; import { withStdoutProtected } from "../_engine/console-capture.ts"; import { withAgentErrors } from "../_engine/errors.ts"; @@ -37,20 +36,16 @@ export function createSetDeploymentPausedCommand(paused: boolean) { notes: CREDENTIALS_NOTE, validate: validateDeploymentActionTarget, async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); if (ctx.settings.dryRun) { - emitResult( - { - [paused ? "would_pause_deployment" : "would_unpause_deployment"]: - ctx.flags.deploymentId ?? ctx.flags.deployment, - target_kind: ctx.flags.deploymentId ? "id" : "state_name", - }, - format, - ); + ctx.output.result({ + [paused ? "would_pause_deployment" : "would_unpause_deployment"]: + ctx.flags.deploymentId ?? ctx.flags.deployment, + target_kind: ctx.flags.deploymentId ? "id" : "state_name", + }); return; } const result = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); const deploymentId = await resolveDeploymentTarget(runtime, ctx.flags); return { @@ -61,11 +56,15 @@ export function createSetDeploymentPausedCommand(paused: boolean) { }; }), ); - if (format === "json") { - emitResult({ deployment_id: result.deploymentId, deployment: result.deployment }, format); - } else { - emitBare(`Deployment ${result.deploymentId} ${paused ? "paused" : "unpaused"}.`); - } + ctx.output.result( + { deployment_id: result.deploymentId, deployment: result.deployment }, + { + text: + ctx.settings.output === "text" + ? `Deployment ${result.deploymentId} ${paused ? "paused" : "unpaused"}.` + : undefined, + }, + ); }, }); } diff --git a/packages/commands/src/commands/managed-agent/deployment/get.ts b/packages/commands/src/commands/managed-agent/deployment/get.ts index fd2841984..f5d666c3a 100644 --- a/packages/commands/src/commands/managed-agent/deployment/get.ts +++ b/packages/commands/src/commands/managed-agent/deployment/get.ts @@ -1,6 +1,5 @@ import { getRemoteDeployment } from "@openagentpack/sdk"; -import { defineCommand, detectOutputFormat } from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; +import { defineCommand } from "bailian-cli-core"; import { displayValue } from "../_engine/api-helpers.ts"; import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts"; import { withStdoutProtected } from "../_engine/console-capture.ts"; @@ -18,21 +17,28 @@ export default defineCommand({ exampleArgs: ["--deployment-id dep_abc"], notes: CREDENTIALS_NOTE, async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); const deployment = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); return getRemoteDeployment(runtime, ctx.flags.deploymentId, { provider: "bailian", }); }), ); - if (format === "json") emitResult(deployment, format); - else { - emitBare(`ID: ${displayValue(deployment.id)}`); - emitBare(`Status: ${deployment.status}`); - emitBare(`Schedule:${displayValue(deployment.schedule?.expression)}`); - emitBare(`Paused: ${displayValue(deployment.paused_reason)}`); - } + ctx.output.result(deployment, { + text: + ctx.settings.output === "text" + ? (() => { + const lines: string[] = []; + { + lines.push(`ID: ${displayValue(deployment.id)}`); + lines.push(`Status: ${deployment.status}`); + lines.push(`Schedule:${displayValue(deployment.schedule?.expression)}`); + lines.push(`Paused: ${displayValue(deployment.paused_reason)}`); + } + return lines.join("\n"); + })() + : undefined, + }); }, }); diff --git a/packages/commands/src/commands/managed-agent/deployment/list.ts b/packages/commands/src/commands/managed-agent/deployment/list.ts index 0ec8c9cb8..3eabd75b8 100644 --- a/packages/commands/src/commands/managed-agent/deployment/list.ts +++ b/packages/commands/src/commands/managed-agent/deployment/list.ts @@ -1,5 +1,5 @@ import { listRemoteDeployments } from "@openagentpack/sdk"; -import { defineCommand, detectOutputFormat } from "bailian-cli-core"; +import { defineCommand } from "bailian-cli-core"; import { emitCollection, validateLimitAndPageLimit } from "../_engine/api-helpers.ts"; import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts"; import { withStdoutProtected } from "../_engine/console-capture.ts"; @@ -16,9 +16,8 @@ export default defineCommand({ notes: CREDENTIALS_NOTE, validate: validateLimitAndPageLimit, async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); const result = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); return fetchAllPages( async (page) => { @@ -43,8 +42,7 @@ export default defineCommand({ ); }), ); - emitCollection({ - format, + emitCollection(ctx, { key: "deployments", items: result.items, headers: ["ID", "STATUS", "SCHEDULE", "PAUSED REASON"], diff --git a/packages/commands/src/commands/managed-agent/deployment/run.ts b/packages/commands/src/commands/managed-agent/deployment/run.ts index 6f4af5a52..9e9b4b21f 100644 --- a/packages/commands/src/commands/managed-agent/deployment/run.ts +++ b/packages/commands/src/commands/managed-agent/deployment/run.ts @@ -1,6 +1,5 @@ import { runRemoteDeployment } from "@openagentpack/sdk"; -import { defineCommand, detectOutputFormat } from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; +import { defineCommand } from "bailian-cli-core"; import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts"; import { withStdoutProtected } from "../_engine/console-capture.ts"; import { withAgentErrors } from "../_engine/errors.ts"; @@ -30,19 +29,15 @@ export default defineCommand({ notes: CREDENTIALS_NOTE, validate: validateDeploymentActionTarget, async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); if (ctx.settings.dryRun) { - emitResult( - { - would_run_deployment: ctx.flags.deploymentId ?? ctx.flags.deployment, - target_kind: ctx.flags.deploymentId ? "id" : "state_name", - }, - format, - ); + ctx.output.result({ + would_run_deployment: ctx.flags.deploymentId ?? ctx.flags.deployment, + target_kind: ctx.flags.deploymentId ? "id" : "state_name", + }); return; } const result = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); const deploymentId = await resolveDeploymentTarget(runtime, ctx.flags); return { @@ -51,8 +46,14 @@ export default defineCommand({ }; }), ); - if (format === "json") - emitResult({ deployment_id: result.deploymentId, ...result.run }, format); - else emitBare(`Deployment ${result.deploymentId} started. Run: ${result.run.run_id ?? "-"}`); + ctx.output.result( + { deployment_id: result.deploymentId, ...result.run }, + { + text: + ctx.settings.output === "text" + ? `Deployment ${result.deploymentId} started. Run: ${result.run.run_id ?? "-"}` + : undefined, + }, + ); }, }); diff --git a/packages/commands/src/commands/managed-agent/deployment/runs/get.ts b/packages/commands/src/commands/managed-agent/deployment/runs/get.ts index 2b52512e7..2505130c5 100644 --- a/packages/commands/src/commands/managed-agent/deployment/runs/get.ts +++ b/packages/commands/src/commands/managed-agent/deployment/runs/get.ts @@ -1,6 +1,5 @@ import { getRemoteDeploymentRun } from "@openagentpack/sdk"; -import { defineCommand, detectOutputFormat } from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; +import { defineCommand } from "bailian-cli-core"; import { displayValue } from "../../_engine/api-helpers.ts"; import { buildAgentRuntime, CREDENTIALS_NOTE } from "../../_engine/config-loader.ts"; import { withStdoutProtected } from "../../_engine/console-capture.ts"; @@ -18,21 +17,28 @@ export default defineCommand({ exampleArgs: ["--run-id run_abc"], notes: CREDENTIALS_NOTE, async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); const run = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); return getRemoteDeploymentRun(runtime, ctx.flags.runId, { provider: "bailian" }); }), ); - if (format === "json") emitResult(run, format); - else { - emitBare(`ID: ${run.id}`); - emitBare(`Deployment: ${displayValue(run.deployment_id)}`); - emitBare(`Session: ${displayValue(run.session_id)}`); - emitBare(`Status: ${displayValue(run.status)}`); - emitBare(`Created: ${displayValue(run.created_at)}`); - emitBare(`Error: ${displayValue(run.error)}`); - } + ctx.output.result(run, { + text: + ctx.settings.output === "text" + ? (() => { + const lines: string[] = []; + { + lines.push(`ID: ${run.id}`); + lines.push(`Deployment: ${displayValue(run.deployment_id)}`); + lines.push(`Session: ${displayValue(run.session_id)}`); + lines.push(`Status: ${displayValue(run.status)}`); + lines.push(`Created: ${displayValue(run.created_at)}`); + lines.push(`Error: ${displayValue(run.error)}`); + } + return lines.join("\n"); + })() + : undefined, + }); }, }); diff --git a/packages/commands/src/commands/managed-agent/deployment/runs/list.ts b/packages/commands/src/commands/managed-agent/deployment/runs/list.ts index de9ac1367..12d3f20ed 100644 --- a/packages/commands/src/commands/managed-agent/deployment/runs/list.ts +++ b/packages/commands/src/commands/managed-agent/deployment/runs/list.ts @@ -1,5 +1,5 @@ import { listRemoteDeploymentRuns } from "@openagentpack/sdk"; -import { defineCommand, detectOutputFormat } from "bailian-cli-core"; +import { defineCommand } from "bailian-cli-core"; import { emitCollection, validateLimitAndPageLimit } from "../../_engine/api-helpers.ts"; import { buildAgentRuntime, CREDENTIALS_NOTE } from "../../_engine/config-loader.ts"; import { withStdoutProtected } from "../../_engine/console-capture.ts"; @@ -19,9 +19,8 @@ export default defineCommand({ notes: CREDENTIALS_NOTE, validate: validateLimitAndPageLimit, async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); const result = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); return fetchAllPages( async (page) => { @@ -41,8 +40,7 @@ export default defineCommand({ ); }), ); - emitCollection({ - format, + emitCollection(ctx, { key: "runs", items: result.items, headers: ["ID", "DEPLOYMENT", "SESSION", "STATUS", "CREATED"], diff --git a/packages/commands/src/commands/managed-agent/deployment/search.ts b/packages/commands/src/commands/managed-agent/deployment/search.ts index d25e9549e..520fde674 100644 --- a/packages/commands/src/commands/managed-agent/deployment/search.ts +++ b/packages/commands/src/commands/managed-agent/deployment/search.ts @@ -1,5 +1,5 @@ import { listRemoteDeployments } from "@openagentpack/sdk"; -import { defineCommand, detectOutputFormat } from "bailian-cli-core"; +import { defineCommand } from "bailian-cli-core"; import { emitCollection, validateLimitAndPageLimit } from "../_engine/api-helpers.ts"; import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts"; import { withStdoutProtected } from "../_engine/console-capture.ts"; @@ -25,9 +25,8 @@ export default defineCommand({ ], validate: validateLimitAndPageLimit, async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); const result = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); return fetchAllPages( async (page) => { @@ -53,8 +52,7 @@ export default defineCommand({ ); }), ); - emitCollection({ - format, + emitCollection(ctx, { key: "deployments", items: result.items, headers: ["ID", "STATUS", "SCHEDULE", "PAUSED REASON"], diff --git a/packages/commands/src/commands/managed-agent/destroy.ts b/packages/commands/src/commands/managed-agent/destroy.ts index 87eb8b5ee..4bbf44b3a 100644 --- a/packages/commands/src/commands/managed-agent/destroy.ts +++ b/packages/commands/src/commands/managed-agent/destroy.ts @@ -1,11 +1,4 @@ -import { - BailianError, - defineCommand, - detectOutputFormat, - ExitCode, - type FlagsDef, -} from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; +import { BailianError, defineCommand, ExitCode, type FlagsDef } from "bailian-cli-core"; import { destroyPlannedProjectResources, planDestroyProjectContext } from "@openagentpack/sdk"; import { formatResourceLabel } from "./_engine/address-utils.ts"; import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts"; @@ -50,23 +43,20 @@ export default defineCommand({ notes: CREDENTIALS_NOTE, async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const file = flags.file ?? "agents.yaml"; if (settings.dryRun) { - emitResult( - { - would_destroy: { cascade: Boolean(flags.cascade) }, - config_file: file, - hint: "Run `managed-agent state list` to see the resources tracked in state.", - }, - format, - ); + ctx.output.result({ + would_destroy: { cascade: Boolean(flags.cascade) }, + config_file: file, + hint: "Run `managed-agent state list` to see the resources tracked in state.", + }); return; } const planned = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, file); const planned = planDestroyProjectContext(runtime); return { @@ -83,38 +73,59 @@ export default defineCommand({ const resources = planned.resources; if (resources.length === 0) { - if (format === "json") emitResult({ destroyed: 0, total: 0 }, format); - else emitBare("No resources in state. Nothing to destroy."); + ctx.output.result( + { destroyed: 0, total: 0 }, + { + text: + ctx.settings.output === "text" + ? "No resources in state. Nothing to destroy." + : undefined, + }, + ); return; } - // In --output json, stdout must stay a single-JSON data channel: the - // resource preview is progress info → stderr; text mode keeps stdout. + // Resource previews are process hints; the final summary owns stdout. for (const resource of resources) { const line = ` - ${formatResourceLabel(resource.address)} [${resource.remote_id}]`; - if (format === "json") process.stderr.write(`${line}\n`); - else emitBare(line); + ctx.output.info(line); } const result = await withAgentErrors(() => - withStdoutProtected(() => + withStdoutProtected(ctx, () => destroyPlannedProjectResources(planned, { cascade: flags.cascade, onCascadeRequired: async () => Boolean(flags.cascade), onResourceResult: (item) => { const label = formatResourceLabel(item.resource.address); - process.stderr.write(` ${item.status === "success" ? "✓" : "✗"} ${label}\n`); + if (item.status === "success") ctx.output.info(` ✓ ${label}`); + else + ctx.output.warning({ + code: "AGENT_DESTROY_RESOURCE_FAILED", + message: item.error ?? { + "en-US": `Could not remove ${label}.`, + "zh-CN": `无法移除 ${label}。`, + }, + details: { + status: item.status, + address: item.resource.address, + remote_id: item.resource.remote_id, + }, + }); }, }), ), ); - if (format === "json") { - emitResult({ destroyed: result.destroyed, total: result.resources.length }, format); - } else { - const status = result.partial ? "Destroy incomplete" : "Destroy complete"; - emitBare(`\n${status}. ${result.destroyed}/${result.resources.length} resources removed.`); - } + ctx.output.result( + { destroyed: result.destroyed, total: result.resources.length }, + { + text: + ctx.settings.output === "text" + ? `\n${result.partial ? "Destroy incomplete" : "Destroy complete"}. ${result.destroyed}/${result.resources.length} resources removed.` + : undefined, + }, + ); if (result.partial) { const firstFailure = result.results.find((item) => item.status !== "success"); diff --git a/packages/commands/src/commands/managed-agent/environment/get.ts b/packages/commands/src/commands/managed-agent/environment/get.ts index 67898770d..41ec876fd 100644 --- a/packages/commands/src/commands/managed-agent/environment/get.ts +++ b/packages/commands/src/commands/managed-agent/environment/get.ts @@ -1,6 +1,5 @@ import { getRemoteEnvironment } from "@openagentpack/sdk"; -import { defineCommand, detectOutputFormat } from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; +import { defineCommand } from "bailian-cli-core"; import { displayValue } from "../_engine/api-helpers.ts"; import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts"; import { withStdoutProtected } from "../_engine/console-capture.ts"; @@ -18,24 +17,28 @@ export default defineCommand({ exampleArgs: ["--environment-id env_abc"], notes: CREDENTIALS_NOTE, async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); const environment = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); return getRemoteEnvironment(runtime, ctx.flags.environmentId, { provider: "bailian", }); }), ); - if (format === "json") { - emitResult(environment, format); - return; - } - emitBare(`ID: ${environment.id}`); - emitBare(`Name: ${displayValue(environment.name)}`); - emitBare(`Description: ${displayValue(environment.description, 120)}`); - emitBare(`Scope: ${displayValue(environment.scope)}`); - emitBare(`Version: ${displayValue(environment.version)}`); - emitBare(`Updated: ${displayValue(environment.updated_at)}`); + ctx.output.result(environment, { + text: + ctx.settings.output === "text" + ? (() => { + const lines: string[] = []; + lines.push(`ID: ${environment.id}`); + lines.push(`Name: ${displayValue(environment.name)}`); + lines.push(`Description: ${displayValue(environment.description, 120)}`); + lines.push(`Scope: ${displayValue(environment.scope)}`); + lines.push(`Version: ${displayValue(environment.version)}`); + lines.push(`Updated: ${displayValue(environment.updated_at)}`); + return lines.join("\n"); + })() + : undefined, + }); }, }); diff --git a/packages/commands/src/commands/managed-agent/environment/list.ts b/packages/commands/src/commands/managed-agent/environment/list.ts index ebe2d2538..99c67b229 100644 --- a/packages/commands/src/commands/managed-agent/environment/list.ts +++ b/packages/commands/src/commands/managed-agent/environment/list.ts @@ -1,5 +1,5 @@ import { listRemoteEnvironments } from "@openagentpack/sdk"; -import { defineCommand, detectOutputFormat } from "bailian-cli-core"; +import { defineCommand } from "bailian-cli-core"; import { emitCollection, validateLimitAndPageLimit } from "../_engine/api-helpers.ts"; import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts"; import { withStdoutProtected } from "../_engine/console-capture.ts"; @@ -16,9 +16,8 @@ export default defineCommand({ notes: CREDENTIALS_NOTE, validate: validateLimitAndPageLimit, async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); const result = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); return fetchAllPages( async (page) => { @@ -39,8 +38,7 @@ export default defineCommand({ ); }), ); - emitCollection({ - format, + emitCollection(ctx, { key: "environments", items: result.items, headers: ["ID", "NAME", "SCOPE", "VERSION", "UPDATED"], diff --git a/packages/commands/src/commands/managed-agent/environment/search.ts b/packages/commands/src/commands/managed-agent/environment/search.ts index 0ce32de61..cced7a3a5 100644 --- a/packages/commands/src/commands/managed-agent/environment/search.ts +++ b/packages/commands/src/commands/managed-agent/environment/search.ts @@ -1,5 +1,5 @@ import { listRemoteEnvironments } from "@openagentpack/sdk"; -import { defineCommand, detectOutputFormat } from "bailian-cli-core"; +import { defineCommand } from "bailian-cli-core"; import { emitCollection, matchesQuery, @@ -23,9 +23,8 @@ export default defineCommand({ notes: CREDENTIALS_NOTE, validate: validateLimitAndPageLimit, async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); const result = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); return searchCursorPages( async (page) => { @@ -52,8 +51,7 @@ export default defineCommand({ ); }), ); - emitCollection({ - format, + emitCollection(ctx, { key: "environments", items: result.items, headers: ["ID", "NAME", "SCOPE", "VERSION", "UPDATED"], diff --git a/packages/commands/src/commands/managed-agent/file.ts b/packages/commands/src/commands/managed-agent/file.ts index df4a93573..072d12a2c 100644 --- a/packages/commands/src/commands/managed-agent/file.ts +++ b/packages/commands/src/commands/managed-agent/file.ts @@ -7,8 +7,7 @@ import { listRemoteFiles, uploadFile, } from "@openagentpack/sdk"; -import { defineCommand, detectOutputFormat } from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; +import { defineCommand } from "bailian-cli-core"; import { API_TARGET_FLAGS, CURSOR_FLAGS, @@ -106,21 +105,17 @@ export const managedAgentFileUpload = defineCommand({ exampleArgs: ["--path ./report.pdf"], notes: CREDENTIALS_NOTE, async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); if (ctx.settings.dryRun) { - emitResult( - { - would_upload_file: ctx.flags.path, - filename: ctx.flags.filename ?? basename(ctx.flags.path), - mime_type: ctx.flags.mimeType ?? inferMimeType(ctx.flags.path), - }, - format, - ); + ctx.output.result({ + would_upload_file: ctx.flags.path, + filename: ctx.flags.filename ?? basename(ctx.flags.path), + mime_type: ctx.flags.mimeType ?? inferMimeType(ctx.flags.path), + }); return; } const content = await readInputFile(ctx.flags.path); const file = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); return uploadFile(runtime, content, ctx.flags.filename ?? basename(ctx.flags.path), { provider: "bailian", @@ -128,8 +123,10 @@ export const managedAgentFileUpload = defineCommand({ }); }), ); - if (format === "json") emitResult(file, format); - else emitBare(`File uploaded: ${file.id} (${file.filename})`); + ctx.output.result(file, { + text: + ctx.settings.output === "text" ? `File uploaded: ${file.id} (${file.filename})` : undefined, + }); }, }); @@ -142,9 +139,8 @@ export const managedAgentFileList = defineCommand({ notes: CREDENTIALS_NOTE, validate: validateLimitAndPageLimit, async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); const result = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); return fetchAllPages( async (page) => { @@ -165,8 +161,7 @@ export const managedAgentFileList = defineCommand({ ); }), ); - emitCollection({ - format, + emitCollection(ctx, { key: "files", items: result.items, headers: ["ID", "FILENAME", "STATUS", "BYTES", "SCOPE", "CREATED"], @@ -186,22 +181,25 @@ export const managedAgentFileGet = defineCommand({ exampleArgs: ["--file-id file_abc"], notes: CREDENTIALS_NOTE, async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); const file = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); return getFileInfo(runtime, ctx.flags.fileId, { provider: "bailian" }); }), ); - if (format === "json") emitResult(file, format); - else { - emitBare(`ID: ${file.id}`); - emitBare(`Filename: ${file.filename}`); - emitBare(`MIME: ${file.mime_type}`); - emitBare(`Bytes: ${file.size_bytes}`); - emitBare(`Status: ${displayValue(file.status)}`); - emitBare(`Scope: ${displayValue(file.scope?.id)}`); - } + ctx.output.result(file, { + text: + ctx.settings.output === "text" + ? [ + `ID: ${file.id}`, + `Filename: ${file.filename}`, + `MIME: ${file.mime_type}`, + `Bytes: ${file.size_bytes}`, + `Status: ${displayValue(file.status)}`, + `Scope: ${displayValue(file.scope?.id)}`, + ].join("\n") + : undefined, + }); }, }); @@ -214,9 +212,8 @@ export const managedAgentFileSearch = defineCommand({ notes: CREDENTIALS_NOTE, validate: validateLimitAndPageLimit, async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); const result = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); return searchCursorPages( async (page) => { @@ -237,8 +234,7 @@ export const managedAgentFileSearch = defineCommand({ ); }), ); - emitCollection({ - format, + emitCollection(ctx, { key: "files", items: result.items, headers: ["ID", "FILENAME", "STATUS", "BYTES", "SCOPE", "CREATED"], @@ -263,24 +259,24 @@ export const managedAgentFileDownload = defineCommand({ exampleArgs: ["--file-id file_abc --output-file ./artifact.pdf"], notes: CREDENTIALS_NOTE, async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); if (ctx.settings.dryRun) { - emitResult( - { would_download_file: ctx.flags.fileId, output_file: ctx.flags.outputFile }, - format, - ); + ctx.output.result({ + would_download_file: ctx.flags.fileId, + output_file: ctx.flags.outputFile, + }); return; } const content = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); return downloadRemoteFile(runtime, ctx.flags.fileId, { provider: "bailian" }); }), ); const outputFile = await writeOutputFile(ctx.flags.outputFile, content, ctx.flags.force); - if (format === "json") - emitResult({ downloaded: ctx.flags.fileId, output_file: outputFile }, format); - else emitBare(`File downloaded to ${outputFile}`); + ctx.output.result( + { downloaded: ctx.flags.fileId, output_file: outputFile }, + { text: ctx.settings.output === "text" ? `File downloaded to ${outputFile}` : undefined }, + ); }, }); @@ -299,18 +295,19 @@ export const managedAgentFileDelete = defineCommand({ exampleArgs: ["--file-id file_abc --dry-run", "--file-id file_abc --yes"], notes: CREDENTIALS_NOTE, async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); if (ctx.settings.dryRun) { - emitResult({ would_delete_file: ctx.flags.fileId }, format); + ctx.output.result({ would_delete_file: ctx.flags.fileId }); return; } await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); await deleteFile(runtime, ctx.flags.fileId, { provider: "bailian" }); }), ); - if (format === "json") emitResult({ deleted: ctx.flags.fileId }, format); - else emitBare(`File ${ctx.flags.fileId} deleted.`); + ctx.output.result( + { deleted: ctx.flags.fileId }, + { text: ctx.settings.output === "text" ? `File ${ctx.flags.fileId} deleted.` : undefined }, + ); }, }); diff --git a/packages/commands/src/commands/managed-agent/init.ts b/packages/commands/src/commands/managed-agent/init.ts index 210cbf23e..f34d94e08 100644 --- a/packages/commands/src/commands/managed-agent/init.ts +++ b/packages/commands/src/commands/managed-agent/init.ts @@ -1,14 +1,7 @@ import { existsSync } from "node:fs"; import { readFile, writeFile } from "node:fs/promises"; import { resolve } from "node:path"; -import { - BailianError, - defineCommand, - detectOutputFormat, - ExitCode, - type FlagsDef, -} from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; +import { BailianError, defineCommand, ExitCode, type FlagsDef } from "bailian-cli-core"; const GITIGNORE_ADDITIONS = ` # agents @@ -82,7 +75,7 @@ export default defineCommand({ exampleArgs: ["", "--agent-name assistant"], async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const agentName = flags.agentName ?? "assistant"; const file = flags.file ?? "agents.yaml"; @@ -102,15 +95,12 @@ export default defineCommand({ const content = await readFile(gitignorePath, "utf8"); wouldUpdateGitignore = !content.includes("agents.state.json"); } - emitResult( - { - would_create: file, - provider: "bailian", - agent: agentName, - would_update_gitignore: wouldUpdateGitignore, - }, - format, - ); + ctx.output.result({ + would_create: file, + provider: "bailian", + agent: agentName, + would_update_gitignore: wouldUpdateGitignore, + }); return; } @@ -126,14 +116,24 @@ export default defineCommand({ await writeFile(gitignorePath, `${GITIGNORE_ADDITIONS.trim()}\n`, "utf8"); } - if (format === "json") { - emitResult({ created: file, provider: "bailian", agent: agentName }, format); - } else { - emitBare(`Created ${resolve(file)}`); - emitBare( - "Credentials: run `bl auth login --api-key --base-url `, or set DASHSCOPE_API_KEY / BAILIAN_BASE_URL.", - ); - emitBare("Next: edit agents.yaml, then run `bl managed-agent plan`."); - } + ctx.output.result( + { created: file, provider: "bailian", agent: agentName }, + { + text: + ctx.settings.output === "text" + ? (() => { + const lines: string[] = []; + { + lines.push(`Created ${resolve(file)}`); + lines.push( + "Credentials: run `bl auth login --api-key --base-url `, or set DASHSCOPE_API_KEY / BAILIAN_BASE_URL.", + ); + lines.push("Next: edit agents.yaml, then run `bl managed-agent plan`."); + } + return lines.join("\n"); + })() + : undefined, + }, + ); }, }); diff --git a/packages/commands/src/commands/managed-agent/memory-store/api.ts b/packages/commands/src/commands/managed-agent/memory-store/api.ts index b5a204769..7dc57a5db 100644 --- a/packages/commands/src/commands/managed-agent/memory-store/api.ts +++ b/packages/commands/src/commands/managed-agent/memory-store/api.ts @@ -13,12 +13,10 @@ import { } from "@openagentpack/sdk"; import { defineCommand, - detectOutputFormat, type CommandRisk, type FlagsDef, type LocalizedText, } from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; import { withStdoutProtected } from "../_engine/console-capture.ts"; import { withAgentErrors } from "../_engine/errors.ts"; import { @@ -209,12 +207,14 @@ async function runOperation( request: Record, invoke: (context: ApiContext) => Promise, ): Promise { - const format = detectOutputFormat(host.settings.output); const result = host.settings.dryRun ? { operation, provider: "bailian", base_url: resolveManagedApiBaseUrl(host), ...request } - : await withAgentErrors(() => withStdoutProtected(() => invoke(buildManagedApiContext(host)))); - if (format === "json") emitResult(result, format); - else emitBare(JSON.stringify(result, null, 2)); + : await withAgentErrors(() => + withStdoutProtected(host, () => invoke(buildManagedApiContext(host))), + ); + host.output.result(result, { + text: host.settings.output === "text" ? JSON.stringify(result, null, 2) : undefined, + }); } export const managedAgentMemoryStoreList = defineCommand({ diff --git a/packages/commands/src/commands/managed-agent/plan.ts b/packages/commands/src/commands/managed-agent/plan.ts index b08039894..1d96b1d04 100644 --- a/packages/commands/src/commands/managed-agent/plan.ts +++ b/packages/commands/src/commands/managed-agent/plan.ts @@ -1,11 +1,4 @@ -import { - BailianError, - defineCommand, - detectOutputFormat, - ExitCode, - type FlagsDef, -} from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; +import { BailianError, defineCommand, ExitCode, type FlagsDef } from "bailian-cli-core"; import { planProjectContext } from "@openagentpack/sdk"; import { formatResourceLabel } from "./_engine/address-utils.ts"; import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts"; @@ -58,7 +51,6 @@ export default defineCommand({ ], async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); const file = flags.file ?? "agents.yaml"; // Offline mode never talks to a provider and never saves refreshed state: // --no-refresh by explicit request, --dry-run by contract (read-only run). @@ -67,15 +59,16 @@ export default defineCommand({ const offline = Boolean(flags.noRefresh) || settings.dryRun; const planned = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, file, { credentials: offline ? "none" : "all", }); return planProjectContext(runtime, { provider: "bailian", refresh: !offline, - quiet: format === "json", - onFeedback: format === "json" ? undefined : renderAgentFeedback, + // SDK quiet also drops warnings before onFeedback; the reporter owns filtering. + quiet: false, + onFeedback: (event) => renderAgentFeedback(ctx.output, event), }); }), ); @@ -84,37 +77,37 @@ export default defineCommand({ const hasErrors = plan.diagnostics.some((diag) => diag.severity === "error"); const failureMessage = formatAgentDiagnosticFailure(plan.diagnostics, "Plan contains errors."); - if (format === "json") { - emitResult(plan, format); - if (hasErrors) throw new BailianError(failureMessage, ExitCode.GENERAL); - return; - } - + const lines: string[] = []; for (const diag of plan.diagnostics) { - emitBare(`[${diag.severity}] ${diag.code}: ${diag.message}`); + lines.push(`[${diag.severity}] ${diag.code}: ${diag.message}`); + } + if (hasErrors) { + ctx.output.result(plan, { text: lines.join("\n") }); + throw new BailianError(failureMessage, ExitCode.GENERAL); } - if (hasErrors) throw new BailianError(failureMessage, ExitCode.GENERAL); const creates = plan.actions.filter((action) => action.action === "create"); const updates = plan.actions.filter((action) => action.action === "update"); const deletes = plan.actions.filter((action) => action.action === "delete"); if (creates.length + updates.length + deletes.length === 0) { - emitBare("No changes. Infrastructure is up-to-date."); - if (flags.refreshOnly) emitBare("Refresh-only mode: no remote mutations were performed."); + lines.push("No changes. Infrastructure is up-to-date."); + if (flags.refreshOnly) lines.push("Refresh-only mode: no remote mutations were performed."); + ctx.output.result(plan, { text: lines.join("\n") }); return; } - emitBare("\nPlanned actions:\n"); - for (const action of creates) emitBare(` + ${formatResourceLabel(action.address)}`); + lines.push("\nPlanned actions:\n"); + for (const action of creates) lines.push(` + ${formatResourceLabel(action.address)}`); for (const action of updates) { - emitBare(` ~ ${formatResourceLabel(action.address)}`); - if (action.reason) emitBare(` ${action.reason}`); + lines.push(` ~ ${formatResourceLabel(action.address)}`); + if (action.reason) lines.push(` ${action.reason}`); } - for (const action of deletes) emitBare(` - ${formatResourceLabel(action.address)}`); - emitBare( + for (const action of deletes) lines.push(` - ${formatResourceLabel(action.address)}`); + lines.push( `\nPlan: ${creates.length} to create, ${updates.length} to update, ${deletes.length} to destroy.`, ); - if (flags.refreshOnly) emitBare("Refresh-only mode: no remote mutations will be performed."); + if (flags.refreshOnly) lines.push("Refresh-only mode: no remote mutations will be performed."); + ctx.output.result(plan, { text: lines.join("\n") }); }, }); diff --git a/packages/commands/src/commands/managed-agent/project.ts b/packages/commands/src/commands/managed-agent/project.ts index 452c6b03e..512ad9232 100644 --- a/packages/commands/src/commands/managed-agent/project.ts +++ b/packages/commands/src/commands/managed-agent/project.ts @@ -1,13 +1,6 @@ import { stat } from "node:fs/promises"; import { join } from "node:path"; -import { - BailianError, - defineCommand, - detectOutputFormat, - ExitCode, - type FlagsDef, -} from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; +import { BailianError, defineCommand, ExitCode, type FlagsDef } from "bailian-cli-core"; import { commitProjectBuild, createDirectoryWorkspaceVersionService, @@ -38,8 +31,6 @@ const PROJECT_FLAG = { }, } satisfies FlagsDef; -const JSON_FORMAT = "json" as const; - export const managedAgentProjectInit = defineCommand({ description: { "en-US": "Create a directory project or convert the local agents.yaml", @@ -74,16 +65,13 @@ export const managedAgentProjectInit = defineCommand({ async run(ctx) { const projectRoot = ctx.flags.project ?? "./managed-agent"; if (ctx.settings.dryRun) { - emitResult( - { - would_initialize_project: projectRoot, - }, - detectOutputFormat(ctx.settings.output), - ); + ctx.output.result({ + would_initialize_project: projectRoot, + }); return; } const result = await initializeDirectoryProject({ projectRoot }); - emitResult(result, detectOutputFormat(ctx.settings.output)); + ctx.output.result(result); }, }); @@ -95,16 +83,24 @@ export const managedAgentProjectValidate = defineCommand({ exampleArgs: ["", "--project ./my-agent"], async run(ctx) { const result = await validateDirectoryProject(ctx.flags.project ?? "."); - const format = detectOutputFormat(ctx.settings.output); - if (format === JSON_FORMAT) emitResult(safeInspection(result), format); - else { - for (const diagnostic of [...result.diagnostics, ...result.warnings]) { - emitBare(`${diagnostic.severity}: ${diagnostic.code}: ${diagnostic.message}`); - } - if (!result.diagnostics.some((diagnostic) => diagnostic.severity === "error")) { - emitBare(`Project is valid (${result.project_revision.slice(0, 12)}).`); - } - } + + ctx.output.result(safeInspection(result), { + text: + ctx.settings.output === "text" + ? (() => { + const lines: string[] = []; + { + for (const diagnostic of [...result.diagnostics, ...result.warnings]) { + lines.push(`${diagnostic.severity}: ${diagnostic.code}: ${diagnostic.message}`); + } + if (!result.diagnostics.some((diagnostic) => diagnostic.severity === "error")) { + lines.push(`Project is valid (${result.project_revision.slice(0, 12)}).`); + } + } + return lines.join("\n"); + })() + : undefined, + }); if (result.diagnostics.some((diagnostic) => diagnostic.severity === "error")) { throw new BailianError("Directory project validation failed.", ExitCode.GENERAL); } @@ -148,9 +144,9 @@ export const managedAgentProjectBuild = defineCommand({ async run(ctx) { const root = ctx.flags.project ?? "."; const preview = await previewProjectBuild(root); - const format = detectOutputFormat(ctx.settings.output); + if (ctx.settings.dryRun) { - emitResult(safeInspection(preview), format); + ctx.output.result(safeInspection(preview)); return; } if (!preview.can_build) @@ -163,10 +159,7 @@ export const managedAgentProjectBuild = defineCommand({ projectRoot: root, baseRevision: preview.project_revision, }); - emitResult( - { manifest: built.manifest, organization_moves: preview.organization_moves }, - format, - ); + ctx.output.result({ manifest: built.manifest, organization_moves: preview.organization_moves }); }, }); @@ -206,7 +199,7 @@ export const managedAgentProjectPublish = defineCommand({ exampleArgs: ["--yes", "--project ./my-agent --yes"], notes: CREDENTIALS_NOTE, async run(ctx) { - installSdkTransport(ctx); + installSdkTransport(); const root = ctx.flags.project ?? "."; const resolveBuild: ProjectBuildResolver = async (buildPath, options) => (await resolveAgentProjectConfig(ctx, buildPath, { @@ -214,34 +207,31 @@ export const managedAgentProjectPublish = defineCommand({ overrideBailianBaseUrl: true, })) as unknown as Awaited>; const planned = await withAgentErrors(() => - withStdoutProtected(() => + withStdoutProtected(ctx, () => planProjectPublish(root, { provider: "bailian", refresh: !ctx.flags.noRefresh, - quiet: true, - onFeedback: renderAgentFeedback, + quiet: false, + onFeedback: (event) => renderAgentFeedback(ctx.output, event), resolveBuild, }), ), ); const actions = planned.planned.plan.actions.filter((action) => action.action !== "no-op"); if (ctx.settings.dryRun) { - emitResult( - { - project_revision: planned.project_revision, - build_manifest: planned.build_manifest, - plan: planned.planned.plan, - }, - detectOutputFormat(ctx.settings.output), - ); + ctx.output.result({ + project_revision: planned.project_revision, + build_manifest: planned.build_manifest, + plan: planned.planned.plan, + }); return; } for (const action of actions) { const marker = action.action === "create" ? "+" : action.action === "update" ? "~" : "-"; - emitBare(`${marker} ${formatResourceLabel(action.address)}`); + ctx.output.info(`${marker} ${formatResourceLabel(action.address)}`); } const result = await withAgentErrors(() => - withStdoutProtected(() => + withStdoutProtected(ctx, () => executeProjectPublish({ projectRoot: planned.project_root, expectedProjectRevision: planned.project_revision, @@ -251,12 +241,12 @@ export const managedAgentProjectPublish = defineCommand({ refresh: !ctx.flags.noRefresh, concurrency: ctx.flags.concurrency, policy: "force", - onFeedback: renderAgentFeedback, + onFeedback: (event) => renderAgentFeedback(ctx.output, event), resolveBuild, }), ), ); - emitResult(result, detectOutputFormat(ctx.settings.output)); + ctx.output.result(result); }, }); @@ -284,13 +274,16 @@ export const managedAgentProjectWorkbench = defineCommand({ async run(ctx) { const root = ctx.flags.project ?? "."; if (ctx.settings.dryRun) { - emitResult( - { would_launch: "workbench", project_root: root, port: ctx.flags.port ?? 4848 }, - detectOutputFormat(ctx.settings.output), - ); + ctx.output.result({ + would_launch: "workbench", + project_root: root, + port: ctx.flags.port ?? 4848, + }); return; } await launchManagedAgentPlayground({ + output: ctx.output, + localize: ctx.localize, project: root, port: ctx.flags.port ?? 4848, open: !ctx.flags.noOpen, @@ -316,16 +309,13 @@ function versionToggleCommand(enabled: boolean) { async run(ctx) { const service = createDirectoryWorkspaceVersionService(ctx.flags.project ?? "."); if (ctx.settings.dryRun) { - emitResult( - { would_set_enabled: enabled, status: await service.status() }, - detectOutputFormat(ctx.settings.output), - ); + ctx.output.result({ would_set_enabled: enabled, status: await service.status() }); return; } const result = enabled ? await service.enable("Enable project versions") : await service.disable(); - emitResult(result, detectOutputFormat(ctx.settings.output)); + ctx.output.result(result); }, }); } @@ -340,9 +330,8 @@ export const managedAgentProjectVersionStatus = defineCommand({ flags: PROJECT_FLAG, exampleArgs: ["", "--project ./my-agent --output json"], async run(ctx) { - emitResult( + ctx.output.result( await createDirectoryWorkspaceVersionService(ctx.flags.project ?? ".").status(), - detectOutputFormat(ctx.settings.output), ); }, }); @@ -366,12 +355,11 @@ export const managedAgentProjectVersionList = defineCommand({ }, exampleArgs: ["", "--limit 20 --output json"], async run(ctx) { - emitResult( + ctx.output.result( await createDirectoryWorkspaceVersionService(ctx.flags.project ?? ".").listVersions({ limit: ctx.flags.limit, cursor: ctx.flags.cursor, }), - detectOutputFormat(ctx.settings.output), ); }, }); @@ -392,11 +380,10 @@ export const managedAgentProjectVersionPreview = defineCommand({ flags: { ...PROJECT_FLAG, ...VERSION_ID_FLAG }, exampleArgs: ["--version-id "], async run(ctx) { - emitResult( + ctx.output.result( await createDirectoryWorkspaceVersionService(ctx.flags.project ?? ".").previewVersion( ctx.flags.versionId, ), - detectOutputFormat(ctx.settings.output), ); }, }); @@ -427,17 +414,14 @@ export const managedAgentProjectVersionRestore = defineCommand({ ExitCode.GENERAL, ); if (ctx.settings.dryRun) { - emitResult( - { would_restore: ctx.flags.versionId, preview }, - detectOutputFormat(ctx.settings.output), - ); + ctx.output.result({ would_restore: ctx.flags.versionId, preview }); return; } const restored = await service.restoreVersion(ctx.flags.versionId, { headVersion: preview.base_head_version, projectRevision: preview.base_project_revision, }); - emitResult(restored, detectOutputFormat(ctx.settings.output)); + ctx.output.result(restored); }, }); diff --git a/packages/commands/src/commands/managed-agent/session-create.ts b/packages/commands/src/commands/managed-agent/session-create.ts index 111a9fba3..285877010 100644 --- a/packages/commands/src/commands/managed-agent/session-create.ts +++ b/packages/commands/src/commands/managed-agent/session-create.ts @@ -1,5 +1,4 @@ -import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; +import { defineCommand, type FlagsDef } from "bailian-cli-core"; import { createSessionForAgent } from "@openagentpack/sdk"; import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts"; import { withStdoutProtected } from "./_engine/console-capture.ts"; @@ -82,33 +81,30 @@ export default defineCommand({ }, async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const file = flags.file ?? "agents.yaml"; const environmentVariables = parseEnvironmentVariables(flags.environment_variables); const resources = await readSessionResources(flags.resource); if (settings.dryRun) { - emitResult( - { - would_create_session: { - agent: flags.agent ?? "auto", - provider: "bailian", - environment: flags.environment, - environment_variables: redactEnvironmentVariables(environmentVariables), - vault: flags.vault, - memory_stores: parseMemoryStores(flags.memoryStores), - resources, - title: flags.title, - }, - config_file: file, + ctx.output.result({ + would_create_session: { + agent: flags.agent ?? "auto", + provider: "bailian", + environment: flags.environment, + environment_variables: redactEnvironmentVariables(environmentVariables), + vault: flags.vault, + memory_stores: parseMemoryStores(flags.memoryStores), + resources, + title: flags.title, }, - format, - ); + config_file: file, + }); return; } const run = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, file); return createSessionForAgent(runtime, { agent: flags.agent, @@ -124,17 +120,26 @@ export default defineCommand({ ); const { agentName, session } = run; - if (format === "json") { - emitResult({ agent: agentName, session }, format); - return; - } - emitBare(`Session created: ${session.id}`); - emitBare(` Agent: ${agentName}`); - emitBare(` Environment: ${session.environment_id}`); - emitBare(` Status: ${session.status}`); - if (session.vault_ids.length) emitBare(` Vaults: ${session.vault_ids.join(", ")}`); - if (session.memory_store_ids.length) { - emitBare(` Memory: ${session.memory_store_ids.join(", ")}`); - } + ctx.output.result( + { agent: agentName, session }, + { + text: + ctx.settings.output === "text" + ? (() => { + const lines: string[] = []; + lines.push(`Session created: ${session.id}`); + lines.push(` Agent: ${agentName}`); + lines.push(` Environment: ${session.environment_id}`); + lines.push(` Status: ${session.status}`); + if (session.vault_ids.length) + lines.push(` Vaults: ${session.vault_ids.join(", ")}`); + if (session.memory_store_ids.length) { + lines.push(` Memory: ${session.memory_store_ids.join(", ")}`); + } + return lines.join("\n"); + })() + : undefined, + }, + ); }, }); diff --git a/packages/commands/src/commands/managed-agent/session-debug.ts b/packages/commands/src/commands/managed-agent/session-debug.ts index 8d96dd332..78547a25b 100644 --- a/packages/commands/src/commands/managed-agent/session-debug.ts +++ b/packages/commands/src/commands/managed-agent/session-debug.ts @@ -9,8 +9,7 @@ import { type ProviderSessionInfo, } from "@openagentpack/sdk"; import { sanitizeSessionEvents } from "@openagentpack/sdk/session-events"; -import { defineCommand, detectOutputFormat } from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; +import { defineCommand } from "bailian-cli-core"; import { API_TARGET_FLAGS } from "./_engine/api-helpers.ts"; import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts"; import { withStdoutProtected } from "./_engine/console-capture.ts"; @@ -268,23 +267,28 @@ export const managedAgentSessionDebug = defineCommand({ exampleArgs: ["--session-id sess_abc", "--session-id sess_abc --output json"], notes: CREDENTIALS_NOTE, async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); const bundle = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); return collectSessionDiagnostics(runtime, ctx.flags.sessionId, "bailian"); }), ); const redacted = redactSensitiveValues(bundle); - if (format === "json") { - emitResult(redacted, format); - return; - } - emitBare(`Session: ${bundle.session.id} (${bundle.session.status})`); - emitBare(`Events: ${bundle.events.length}`); - emitBare(`Scoped files: ${bundle.files.length}`); - emitBare(`Partial errors: ${bundle.errors.length}`); - for (const error of bundle.errors) emitBare(` ${error.component}: ${error.message}`); + ctx.output.result(redacted, { + text: + ctx.settings.output === "text" + ? (() => { + const lines: string[] = []; + lines.push(`Session: ${bundle.session.id} (${bundle.session.status})`); + lines.push(`Events: ${bundle.events.length}`); + lines.push(`Scoped files: ${bundle.files.length}`); + lines.push(`Partial errors: ${bundle.errors.length}`); + for (const error of bundle.errors) + lines.push(` ${error.component}: ${error.message}`); + return lines.join("\n"); + })() + : undefined, + }); }, }); @@ -306,16 +310,15 @@ export const managedAgentSessionExport = defineCommand({ }, ], async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); if (ctx.settings.dryRun) { - emitResult( - { would_export_session: ctx.flags.sessionId, output_file: ctx.flags.outputFile }, - format, - ); + ctx.output.result({ + would_export_session: ctx.flags.sessionId, + output_file: ctx.flags.outputFile, + }); return; } const bundle = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); return collectSessionDiagnostics(runtime, ctx.flags.sessionId, "bailian"); }), @@ -342,15 +345,14 @@ export const managedAgentSessionExport = defineCommand({ { name: "errors.json", content: jsonBytes(bundle.errors) }, ]); const outputFile = await writeOutputFile(ctx.flags.outputFile, zip, ctx.flags.force); - if (format === "json") { - emitResult( - { exported: bundle.session.id, output_file: outputFile, partial: manifest.partial }, - format, - ); - } else { - emitBare( - `Session diagnostics exported to ${outputFile}${manifest.partial ? " (partial)" : ""}`, - ); - } + ctx.output.result( + { exported: bundle.session.id, output_file: outputFile, partial: manifest.partial }, + { + text: + ctx.settings.output === "text" + ? `Session diagnostics exported to ${outputFile}${manifest.partial ? " (partial)" : ""}` + : undefined, + }, + ); }, }); diff --git a/packages/commands/src/commands/managed-agent/session-delete.ts b/packages/commands/src/commands/managed-agent/session-delete.ts index f7f3f147d..ff55b817f 100644 --- a/packages/commands/src/commands/managed-agent/session-delete.ts +++ b/packages/commands/src/commands/managed-agent/session-delete.ts @@ -1,5 +1,4 @@ -import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; +import { defineCommand, type FlagsDef } from "bailian-cli-core"; import { deleteSession } from "@openagentpack/sdk"; import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts"; import { withStdoutProtected } from "./_engine/console-capture.ts"; @@ -38,29 +37,28 @@ export default defineCommand({ notes: CREDENTIALS_NOTE, async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const file = flags.file ?? "agents.yaml"; if (settings.dryRun) { - emitResult( - { - would_delete_session: flags.sessionId, - provider: "bailian", - config_file: file, - }, - format, - ); + ctx.output.result({ + would_delete_session: flags.sessionId, + provider: "bailian", + config_file: file, + }); return; } await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, file); await deleteSession(runtime, flags.sessionId, "bailian"); }), ); - if (format === "json") emitResult({ deleted: flags.sessionId }, format); - else emitBare(`Session ${flags.sessionId} deleted.`); + ctx.output.result( + { deleted: flags.sessionId }, + { text: ctx.settings.output === "text" ? `Session ${flags.sessionId} deleted.` : undefined }, + ); }, }); diff --git a/packages/commands/src/commands/managed-agent/session-event.ts b/packages/commands/src/commands/managed-agent/session-event.ts index 111b3fb16..ebd19b6f1 100644 --- a/packages/commands/src/commands/managed-agent/session-event.ts +++ b/packages/commands/src/commands/managed-agent/session-event.ts @@ -7,8 +7,7 @@ import { streamSessionEvents, } from "@openagentpack/sdk"; import { sanitizeSessionEvents } from "@openagentpack/sdk/session-events"; -import { BailianError, defineCommand, detectOutputFormat, ExitCode } from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; +import { BailianError, defineCommand, ExitCode } from "bailian-cli-core"; import { API_TARGET_FLAGS, CURSOR_FLAGS, @@ -144,9 +143,8 @@ export const managedAgentSessionEventList = defineCommand({ ], validate: validateLimitAndPageLimit, async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); const result = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); return fetchAllPages( async (page) => { @@ -170,8 +168,7 @@ export const managedAgentSessionEventList = defineCommand({ ); }), ); - emitCollection({ - format, + emitCollection(ctx, { key: "events", items: sanitizeSessionEvents(result.items), headers: ["ID", "TYPE", "THREAD", "CONTENT"], @@ -197,22 +194,28 @@ export const managedAgentSessionEventSend = defineCommand({ ], notes: CREDENTIALS_NOTE, async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); const events = normalizeEventInput(await readJsonInput(ctx.flags.event)); if (ctx.settings.dryRun) { - emitResult({ would_send_events: events, session_id: ctx.flags.sessionId }, format); + ctx.output.result({ would_send_events: events, session_id: ctx.flags.sessionId }); return; } const result = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); return sendRemoteSessionEvents(runtime, ctx.flags.sessionId, events, { provider: "bailian", }); }), ); - if (format === "json") emitResult({ session_id: ctx.flags.sessionId, ...result }, format); - else emitBare(`Sent ${events.length} event(s): ${result.event_ids.join(", ")}`); + ctx.output.result( + { session_id: ctx.flags.sessionId, ...result }, + { + text: + ctx.settings.output === "text" + ? `Sent ${events.length} event(s): ${result.event_ids.join(", ")}` + : undefined, + }, + ); }, }); @@ -235,9 +238,8 @@ export const managedAgentSessionEventStream = defineCommand({ }, ], async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); const events: ProviderSessionEvent[] = []; for await (const event of streamSessionEvents(runtime, ctx.flags.sessionId, { @@ -245,15 +247,15 @@ export const managedAgentSessionEventStream = defineCommand({ after_id: ctx.flags.afterId, })) { events.push(event); - if (format !== "json") emitBare(JSON.stringify(sanitizeSessionEvents([event])[0])); + // Native text event stream; JSON is collected for one final envelope. + if (ctx.settings.output === "text") + process.stdout.write(JSON.stringify(sanitizeSessionEvents([event])[0]) + "\n"); if (event.type === "status" && isTerminalSessionStatus(event.status)) break; } - if (format === "json") { - emitResult( - { session_id: ctx.flags.sessionId, events: sanitizeSessionEvents(events) }, - format, - ); - } + ctx.output.result( + { session_id: ctx.flags.sessionId, events: sanitizeSessionEvents(events) }, + { text: "" }, + ); }), ); }, diff --git a/packages/commands/src/commands/managed-agent/session-get.ts b/packages/commands/src/commands/managed-agent/session-get.ts index be6a8e19f..dd944d23d 100644 --- a/packages/commands/src/commands/managed-agent/session-get.ts +++ b/packages/commands/src/commands/managed-agent/session-get.ts @@ -1,5 +1,4 @@ -import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; +import { defineCommand, type FlagsDef } from "bailian-cli-core"; import { getSession } from "@openagentpack/sdk"; import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts"; import { withStdoutProtected } from "./_engine/console-capture.ts"; @@ -30,27 +29,32 @@ export default defineCommand({ exampleArgs: ["--session-id sess_abc123"], notes: CREDENTIALS_NOTE, async run(ctx) { - const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const { flags } = ctx; + const file = flags.file ?? "agents.yaml"; const session = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, file); return getSession(runtime, flags.sessionId, "bailian"); }), ); - if (format === "json") { - emitResult(session, format); - return; - } - emitBare(` ID: ${session.id}`); - emitBare(` Agent: ${session.agent_id}`); - emitBare(` Environment: ${session.environment_id}`); - emitBare(` Status: ${session.status}`); - if (session.title) emitBare(` Title: ${session.title}`); - emitBare(` Created: ${session.created_at}`); - emitBare(` Updated: ${session.updated_at}`); + ctx.output.result(session, { + text: + ctx.settings.output === "text" + ? (() => { + const lines: string[] = []; + lines.push(` ID: ${session.id}`); + lines.push(` Agent: ${session.agent_id}`); + lines.push(` Environment: ${session.environment_id}`); + lines.push(` Status: ${session.status}`); + if (session.title) lines.push(` Title: ${session.title}`); + lines.push(` Created: ${session.created_at}`); + lines.push(` Updated: ${session.updated_at}`); + return lines.join("\n"); + })() + : undefined, + }); }, }); diff --git a/packages/commands/src/commands/managed-agent/session-list.ts b/packages/commands/src/commands/managed-agent/session-list.ts index 8be8fa1c3..ae9cdffa4 100644 --- a/packages/commands/src/commands/managed-agent/session-list.ts +++ b/packages/commands/src/commands/managed-agent/session-list.ts @@ -1,5 +1,5 @@ -import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core"; -import { emitBare, emitResult, formatTable } from "bailian-cli-runtime"; +import { defineCommand, type FlagsDef } from "bailian-cli-core"; +import { formatTable } from "bailian-cli-runtime"; import { listSessionSummaries } from "@openagentpack/sdk"; import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts"; import { withStdoutProtected } from "./_engine/console-capture.ts"; @@ -72,8 +72,8 @@ export default defineCommand({ notes: CREDENTIALS_NOTE, validate: validateLimitAndPageLimit, async run(ctx) { - const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const { flags } = ctx; + const file = flags.file ?? "agents.yaml"; const { @@ -81,7 +81,7 @@ export default defineCommand({ hasMore, nextPage, } = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, file); return fetchAllPages( async (page) => { @@ -109,30 +109,35 @@ export default defineCommand({ ); const sessions = summaries.map((summary) => summary.session); - if (format === "json") { - emitResult({ sessions, has_more: hasMore, next_page: nextPage }, format); - return; - } - if (sessions.length === 0) { - emitBare("No sessions found."); - return; - } + const text = + ctx.settings.output === "text" + ? (() => { + const lines: string[] = []; + if (sessions.length === 0) { + lines.push("No sessions found."); + return lines.join("\n"); + } - const agentNames = new Map( - summaries - .filter((summary) => summary.agentName) - .map((summary) => [summary.session.id, summary.agentName!]), - ); - const headers = ["ID", "TITLE", "AGENT", "STATUS", "CREATED"]; - const rows = sessions.map((session) => [ - session.id, - (session.title ?? "").slice(0, 20), - agentNames.get(session.id) ?? session.agent_id.slice(0, 12), - session.status, - session.created_at, - ]); - for (const line of formatTable(headers, rows)) emitBare(line); - emitBare(`\nTotal: ${sessions.length}`); - if (hasMore) emitBare("More sessions available. Use --all to fetch all."); + const agentNames = new Map( + summaries + .filter((summary) => summary.agentName) + .map((summary) => [summary.session.id, summary.agentName!]), + ); + const headers = ["ID", "TITLE", "AGENT", "STATUS", "CREATED"]; + const rows = sessions.map((session) => [ + session.id, + (session.title ?? "").slice(0, 20), + agentNames.get(session.id) ?? session.agent_id.slice(0, 12), + session.status, + session.created_at, + ]); + for (const line of formatTable(headers, rows)) lines.push(line); + lines.push(`\nTotal: ${sessions.length}`); + if (hasMore) lines.push("More sessions available. Use --all to fetch all."); + + return lines.join("\n"); + })() + : undefined; + ctx.output.result({ sessions, has_more: hasMore, next_page: nextPage }, { text }); }, }); diff --git a/packages/commands/src/commands/managed-agent/session-management.ts b/packages/commands/src/commands/managed-agent/session-management.ts index d7e6ce395..8e17fc6f8 100644 --- a/packages/commands/src/commands/managed-agent/session-management.ts +++ b/packages/commands/src/commands/managed-agent/session-management.ts @@ -4,8 +4,7 @@ import { listSessionSummaries, updateRemoteSession, } from "@openagentpack/sdk"; -import { BailianError, defineCommand, detectOutputFormat, ExitCode } from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; +import { BailianError, defineCommand, ExitCode } from "bailian-cli-core"; import { API_TARGET_FLAGS, CURSOR_FLAGS, @@ -126,9 +125,8 @@ export const managedAgentSessionSearch = defineCommand({ notes: CREDENTIALS_NOTE, validate: validateLimitAndPageLimit, async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); const result = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); return searchCursorPages( async (page) => { @@ -163,8 +161,7 @@ export const managedAgentSessionSearch = defineCommand({ }), ); const sessions = result.items.map((summary) => summary.session); - emitCollection({ - format, + emitCollection(ctx, { key: "sessions", items: sessions, headers: ["ID", "TITLE", "AGENT", "STATUS", "UPDATED"], @@ -191,22 +188,22 @@ export const managedAgentSessionUpdate = defineCommand({ validate: (flags) => !flags.title && !flags.metadata ? "Provide --title or --metadata." : undefined, async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); const input = { title: ctx.flags.title, metadata: await parseMetadata(ctx.flags.metadata) }; if (ctx.settings.dryRun) { - emitResult({ would_update_session: ctx.flags.sessionId, input }, format); + ctx.output.result({ would_update_session: ctx.flags.sessionId, input }); return; } const session = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); return updateRemoteSession(runtime, ctx.flags.sessionId, input, { provider: "bailian", }); }), ); - if (format === "json") emitResult(session, format); - else emitBare(`Session ${session.id} updated.`); + ctx.output.result(session, { + text: ctx.settings.output === "text" ? `Session ${session.id} updated.` : undefined, + }); }, }); @@ -225,18 +222,22 @@ export const managedAgentSessionArchive = defineCommand({ exampleArgs: ["--session-id sess_abc --dry-run", "--session-id sess_abc --yes"], notes: CREDENTIALS_NOTE, async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); if (ctx.settings.dryRun) { - emitResult({ would_archive_session: ctx.flags.sessionId }, format); + ctx.output.result({ would_archive_session: ctx.flags.sessionId }); return; } const session = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); return archiveRemoteSession(runtime, ctx.flags.sessionId, { provider: "bailian" }); }), ); - if (format === "json") emitResult({ archived: ctx.flags.sessionId, session }, format); - else emitBare(`Session ${ctx.flags.sessionId} archived.`); + ctx.output.result( + { archived: ctx.flags.sessionId, session }, + { + text: + ctx.settings.output === "text" ? `Session ${ctx.flags.sessionId} archived.` : undefined, + }, + ); }, }); diff --git a/packages/commands/src/commands/managed-agent/session-run.ts b/packages/commands/src/commands/managed-agent/session-run.ts index 089a8682f..b133236ea 100644 --- a/packages/commands/src/commands/managed-agent/session-run.ts +++ b/packages/commands/src/commands/managed-agent/session-run.ts @@ -1,5 +1,4 @@ -import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; +import { defineCommand, type FlagsDef } from "bailian-cli-core"; import { startSessionRun, startSessionRunPolling } from "@openagentpack/sdk"; import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts"; import { withStdoutProtected } from "./_engine/console-capture.ts"; @@ -120,11 +119,10 @@ export default defineCommand({ }, async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const file = flags.file ?? "agents.yaml"; const environmentVariables = parseEnvironmentVariables(flags.environment_variables); const resources = await readSessionResources(flags.resource); - const asJson = format === "json"; const runOptions = { agent: flags.agent, @@ -138,42 +136,45 @@ export default defineCommand({ }; if (settings.dryRun) { - emitResult( - { - would_run: { - prompt: flags.prompt, - agent: flags.agent ?? "auto", - provider: "bailian", - environment: flags.environment, - environment_variables: redactEnvironmentVariables(environmentVariables), - vault: flags.vault, - memory_stores: runOptions.memoryStores, - resources, - title: flags.title, - mode: flags.noStream ? "polling" : "streaming", - }, - config_file: file, + ctx.output.result({ + would_run: { + prompt: flags.prompt, + agent: flags.agent ?? "auto", + provider: "bailian", + environment: flags.environment, + environment_variables: redactEnvironmentVariables(environmentVariables), + vault: flags.vault, + memory_stores: runOptions.memoryStores, + resources, + title: flags.title, + mode: flags.noStream ? "polling" : "streaming", }, - format, - ); + config_file: file, + }); return; } await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, file); if (flags.noStream) { const run = await startSessionRunPolling(runtime, flags.prompt, runOptions); - if (!asJson) process.stderr.write(`Session created: ${run.session.id}\n`); - renderCollectedEvents(run, asJson, { + ctx.output.info({ + "en-US": `Session created: ${run.session.id}`, + "zh-CN": `Session 已创建:${run.session.id}`, + }); + renderCollectedEvents(ctx, run, { session_id: run.session.id, provider: run.provider, agent: run.agentName, }); } else { const run = await startSessionRun(runtime, flags.prompt, runOptions); - if (!asJson) process.stderr.write(`Session created: ${run.session.id}\n`); - await streamAndRenderEvents(run.events, asJson, { + ctx.output.info({ + "en-US": `Session created: ${run.session.id}`, + "zh-CN": `Session 已创建:${run.session.id}`, + }); + await streamAndRenderEvents(ctx, run.events, { session_id: run.session.id, provider: run.provider, agent: run.agentName, diff --git a/packages/commands/src/commands/managed-agent/session-send.ts b/packages/commands/src/commands/managed-agent/session-send.ts index 30afe8e59..868d6af93 100644 --- a/packages/commands/src/commands/managed-agent/session-send.ts +++ b/packages/commands/src/commands/managed-agent/session-send.ts @@ -1,5 +1,4 @@ -import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; +import { defineCommand, type FlagsDef } from "bailian-cli-core"; import { sendSessionMessagePolling, sendSessionMessageStreaming } from "@openagentpack/sdk"; import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts"; import { withStdoutProtected } from "./_engine/console-capture.ts"; @@ -53,34 +52,30 @@ export default defineCommand({ notes: CREDENTIALS_NOTE, async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const file = flags.file ?? "agents.yaml"; - const asJson = format === "json"; if (settings.dryRun) { - emitResult( - { - would_send: { - session_id: flags.sessionId, - message: flags.message, - provider: "bailian", - mode: flags.noStream ? "polling" : "streaming", - }, - config_file: file, + ctx.output.result({ + would_send: { + session_id: flags.sessionId, + message: flags.message, + provider: "bailian", + mode: flags.noStream ? "polling" : "streaming", }, - format, - ); + config_file: file, + }); return; } await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, file); if (flags.noStream) { const result = await sendSessionMessagePolling(runtime, flags.sessionId, flags.message, { provider: "bailian", }); - renderCollectedEvents(result, asJson, { + renderCollectedEvents(ctx, result, { session_id: flags.sessionId, }); } else { @@ -92,7 +87,7 @@ export default defineCommand({ provider: "bailian", }, ); - await streamAndRenderEvents(events, asJson, { + await streamAndRenderEvents(ctx, events, { session_id: flags.sessionId, }); } diff --git a/packages/commands/src/commands/managed-agent/skill/download.ts b/packages/commands/src/commands/managed-agent/skill/download.ts index d752a6772..4cc27e222 100644 --- a/packages/commands/src/commands/managed-agent/skill/download.ts +++ b/packages/commands/src/commands/managed-agent/skill/download.ts @@ -1,6 +1,5 @@ import { downloadRemoteSkill } from "@openagentpack/sdk"; -import { defineCommand, detectOutputFormat } from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; +import { defineCommand } from "bailian-cli-core"; import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts"; import { withStdoutProtected } from "../_engine/console-capture.ts"; import { withAgentErrors } from "../_engine/errors.ts"; @@ -18,20 +17,16 @@ export default defineCommand({ exampleArgs: ["--skill-id skill_abc --skill-version 3 --output-file ./skill.zip"], notes: CREDENTIALS_NOTE, async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); if (ctx.settings.dryRun) { - emitResult( - { - would_download_skill: ctx.flags.skillId, - version: ctx.flags.skillVersion, - output_file: ctx.flags.outputFile, - }, - format, - ); + ctx.output.result({ + would_download_skill: ctx.flags.skillId, + version: ctx.flags.skillVersion, + output_file: ctx.flags.outputFile, + }); return; } const content = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); return downloadRemoteSkill(runtime, ctx.flags.skillId, ctx.flags.skillVersion, { provider: "bailian", @@ -39,11 +34,9 @@ export default defineCommand({ }), ); const outputFile = await writeOutputFile(ctx.flags.outputFile, content, ctx.flags.force); - if (format === "json") - emitResult( - { downloaded: ctx.flags.skillId, version: ctx.flags.skillVersion, output_file: outputFile }, - format, - ); - else emitBare(`Skill downloaded to ${outputFile}`); + ctx.output.result( + { downloaded: ctx.flags.skillId, version: ctx.flags.skillVersion, output_file: outputFile }, + { text: ctx.settings.output === "text" ? `Skill downloaded to ${outputFile}` : undefined }, + ); }, }); diff --git a/packages/commands/src/commands/managed-agent/skill/get.ts b/packages/commands/src/commands/managed-agent/skill/get.ts index 88f9b7c07..8d44c0ef3 100644 --- a/packages/commands/src/commands/managed-agent/skill/get.ts +++ b/packages/commands/src/commands/managed-agent/skill/get.ts @@ -1,6 +1,5 @@ import { getRemoteSkill } from "@openagentpack/sdk"; -import { defineCommand, detectOutputFormat } from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; +import { defineCommand } from "bailian-cli-core"; import { displayValue } from "../_engine/api-helpers.ts"; import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts"; import { withStdoutProtected } from "../_engine/console-capture.ts"; @@ -15,22 +14,26 @@ export default defineCommand({ exampleArgs: ["--skill-id skill_abc"], notes: CREDENTIALS_NOTE, async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); const skill = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); return getRemoteSkill(runtime, ctx.flags.skillId, { provider: "bailian" }); }), ); - if (format === "json") { - emitResult(skill, format); - return; - } - emitBare(`ID: ${skill.id}`); - emitBare(`Name: ${skill.name}`); - emitBare(`Description: ${displayValue(skill.description, 120)}`); - emitBare(`Source: ${skill.source}`); - emitBare(`Status: ${skill.status}`); - emitBare(`Version: ${displayValue(skill.latest_version)}`); + ctx.output.result(skill, { + text: + ctx.settings.output === "text" + ? (() => { + const lines: string[] = []; + lines.push(`ID: ${skill.id}`); + lines.push(`Name: ${skill.name}`); + lines.push(`Description: ${displayValue(skill.description, 120)}`); + lines.push(`Source: ${skill.source}`); + lines.push(`Status: ${skill.status}`); + lines.push(`Version: ${displayValue(skill.latest_version)}`); + return lines.join("\n"); + })() + : undefined, + }); }, }); diff --git a/packages/commands/src/commands/managed-agent/skill/list.ts b/packages/commands/src/commands/managed-agent/skill/list.ts index 0049f3069..1d772fa83 100644 --- a/packages/commands/src/commands/managed-agent/skill/list.ts +++ b/packages/commands/src/commands/managed-agent/skill/list.ts @@ -1,6 +1,5 @@ import { listRemoteSkills } from "@openagentpack/sdk"; import { defineCommand, detectOutputFormat } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; import { emitCollection, validateLimitAndPageLimit } from "../_engine/api-helpers.ts"; import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts"; import { withStdoutProtected } from "../_engine/console-capture.ts"; @@ -64,7 +63,7 @@ export default defineCommand({ const format = detectOutputFormat(ctx.settings.output); const source = (ctx.flags.source as SkillSource | undefined) ?? "custom"; const result = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); if (source !== "all") { return listOneCatalog(runtime, source, { @@ -91,11 +90,10 @@ export default defineCommand({ }), ); if (format === "json") { - emitResult(buildSkillListJsonResult(source, result), format); + ctx.output.result(buildSkillListJsonResult(source, result)); return; } - emitCollection({ - format, + emitCollection(ctx, { key: "skills", items: result.items, headers: ["ID", "NAME", "SOURCE", "STATUS", "VERSION", "UPDATED"], diff --git a/packages/commands/src/commands/managed-agent/skill/search.ts b/packages/commands/src/commands/managed-agent/skill/search.ts index 0359d4027..5616c39bd 100644 --- a/packages/commands/src/commands/managed-agent/skill/search.ts +++ b/packages/commands/src/commands/managed-agent/skill/search.ts @@ -1,5 +1,5 @@ import { listRemoteSkills } from "@openagentpack/sdk"; -import { defineCommand, detectOutputFormat } from "bailian-cli-core"; +import { defineCommand } from "bailian-cli-core"; import { emitCollection, matchesQuery, @@ -20,10 +20,9 @@ export default defineCommand({ notes: CREDENTIALS_NOTE, validate: validateLimitAndPageLimit, async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); const source = (ctx.flags.source as SkillSource | undefined) ?? "custom"; const result = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); const searchCatalog = (catalog: Exclude) => searchCursorPages( @@ -57,8 +56,7 @@ export default defineCommand({ }; }), ); - emitCollection({ - format, + emitCollection(ctx, { key: "skills", items: result.items, headers: ["ID", "NAME", "SOURCE", "STATUS", "VERSION", "UPDATED"], diff --git a/packages/commands/src/commands/managed-agent/skill/versions.ts b/packages/commands/src/commands/managed-agent/skill/versions.ts index 00b1c1c19..6862fc794 100644 --- a/packages/commands/src/commands/managed-agent/skill/versions.ts +++ b/packages/commands/src/commands/managed-agent/skill/versions.ts @@ -1,5 +1,5 @@ import { listRemoteSkillVersions } from "@openagentpack/sdk"; -import { defineCommand, detectOutputFormat } from "bailian-cli-core"; +import { defineCommand } from "bailian-cli-core"; import { emitCollection, validateLimitAndPageLimit } from "../_engine/api-helpers.ts"; import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts"; import { withStdoutProtected } from "../_engine/console-capture.ts"; @@ -19,9 +19,8 @@ export default defineCommand({ notes: CREDENTIALS_NOTE, validate: validateLimitAndPageLimit, async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); const result = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); return fetchAllPages( async (page) => { @@ -41,8 +40,7 @@ export default defineCommand({ ); }), ); - emitCollection({ - format, + emitCollection(ctx, { key: "versions", items: result.items, headers: ["VERSION", "NAME", "TYPE", "STATUS", "UPDATED"], diff --git a/packages/commands/src/commands/managed-agent/state-import.ts b/packages/commands/src/commands/managed-agent/state-import.ts index 5754238ea..9ba8860ae 100644 --- a/packages/commands/src/commands/managed-agent/state-import.ts +++ b/packages/commands/src/commands/managed-agent/state-import.ts @@ -1,11 +1,4 @@ -import { - BailianError, - defineCommand, - detectOutputFormat, - ExitCode, - type FlagsDef, -} from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; +import { BailianError, defineCommand, ExitCode, type FlagsDef } from "bailian-cli-core"; import { importResource, parseStateAddress } from "@openagentpack/sdk"; import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts"; import { withStdoutProtected } from "./_engine/console-capture.ts"; @@ -72,7 +65,7 @@ export default defineCommand({ notes: CREDENTIALS_NOTE, async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const file = flags.file ?? "agents.yaml"; if (settings.dryRun) { @@ -80,20 +73,17 @@ export default defineCommand({ await withAgentErrors(async () => { parseBailianStateAddress(flags.address); }); - emitResult( - { - would_import: flags.address, - remote_id: flags.remoteId, - resource_version: flags.resourceVersion, - config_file: file, - }, - format, - ); + ctx.output.result({ + would_import: flags.address, + remote_id: flags.remoteId, + resource_version: flags.resourceVersion, + config_file: file, + }); return; } await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { // Parse first so a malformed address fails fast, before any config I/O. const parsed = parseBailianStateAddress(flags.address); const runtime = await buildAgentRuntime(ctx, file); @@ -103,10 +93,14 @@ export default defineCommand({ }), ); - if (format === "json") { - emitResult({ imported: flags.address, remote_id: flags.remoteId }, format); - } else { - emitBare(`Imported ${flags.address} (remote_id: ${flags.remoteId}) into state.`); - } + ctx.output.result( + { imported: flags.address, remote_id: flags.remoteId }, + { + text: + ctx.settings.output === "text" + ? `Imported ${flags.address} (remote_id: ${flags.remoteId}) into state.` + : undefined, + }, + ); }, }); diff --git a/packages/commands/src/commands/managed-agent/state-list.ts b/packages/commands/src/commands/managed-agent/state-list.ts index e6588eb7c..3f137d32e 100644 --- a/packages/commands/src/commands/managed-agent/state-list.ts +++ b/packages/commands/src/commands/managed-agent/state-list.ts @@ -1,5 +1,5 @@ -import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core"; -import { emitBare, emitResult, formatTable } from "bailian-cli-runtime"; +import { defineCommand, type FlagsDef } from "bailian-cli-core"; +import { formatTable } from "bailian-cli-runtime"; import { buildAgentRuntime, OFFLINE_NOTE } from "./_engine/config-loader.ts"; import { withStdoutProtected } from "./_engine/console-capture.ts"; import { withAgentErrors } from "./_engine/errors.ts"; @@ -26,12 +26,12 @@ export default defineCommand({ exampleArgs: ["", "--file agents.yaml"], notes: OFFLINE_NOTE, async run(ctx) { - const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const { flags } = ctx; + const file = flags.file ?? "agents.yaml"; const resources = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, file, { credentials: "none", }); @@ -39,23 +39,28 @@ export default defineCommand({ }), ); - if (format === "json") { - emitResult({ resources }, format); - return; - } - if (resources.length === 0) { - emitBare("No resources tracked in state."); - return; - } + const text = + ctx.settings.output === "text" + ? (() => { + const lines: string[] = []; + if (resources.length === 0) { + lines.push("No resources tracked in state."); + return lines.join("\n"); + } + + const headers = ["TYPE", "NAME", "PROVIDER", "REMOTE ID"]; + const rows = resources.map((resource) => [ + resource.address.type, + resource.address.name, + resource.address.provider, + resource.remote_id ?? "(local)", + ]); + for (const line of formatTable(headers, rows)) lines.push(line); + lines.push(`\nTotal: ${resources.length}`); - const headers = ["TYPE", "NAME", "PROVIDER", "REMOTE ID"]; - const rows = resources.map((resource) => [ - resource.address.type, - resource.address.name, - resource.address.provider, - resource.remote_id ?? "(local)", - ]); - for (const line of formatTable(headers, rows)) emitBare(line); - emitBare(`\nTotal: ${resources.length}`); + return lines.join("\n"); + })() + : undefined; + ctx.output.result({ resources }, { text }); }, }); diff --git a/packages/commands/src/commands/managed-agent/state-rm.ts b/packages/commands/src/commands/managed-agent/state-rm.ts index 22577992b..38f50b20b 100644 --- a/packages/commands/src/commands/managed-agent/state-rm.ts +++ b/packages/commands/src/commands/managed-agent/state-rm.ts @@ -1,11 +1,4 @@ -import { - BailianError, - defineCommand, - detectOutputFormat, - ExitCode, - type FlagsDef, -} from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; +import { BailianError, defineCommand, ExitCode, type FlagsDef } from "bailian-cli-core"; import { parseStateAddress } from "@openagentpack/sdk"; import { buildAgentRuntime, OFFLINE_NOTE } from "./_engine/config-loader.ts"; import { withStdoutProtected } from "./_engine/console-capture.ts"; @@ -51,7 +44,7 @@ export default defineCommand({ notes: OFFLINE_NOTE, async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const file = flags.file ?? "agents.yaml"; if (settings.dryRun) { @@ -59,12 +52,12 @@ export default defineCommand({ await withAgentErrors(async () => { parseStateAddress(flags.address, { requireProvider: false }); }); - emitResult({ would_remove: flags.address, config_file: file }, format); + ctx.output.result({ would_remove: flags.address, config_file: file }); return; } await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, file, { credentials: "none", }); @@ -81,7 +74,9 @@ export default defineCommand({ ); const message = `Removed ${flags.address} from state (remote resource not deleted).`; - if (format === "json") emitResult({ removed: flags.address }, format); - else emitBare(message); + ctx.output.result( + { removed: flags.address }, + { text: ctx.settings.output === "text" ? message : undefined }, + ); }, }); diff --git a/packages/commands/src/commands/managed-agent/state-show.ts b/packages/commands/src/commands/managed-agent/state-show.ts index 82e7fae18..4f5a14937 100644 --- a/packages/commands/src/commands/managed-agent/state-show.ts +++ b/packages/commands/src/commands/managed-agent/state-show.ts @@ -1,11 +1,4 @@ -import { - BailianError, - defineCommand, - detectOutputFormat, - ExitCode, - type FlagsDef, -} from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; +import { BailianError, defineCommand, ExitCode, type FlagsDef } from "bailian-cli-core"; import { parseStateAddress } from "@openagentpack/sdk"; import { buildAgentRuntime, OFFLINE_NOTE } from "./_engine/config-loader.ts"; import { withStdoutProtected } from "./_engine/console-capture.ts"; @@ -42,12 +35,12 @@ export default defineCommand({ exampleArgs: ["--address bailian.agent.assistant"], notes: OFFLINE_NOTE, async run(ctx) { - const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const { flags } = ctx; + const file = flags.file ?? "agents.yaml"; const found = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, file, { credentials: "none", }); @@ -62,7 +55,8 @@ export default defineCommand({ throw new BailianError(`Resource not found: ${flags.address}`, ExitCode.GENERAL); } - if (format === "json") emitResult(found, format); - else emitBare(JSON.stringify(found, null, 2)); + ctx.output.result(found, { + text: ctx.settings.output === "text" ? JSON.stringify(found, null, 2) : undefined, + }); }, }); diff --git a/packages/commands/src/commands/managed-agent/validate.ts b/packages/commands/src/commands/managed-agent/validate.ts index 08abdd922..94517411e 100644 --- a/packages/commands/src/commands/managed-agent/validate.ts +++ b/packages/commands/src/commands/managed-agent/validate.ts @@ -1,11 +1,4 @@ -import { - BailianError, - defineCommand, - detectOutputFormat, - ExitCode, - type FlagsDef, -} from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; +import { BailianError, defineCommand, ExitCode, type FlagsDef } from "bailian-cli-core"; import { validateProjectConfig } from "@openagentpack/sdk"; import { OFFLINE_NOTE, resolveAgentProjectConfig } from "./_engine/config-loader.ts"; import { formatAgentDiagnosticFailure, withAgentErrors } from "./_engine/errors.ts"; @@ -32,8 +25,8 @@ export default defineCommand({ exampleArgs: ["", "--file agents.yaml"], notes: OFFLINE_NOTE, async run(ctx) { - const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const { flags } = ctx; + const file = flags.file ?? "agents.yaml"; const diagnostics = await withAgentErrors(async () => { @@ -45,15 +38,27 @@ export default defineCommand({ const errorCount = diagnostics.filter((diag) => diag.severity === "error").length; - if (format === "json") { - emitResult({ valid: errorCount === 0, diagnostics }, format); - } else { - for (const diag of diagnostics) { - const where = diag.resource ? ` (${diag.resource.type}.${diag.resource.name})` : ""; - emitBare(`[${diag.severity}] ${diag.message}${where}`); - } - if (errorCount === 0) emitBare("Configuration is valid."); - } + ctx.output.result( + { valid: errorCount === 0, diagnostics }, + { + text: + ctx.settings.output === "text" + ? (() => { + const lines: string[] = []; + { + for (const diag of diagnostics) { + const where = diag.resource + ? ` (${diag.resource.type}.${diag.resource.name})` + : ""; + lines.push(`[${diag.severity}] ${diag.message}${where}`); + } + if (errorCount === 0) lines.push("Configuration is valid."); + } + return lines.join("\n"); + })() + : undefined, + }, + ); if (errorCount > 0) { throw new BailianError( diff --git a/packages/commands/src/commands/managed-agent/vault/credential/create.ts b/packages/commands/src/commands/managed-agent/vault/credential/create.ts index c8b9c8fca..3635f8803 100644 --- a/packages/commands/src/commands/managed-agent/vault/credential/create.ts +++ b/packages/commands/src/commands/managed-agent/vault/credential/create.ts @@ -13,7 +13,6 @@ import { ExitCode, type FlagsDef, } from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; import { parseDocument } from "yaml"; import { CREDENTIALS_NOTE } from "../../_engine/config-loader.ts"; import { withStdoutProtected } from "../../_engine/console-capture.ts"; @@ -232,7 +231,7 @@ export default defineCommand({ if (ctx.settings.dryRun || !ctx.flags.yes) { const planned = await withAgentErrors(() => - withStdoutProtected(() => + withStdoutProtected(ctx, () => planVaultCredentialCreateWithStateBackend( backendInput, ctx.flags.vault, @@ -261,16 +260,26 @@ export default defineCommand({ remote_checked: planned.remoteChecked, reuse_remote: planned.reuseRemote, }; - if (format === "json") emitResult(result, format); - else { - emitBare(`Vault: ${ctx.flags.vault}`); - emitBare(`Credential: ${ctx.flags.name.trim()} (secret from ${ctx.flags.secretEnv})`); - emitBare( - ctx.settings.dryRun - ? "Dry run: YAML, State, and remote resources were not changed." - : "Preview only: re-run with --yes to append YAML and create this Credential.", - ); - } + ctx.output.result(result, { + text: + ctx.settings.output === "text" + ? (() => { + const lines: string[] = []; + { + lines.push(`Vault: ${ctx.flags.vault}`); + lines.push( + `Credential: ${ctx.flags.name.trim()} (secret from ${ctx.flags.secretEnv})`, + ); + lines.push( + ctx.settings.dryRun + ? "Dry run: YAML, State, and remote resources were not changed." + : "Preview only: re-run with --yes to append YAML and create this Credential.", + ); + } + return lines.join("\n"); + })() + : undefined, + }); return; } @@ -278,7 +287,7 @@ export default defineCommand({ let created; try { created = await withAgentErrors(() => - withStdoutProtected(() => + withStdoutProtected(ctx, () => createVaultCredentialWithStateBackend( backendInput, ctx.flags.vault, @@ -308,10 +317,12 @@ export default defineCommand({ adopted: created.adopted, status: "completed", }; - if (format === "json") emitResult(result, format); - else { - emitBare(`Wrote ${project.configPath} and created Credential '${ctx.flags.name.trim()}'.`); - } + ctx.output.result(result, { + text: + ctx.settings.output === "text" + ? `Wrote ${project.configPath} and created Credential '${ctx.flags.name.trim()}'.` + : undefined, + }); }, }); diff --git a/packages/commands/src/commands/managed-agent/vault/get.ts b/packages/commands/src/commands/managed-agent/vault/get.ts index f9f577177..b1d96b146 100644 --- a/packages/commands/src/commands/managed-agent/vault/get.ts +++ b/packages/commands/src/commands/managed-agent/vault/get.ts @@ -1,6 +1,5 @@ import { getRemoteVault } from "@openagentpack/sdk"; -import { defineCommand, detectOutputFormat } from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; +import { defineCommand } from "bailian-cli-core"; import { displayValue } from "../_engine/api-helpers.ts"; import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts"; import { withStdoutProtected } from "../_engine/console-capture.ts"; @@ -15,21 +14,25 @@ export default defineCommand({ exampleArgs: ["--vault-id vault_abc"], notes: CREDENTIALS_NOTE, async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); const vault = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); return getRemoteVault(runtime, ctx.flags.vaultId, { provider: "bailian" }); }), ); - if (format === "json") { - emitResult(vault, format); - return; - } - emitBare(`ID: ${vault.id}`); - emitBare(`Name: ${displayValue(vault.display_name)}`); - emitBare(`Type: ${displayValue(vault.type)}`); - emitBare(`Created: ${displayValue(vault.created_at)}`); - emitBare(`Updated: ${displayValue(vault.updated_at)}`); + ctx.output.result(vault, { + text: + ctx.settings.output === "text" + ? (() => { + const lines: string[] = []; + lines.push(`ID: ${vault.id}`); + lines.push(`Name: ${displayValue(vault.display_name)}`); + lines.push(`Type: ${displayValue(vault.type)}`); + lines.push(`Created: ${displayValue(vault.created_at)}`); + lines.push(`Updated: ${displayValue(vault.updated_at)}`); + return lines.join("\n"); + })() + : undefined, + }); }, }); diff --git a/packages/commands/src/commands/managed-agent/vault/list.ts b/packages/commands/src/commands/managed-agent/vault/list.ts index dd3f8a4f0..9ee9deb8f 100644 --- a/packages/commands/src/commands/managed-agent/vault/list.ts +++ b/packages/commands/src/commands/managed-agent/vault/list.ts @@ -1,5 +1,5 @@ import { listRemoteVaults } from "@openagentpack/sdk"; -import { defineCommand, detectOutputFormat } from "bailian-cli-core"; +import { defineCommand } from "bailian-cli-core"; import { emitCollection, validateLimitAndPageLimit } from "../_engine/api-helpers.ts"; import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts"; import { withStdoutProtected } from "../_engine/console-capture.ts"; @@ -16,9 +16,8 @@ export default defineCommand({ notes: CREDENTIALS_NOTE, validate: validateLimitAndPageLimit, async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); const result = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); return fetchAllPages( async (page) => { @@ -39,8 +38,7 @@ export default defineCommand({ ); }), ); - emitCollection({ - format, + emitCollection(ctx, { key: "vaults", items: result.items, headers: ["ID", "NAME", "TYPE", "CREATED", "UPDATED"], diff --git a/packages/commands/src/commands/managed-agent/vault/search.ts b/packages/commands/src/commands/managed-agent/vault/search.ts index 7055759a8..0e30ddb68 100644 --- a/packages/commands/src/commands/managed-agent/vault/search.ts +++ b/packages/commands/src/commands/managed-agent/vault/search.ts @@ -1,5 +1,5 @@ import { listRemoteVaults } from "@openagentpack/sdk"; -import { defineCommand, detectOutputFormat } from "bailian-cli-core"; +import { defineCommand } from "bailian-cli-core"; import { emitCollection, matchesQuery, @@ -20,9 +20,8 @@ export default defineCommand({ notes: CREDENTIALS_NOTE, validate: validateLimitAndPageLimit, async run(ctx) { - const format = detectOutputFormat(ctx.settings.output); const result = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(ctx, async () => { const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); return searchCursorPages( async (page) => { @@ -43,8 +42,7 @@ export default defineCommand({ ); }), ); - emitCollection({ - format, + emitCollection(ctx, { key: "vaults", items: result.items, headers: ["ID", "NAME", "TYPE", "CREATED", "UPDATED"], diff --git a/packages/commands/src/commands/managed-agent/webhook.ts b/packages/commands/src/commands/managed-agent/webhook.ts index 20c91e336..d0b0a2d6d 100644 --- a/packages/commands/src/commands/managed-agent/webhook.ts +++ b/packages/commands/src/commands/managed-agent/webhook.ts @@ -14,13 +14,11 @@ import { import { BailianError, defineCommand, - detectOutputFormat, ExitCode, type CommandRisk, type FlagsDef, type LocalizedText, } from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; import { withStdoutProtected } from "./_engine/console-capture.ts"; import { withAgentErrors } from "./_engine/errors.ts"; import { @@ -164,9 +162,9 @@ interface WebhookOperation { } function emitWebhookResult(result: unknown, host: ManagedApiHost): void { - const format = detectOutputFormat(host.settings.output); - if (format === "json") emitResult(result, format); - else emitBare(JSON.stringify(result, null, 2)); + host.output.result(result, { + text: host.settings.output === "text" ? JSON.stringify(result, null, 2) : undefined, + }); } async function runWebhookOperation( @@ -202,7 +200,7 @@ async function runWebhookOperation( ); } const response = await withAgentErrors(() => - withStdoutProtected(async () => { + withStdoutProtected(host, async () => { const context = buildManagedApiContext(host); const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), Math.ceil(timeoutSeconds * 1000)); @@ -224,9 +222,14 @@ async function runWebhookOperation( ); emitWebhookResult(response, host); if (operation.returnsSecret) { - process.stderr.write( - "Signing Secret is shown only in this successful create/reset response; save it now. / Signing Secret 仅在本次创建或重置成功响应中返回,请立即保存。\n", - ); + host.output.warning({ + message: { + "en-US": + "Signing Secret is shown only in this successful create/reset response; save it now.", + "zh-CN": "Signing Secret 仅在本次创建或重置成功响应中返回,请立即保存。", + }, + code: "AGENT_WEBHOOK_SECRET_ONCE", + }); } } diff --git a/packages/commands/src/commands/managed-agent/workbench.ts b/packages/commands/src/commands/managed-agent/workbench.ts index 7c3ed0007..c89e3b61d 100644 --- a/packages/commands/src/commands/managed-agent/workbench.ts +++ b/packages/commands/src/commands/managed-agent/workbench.ts @@ -1,5 +1,4 @@ -import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; +import { defineCommand, type FlagsDef } from "bailian-cli-core"; import { CREDENTIALS_NOTE } from "./_engine/config-loader.ts"; import { launchManagedAgentPlayground } from "./_engine/playground-launcher.ts"; @@ -65,19 +64,18 @@ export const managedAgentPlayground = defineCommand({ const file = ctx.flags.file ?? "agents.yaml"; const port = ctx.flags.port ?? 4848; if (ctx.settings.dryRun) { - emitResult( - { - would_launch: "playground", - config_file: file, - agent: ctx.flags.agent, - port, - open_browser: !ctx.flags.noOpen, - }, - detectOutputFormat(ctx.settings.output), - ); + ctx.output.result({ + would_launch: "playground", + config_file: file, + agent: ctx.flags.agent, + port, + open_browser: !ctx.flags.noOpen, + }); return; } await launchManagedAgentPlayground({ + output: ctx.output, + localize: ctx.localize, file, agent: ctx.flags.agent, port, diff --git a/packages/commands/src/commands/mcp/call.ts b/packages/commands/src/commands/mcp/call.ts index 771649c68..b15ff278e 100644 --- a/packages/commands/src/commands/mcp/call.ts +++ b/packages/commands/src/commands/mcp/call.ts @@ -3,11 +3,9 @@ import { UsageError, BailianError, bailianMcpPath, - detectOutputFormat, type FlagsDef, type ParsedFlags, } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; import { rethrowWithMcpActivateHint } from "./activate-hint.ts"; const CALL_FLAGS = { @@ -143,18 +141,14 @@ export default defineCommand({ const toolArgs = buildToolArgs(flags); const previewUrl = flags.url || ctx.client.url(bailianMcpPath(serverCode)); - const format = detectOutputFormat(settings.output); if (settings.dryRun) { - emitResult( - { - server: serverCode, - url: previewUrl, - tool: toolName, - arguments: toolArgs, - }, - format, - ); + ctx.output.result({ + server: serverCode, + url: previewUrl, + tool: toolName, + arguments: toolArgs, + }); return; } @@ -169,7 +163,7 @@ export default defineCommand({ throw new BailianError(`Tool error: ${errText}`); } - emitResult(result, format); + ctx.output.result(result); } catch (error) { if (!flags.url) { rethrowWithMcpActivateHint(error, serverCode); diff --git a/packages/commands/src/commands/mcp/connect.ts b/packages/commands/src/commands/mcp/connect.ts index a303948e8..66b02cd2a 100644 --- a/packages/commands/src/commands/mcp/connect.ts +++ b/packages/commands/src/commands/mcp/connect.ts @@ -6,12 +6,10 @@ import { bailianMcpPath, bailianMcpSsePath, defineCommand, - detectOutputFormat, getConfigDir, trackingHeaders, type FlagsDef, } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; import { MCP_AGENT_IDS, connectMcpAgents, @@ -109,19 +107,15 @@ export default defineCommand({ const endpoint = `${REGIONS.cn}${endpointPath}`; const channelHeaders = trackingHeaders(ctx.identity); const headerNames = ["Authorization", ...Object.keys(channelHeaders)]; - const format = detectOutputFormat(settings.output); if (settings.dryRun) { - emitResult( - { - server: flags.server, - transport, - endpoint, - agents, - header_names: headerNames, - }, - format, - ); + ctx.output.result({ + server: flags.server, + transport, + endpoint, + agents, + header_names: headerNames, + }); return; } @@ -139,6 +133,6 @@ export default defineCommand({ configDir: getConfigDir(), }); - emitResult({ server: flags.server, transport, endpoint, results }, format); + ctx.output.result({ server: flags.server, transport, endpoint, results }); }, }); diff --git a/packages/commands/src/commands/mcp/disconnect.ts b/packages/commands/src/commands/mcp/disconnect.ts index 82d1c1412..75ac40b13 100644 --- a/packages/commands/src/commands/mcp/disconnect.ts +++ b/packages/commands/src/commands/mcp/disconnect.ts @@ -3,11 +3,9 @@ import { BailianError, ExitCode, defineCommand, - detectOutputFormat, getConfigDir, type FlagsDef, } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; import { MCP_AGENT_IDS, disconnectMcpAgents, @@ -67,9 +65,8 @@ export default defineCommand({ ExitCode.USAGE, ); } - const format = detectOutputFormat(settings.output); if (settings.dryRun) { - emitResult({ server: flags.server, agents, action: "disconnect" }, format); + ctx.output.result({ server: flags.server, agents, action: "disconnect" }); return; } const results = disconnectMcpAgents({ @@ -78,6 +75,6 @@ export default defineCommand({ home: homedir(), configDir: getConfigDir(), }); - emitResult({ server: flags.server, results }, format); + ctx.output.result({ server: flags.server, results }); }, }); diff --git a/packages/commands/src/commands/mcp/list.ts b/packages/commands/src/commands/mcp/list.ts index 3ef3aacd2..4d42b6c1b 100644 --- a/packages/commands/src/commands/mcp/list.ts +++ b/packages/commands/src/commands/mcp/list.ts @@ -1,11 +1,9 @@ import { defineCommand, effectiveConsoleGatewayConfig, - detectOutputFormat, BailianError, ExitCode, } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; const MCP_LIST_API = "zeldaEasy.broadscope-bailian.mcp-server.PageList"; @@ -65,7 +63,6 @@ export default defineCommand({ const type = flags.type || "OFFICIAL"; const pageNo = flags.page || 1; const pageSize = flags.pageSize || 30; - const format = detectOutputFormat(settings.output); const data = { reqDTO: { @@ -79,7 +76,7 @@ export default defineCommand({ }; if (settings.dryRun) { - emitResult({ api: MCP_LIST_API, data, ...effectiveConsoleGatewayConfig(settings) }, format); + ctx.output.result({ api: MCP_LIST_API, data, ...effectiveConsoleGatewayConfig(settings) }); return; } @@ -113,6 +110,6 @@ export default defineCommand({ streamable: item.streamable === true, })); - emitResult({ total, servers }, format); + ctx.output.result({ total, servers }); }, }); diff --git a/packages/commands/src/commands/mcp/tools.ts b/packages/commands/src/commands/mcp/tools.ts index 885ae7867..d58a856ba 100644 --- a/packages/commands/src/commands/mcp/tools.ts +++ b/packages/commands/src/commands/mcp/tools.ts @@ -1,5 +1,4 @@ -import { defineCommand, bailianMcpPath, detectOutputFormat } from "bailian-cli-core"; -import { emitResult } from "bailian-cli-runtime"; +import { defineCommand, bailianMcpPath } from "bailian-cli-core"; import { rethrowWithMcpActivateHint } from "./activate-hint.ts"; export default defineCommand({ @@ -40,10 +39,9 @@ export default defineCommand({ const code = flags.server; const previewUrl = flags.url || ctx.client.url(bailianMcpPath(code)); - const format = detectOutputFormat(settings.output); if (settings.dryRun) { - emitResult({ server: code, url: previewUrl, action: "tools/list" }, format); + ctx.output.result({ server: code, url: previewUrl, action: "tools/list" }); return; } @@ -52,7 +50,7 @@ export default defineCommand({ const connected = await ctx.client.connectBailianMcp(code, flags.url); client = connected.client; const tools = await connected.client.listTools(); - emitResult({ server: code, url: connected.url, tools }, format); + ctx.output.result({ server: code, url: connected.url, tools }); } catch (error) { if (!flags.url) { rethrowWithMcpActivateHint(error, code); diff --git a/packages/commands/src/commands/memory/add.ts b/packages/commands/src/commands/memory/add.ts index 988f499c0..96d2216c2 100644 --- a/packages/commands/src/commands/memory/add.ts +++ b/packages/commands/src/commands/memory/add.ts @@ -5,15 +5,14 @@ import { ExitCode, memoryAddAsyncPath, memoryEndpoint, - detectOutputFormat, type FlagsDef, type ParsedFlags, type MemoryAddAsyncResponse, type MemoryAddRequest, type MemoryEvent, + type LocalizedText, type MemoryMessage, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { MEMORY_LIBRARY_FLAG, MEMORY_RATE_LIMIT_NOTE, @@ -145,7 +144,12 @@ function assertMessageShapes(messages: MemoryMessage[]): void { } /** Text rendering of terminal events: result lines for extraction tasks, one status line per profile task. */ -function emitEventsText(events: MemoryEvent[]): void { +function renderEventsText( + events: MemoryEvent[], + localize: (text: LocalizedText) => string, +): string { + const lines: string[] = []; + let emitted = 0; for (const event of events) { const status = event.status ?? "UNKNOWN"; @@ -153,7 +157,12 @@ function emitEventsText(events: MemoryEvent[]): void { if (isMemoryEventSuccess(status)) { const results = event.result ?? []; if (results.length === 0) { - emitBare(`${label}: ${status} (no memory change)`); + lines.push( + localize({ + "en-US": `${label}: ${status} (no memory change)`, + "zh-CN": `${label}: ${status}(记忆无变更)`, + }), + ); emitted += 1; continue; } @@ -162,16 +171,19 @@ function emitEventsText(events: MemoryEvent[]): void { event.resource_type === "user_profile" ? `user_profile ${item.name ?? event.resource_id ?? "-"}` : (item.memory_node_id ?? "-"); - emitBare(`[${item.event ?? "ADD"}] ${target} ${item.content ?? ""}`); + lines.push(`[${item.event ?? "ADD"}] ${target} ${item.content ?? ""}`); emitted += 1; } continue; } // UNRECORDED and any other non-success terminal status: surface as-is. - emitBare(`${label}: ${status}`); + lines.push(`${label}: ${status}`); emitted += 1; } - if (emitted === 0) emitBare("No memory node changed."); + if (emitted === 0) + lines.push(localize({ "en-US": "No memory node changed.", "zh-CN": "未更改任何记忆节点。" })); + + return lines.join("\n"); } export default defineCommand({ @@ -285,12 +297,12 @@ export default defineCommand({ } if (flags.libraryId) body.memory_library_id = flags.libraryId; - const format = detectOutputFormat(settings.output); + const format = settings.output; const workspaceId = resolveWorkspaceId(ctx); const url = memoryEndpoint(workspaceId, memoryAddAsyncPath()); if (settings.dryRun) { - emitResult({ endpoint: url, method: "POST", request: body }, format); + ctx.output.result({ endpoint: url, method: "POST", request: body }); return; } @@ -305,15 +317,22 @@ export default defineCommand({ // Preserve the submission receipt even when completion cannot be tracked. if (waitSeconds === 0 || !eventId) { - if (settings.quiet || format === "text") { - emitBare( - eventId - ? `Submitted: event ${eventId} (${(submission.events ?? []).length} task(s) pending).` - : "Submitted (no event ID returned).", - ); - } else { - emitResult(submission, format); - } + ctx.output.result( + submission, + format === "text" + ? { + text: eventId + ? ctx.localize({ + "en-US": `Submitted: event ${eventId} (${(submission.events ?? []).length} task(s) pending).`, + "zh-CN": `已提交:事件 ${eventId}(${(submission.events ?? []).length} 个任务待完成)。`, + }) + : ctx.localize({ + "en-US": "Submitted (no event ID returned).", + "zh-CN": "已提交(未返回事件 ID)。", + }), + } + : undefined, + ); if (waitSeconds !== 0) { throw new BailianError( "The submission returned no event_id; extraction completion cannot be confirmed. / 提交响应未返回 event_id,无法确认抽取完成。", @@ -335,7 +354,11 @@ export default defineCommand({ const statuses = events .map((event) => `${event.resource_type ?? "task"}=${event.status ?? "UNKNOWN"}`) .join(" "); - process.stderr.write(`[poll ${round}] ${eventId} ${statuses}\n`); + ctx.output.debug({ + code: "MEMORY_EXTRACTION_POLL", + message: { "en-US": "Polling memory extraction", "zh-CN": "正在查询记忆抽取状态" }, + details: { round, event_id: eventId, statuses }, + }); } : undefined, }); @@ -354,11 +377,12 @@ export default defineCommand({ // Partial failure: surface the completed part first, then propagate the // failed events verbatim (server errors are never translated). - if (settings.quiet || format === "text") { - emitEventsText(reportableEvents); - } else { - emitResult({ request_id: submission.request_id, event_id: eventId, events }, format); - } + ctx.output.result( + { request_id: submission.request_id, event_id: eventId, events }, + format === "text" + ? { text: renderEventsText(reportableEvents, (text) => ctx.localize(text)) } + : undefined, + ); if (failedEvents.length > 0) { throw new BailianError( diff --git a/packages/commands/src/commands/memory/delete.ts b/packages/commands/src/commands/memory/delete.ts index 8fa578fa7..6cce43d50 100644 --- a/packages/commands/src/commands/memory/delete.ts +++ b/packages/commands/src/commands/memory/delete.ts @@ -1,11 +1,4 @@ -import { - defineCommand, - memoryEndpoint, - memoryNodePath, - detectOutputFormat, - type FlagsDef, -} from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; +import { defineCommand, memoryEndpoint, memoryNodePath, type FlagsDef } from "bailian-cli-core"; import { buildQuery } from "../shared/params.ts"; import { MEMORY_LIBRARY_FLAG, @@ -59,13 +52,13 @@ export default defineCommand({ const { settings, flags } = ctx; const nodeId = flags.nodeId; - const format = detectOutputFormat(settings.output); + const format = settings.output; const url = memoryEndpoint(resolveWorkspaceId(ctx), memoryNodePath(nodeId)) + buildQuery({ memory_library_id: flags.libraryId }); if (settings.dryRun) { - emitResult({ endpoint: url, method: "DELETE" }, format); + ctx.output.result({ endpoint: url, method: "DELETE" }); return; } @@ -74,10 +67,16 @@ export default defineCommand({ method: "DELETE", }); - if (settings.quiet || format === "text") { - emitBare(`Memory node ${nodeId} deleted.`); - } else { - emitResult(response, format); - } + ctx.output.result( + response, + format === "text" + ? { + text: ctx.localize({ + "en-US": `Memory node ${nodeId} deleted.`, + "zh-CN": `记忆节点 ${nodeId} 已删除。`, + }), + } + : undefined, + ); }, }); diff --git a/packages/commands/src/commands/memory/list.ts b/packages/commands/src/commands/memory/list.ts index e10a38a91..e065acc2e 100644 --- a/packages/commands/src/commands/memory/list.ts +++ b/packages/commands/src/commands/memory/list.ts @@ -2,11 +2,9 @@ import { defineCommand, memoryEndpoint, memoryListPath, - detectOutputFormat, type FlagsDef, type MemoryNodeListResponse, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { buildQuery } from "../shared/params.ts"; import { MEMORY_LIBRARY_FLAG, @@ -72,7 +70,7 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const format = settings.output; const url = memoryEndpoint(resolveWorkspaceId(ctx), memoryListPath()) + buildQuery({ @@ -84,7 +82,7 @@ export default defineCommand({ }); if (settings.dryRun) { - emitResult({ endpoint: url, method: "GET" }, format); + ctx.output.result({ endpoint: url, method: "GET" }); return; } @@ -93,23 +91,32 @@ export default defineCommand({ method: "GET", }); - if (settings.quiet || format === "text") { + ctx.output.result(response, format === "text" ? { text: renderText0() } : undefined); + function renderText0(): string { + const lines: string[] = []; if (!response.memory_nodes || response.memory_nodes.length === 0) { - emitBare("No memory nodes found."); + lines.push( + ctx.localize({ "en-US": "No memory nodes found.", "zh-CN": "未找到记忆节点。" }), + ); } else { for (const node of response.memory_nodes) { const typePrefix = node.memory_type ? `[${node.memory_type}] ` : ""; - emitBare(`${typePrefix}[${node.memory_node_id}] ${node.content}`); + lines.push(`${typePrefix}[${node.memory_node_id}] ${node.content}`); if (node.meta_data && Object.keys(node.meta_data).length > 0) { - emitBare(` meta: ${JSON.stringify(node.meta_data)}`); + lines.push(` meta: ${JSON.stringify(node.meta_data)}`); } } if (response.total !== undefined) { - emitBare(`\nTotal: ${response.total}`); + lines.push( + ctx.localize({ + "en-US": `\nTotal: ${response.total}`, + "zh-CN": `\n总计:${response.total}`, + }), + ); } } - } else { - emitResult(response, format); + + return lines.join("\n"); } }, }); diff --git a/packages/commands/src/commands/memory/node-show.ts b/packages/commands/src/commands/memory/node-show.ts index f6d71520d..ff88ac87b 100644 --- a/packages/commands/src/commands/memory/node-show.ts +++ b/packages/commands/src/commands/memory/node-show.ts @@ -2,11 +2,9 @@ import { defineCommand, memoryEndpoint, memoryNodePath, - detectOutputFormat, type FlagsDef, type MemoryNodeDetailResponse, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { MEMORY_WORKSPACE_NOTE, WORKSPACE_FLAG, resolveWorkspaceId } from "./shared.ts"; const NODE_SHOW_FLAGS = { @@ -40,11 +38,11 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const format = settings.output; const url = memoryEndpoint(resolveWorkspaceId(ctx), memoryNodePath(flags.nodeId)); if (settings.dryRun) { - emitResult({ endpoint: url, method: "GET" }, format); + ctx.output.result({ endpoint: url, method: "GET" }); return; } @@ -53,27 +51,74 @@ export default defineCommand({ method: "GET", }); - if (settings.quiet || format === "text") { + let text: string | undefined; + if (format === "text") { const node = response.memory_node; + const lines: string[] = []; if (!node) { - emitBare("Memory node not found."); - return; + lines.push( + ctx.localize({ "en-US": "Memory node not found.", "zh-CN": "未找到记忆节点。" }), + ); + } else { + lines.push(`[${node.memory_node_id}] ${node.content}`); + lines.push( + ctx.localize({ + "en-US": ` type: ${node.memory_type ?? "-"} status: ${node.status ?? "-"}`, + "zh-CN": ` 类型: ${node.memory_type ?? "-"} 状态: ${node.status ?? "-"}`, + }), + ); + if (node.project_id) + lines.push( + ctx.localize({ + "en-US": ` project: ${node.project_id}`, + "zh-CN": ` 项目: ${node.project_id}`, + }), + ); + if (node.timestamp !== undefined) + lines.push( + ctx.localize({ + "en-US": ` timestamp: ${node.timestamp}`, + "zh-CN": ` 时间戳: ${node.timestamp}`, + }), + ); + if (node.created_at !== undefined) + lines.push( + ctx.localize({ + "en-US": ` created_at: ${node.created_at}`, + "zh-CN": ` 创建时间: ${node.created_at}`, + }), + ); + if (node.updated_at !== undefined) + lines.push( + ctx.localize({ + "en-US": ` updated_at: ${node.updated_at}`, + "zh-CN": ` 更新时间: ${node.updated_at}`, + }), + ); + if (node.meta_data && Object.keys(node.meta_data).length > 0) + lines.push( + ctx.localize({ + "en-US": ` meta: ${JSON.stringify(node.meta_data)}`, + "zh-CN": ` 元数据: ${JSON.stringify(node.meta_data)}`, + }), + ); + if (node.media_desc) + lines.push( + ctx.localize({ + "en-US": ` media_desc: ${node.media_desc}`, + "zh-CN": ` 媒体描述: ${node.media_desc}`, + }), + ); + if (node.media_urls && node.media_urls.length > 0) + lines.push( + ctx.localize({ + "en-US": ` media_urls: ${node.media_urls.join(", ")}`, + "zh-CN": ` 媒体 URL: ${node.media_urls.join(", ")}`, + }), + ); } - emitBare(`[${node.memory_node_id}] ${node.content}`); - emitBare(` type: ${node.memory_type ?? "-"} status: ${node.status ?? "-"}`); - if (node.project_id) emitBare(` project: ${node.project_id}`); - if (node.timestamp !== undefined) emitBare(` timestamp: ${node.timestamp}`); - if (node.created_at !== undefined) emitBare(` created_at: ${node.created_at}`); - if (node.updated_at !== undefined) emitBare(` updated_at: ${node.updated_at}`); - if (node.meta_data && Object.keys(node.meta_data).length > 0) { - emitBare(` meta: ${JSON.stringify(node.meta_data)}`); - } - if (node.media_desc) emitBare(` media_desc: ${node.media_desc}`); - if (node.media_urls && node.media_urls.length > 0) { - emitBare(` media_urls: ${node.media_urls.join(", ")}`); - } - } else { - emitResult(response, format); + text = lines.join("\n"); } + ctx.output.result(response, text === undefined ? undefined : { text }); }, }); diff --git a/packages/commands/src/commands/memory/profile-create.ts b/packages/commands/src/commands/memory/profile-create.ts index 3cfd07524..d2561cec2 100644 --- a/packages/commands/src/commands/memory/profile-create.ts +++ b/packages/commands/src/commands/memory/profile-create.ts @@ -3,14 +3,12 @@ import { UsageError, memoryEndpoint, profileSchemaPath, - detectOutputFormat, type FlagsDef, type MemoryPlanVersion, type ProfileAttribute, type ProfileSchemaCreateRequest, type ProfileSchemaCreateResponse, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { MEMORY_LIBRARY_FLAG, MEMORY_WORKSPACE_NOTE, @@ -108,11 +106,11 @@ export default defineCommand({ if (flags.extractScene) body.extract_scene = flags.extractScene; if (flags.libraryId) body.memory_library_id = flags.libraryId; - const format = detectOutputFormat(settings.output); + const format = settings.output; const url = memoryEndpoint(resolveWorkspaceId(ctx), profileSchemaPath()); if (settings.dryRun) { - emitResult({ endpoint: url, method: "POST", request: body }, format); + ctx.output.result({ endpoint: url, method: "POST", request: body }); return; } @@ -122,10 +120,16 @@ export default defineCommand({ body, }); - if (settings.quiet || format === "text") { - emitBare(`Profile schema created: ${response.profile_schema_id}`); - } else { - emitResult(response, format); - } + ctx.output.result( + response, + format === "text" + ? { + text: ctx.localize({ + "en-US": `Profile schema created: ${response.profile_schema_id}`, + "zh-CN": `画像 Schema 已创建:${response.profile_schema_id}`, + }), + } + : undefined, + ); }, }); diff --git a/packages/commands/src/commands/memory/profile-delete.ts b/packages/commands/src/commands/memory/profile-delete.ts index 142da8091..34e0fe7cc 100644 --- a/packages/commands/src/commands/memory/profile-delete.ts +++ b/packages/commands/src/commands/memory/profile-delete.ts @@ -2,10 +2,8 @@ import { defineCommand, memoryEndpoint, profileSchemaItemPath, - detectOutputFormat, type FlagsDef, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { buildQuery } from "../shared/params.ts"; import { MEMORY_LIBRARY_FLAG, @@ -59,13 +57,13 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const format = settings.output; const url = memoryEndpoint(resolveWorkspaceId(ctx), profileSchemaItemPath(flags.schemaId)) + buildQuery({ memory_library_id: flags.libraryId }); if (settings.dryRun) { - emitResult({ endpoint: url, method: "DELETE" }, format); + ctx.output.result({ endpoint: url, method: "DELETE" }); return; } @@ -74,10 +72,16 @@ export default defineCommand({ method: "DELETE", }); - if (settings.quiet || format === "text") { - emitBare(`Profile schema ${flags.schemaId} deleted.`); - } else { - emitResult(response, format); - } + ctx.output.result( + response, + format === "text" + ? { + text: ctx.localize({ + "en-US": `Profile schema ${flags.schemaId} deleted.`, + "zh-CN": `画像模板 ${flags.schemaId} 已删除。`, + }), + } + : undefined, + ); }, }); diff --git a/packages/commands/src/commands/memory/profile-get.ts b/packages/commands/src/commands/memory/profile-get.ts index 9217758c5..b5f3f9169 100644 --- a/packages/commands/src/commands/memory/profile-get.ts +++ b/packages/commands/src/commands/memory/profile-get.ts @@ -2,11 +2,9 @@ import { defineCommand, memoryEndpoint, userProfilePath, - detectOutputFormat, type FlagsDef, type UserProfileResponse, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { buildQuery } from "../shared/params.ts"; import { MEMORY_LIBRARY_FLAG, @@ -62,7 +60,7 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const format = settings.output; const url = memoryEndpoint(resolveWorkspaceId(ctx), userProfilePath(flags.schemaId)) + buildQuery({ @@ -71,7 +69,7 @@ export default defineCommand({ }); if (settings.dryRun) { - emitResult({ endpoint: url, method: "GET" }, format); + ctx.output.result({ endpoint: url, method: "GET" }); return; } @@ -80,26 +78,32 @@ export default defineCommand({ method: "GET", }); - if (settings.quiet || format === "text") { + ctx.output.result(response, format === "text" ? { text: renderText0() } : undefined); + function renderText0(): string { + const lines: string[] = []; const attributes = response.profile?.attributes ?? []; if (attributes.length === 0) { - emitBare("No profile data found."); + lines.push( + ctx.localize({ "en-US": "No profile data found.", "zh-CN": "未找到画像数据。" }), + ); } else { for (const attribute of attributes) { const valueItems = attribute.value_items ?? []; if (valueItems.length > 0) { for (const item of valueItems) { - emitBare( - `${attribute.name}: ${item.value ?? "(empty)"} (item ${item.item_id ?? "-"}, ${item.status ?? "-"})`, + lines.push( + `${attribute.name}: ${item.value ?? ctx.localize({ "en-US": "(empty)", "zh-CN": "(空)" })} (item ${item.item_id ?? "-"}, ${item.status ?? "-"})`, ); } } else { - emitBare(`${attribute.name}: ${attribute.value ?? "(empty)"}`); + lines.push( + `${attribute.name}: ${attribute.value ?? ctx.localize({ "en-US": "(empty)", "zh-CN": "(空)" })}`, + ); } } } - } else { - emitResult(response, format); + + return lines.join("\n"); } }, }); diff --git a/packages/commands/src/commands/memory/profile-list.ts b/packages/commands/src/commands/memory/profile-list.ts index f4d1cd57e..72bc86ed0 100644 --- a/packages/commands/src/commands/memory/profile-list.ts +++ b/packages/commands/src/commands/memory/profile-list.ts @@ -2,11 +2,9 @@ import { defineCommand, memoryEndpoint, profileSchemaPath, - detectOutputFormat, type FlagsDef, type ProfileSchemaListResponse, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { buildQuery } from "../shared/params.ts"; import { MEMORY_LIBRARY_FLAG, @@ -53,7 +51,7 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const format = settings.output; const url = memoryEndpoint(resolveWorkspaceId(ctx), profileSchemaPath()) + buildQuery({ @@ -63,7 +61,7 @@ export default defineCommand({ }); if (settings.dryRun) { - emitResult({ endpoint: url, method: "GET" }, format); + ctx.output.result({ endpoint: url, method: "GET" }); return; } @@ -72,22 +70,30 @@ export default defineCommand({ method: "GET", }); - if (settings.quiet || format === "text") { + let text: string | undefined; + if (format === "text") { const schemas = response.profile_schemas ?? []; + const lines: string[] = []; if (schemas.length === 0) { - emitBare("No profile schemas found."); + lines.push( + ctx.localize({ "en-US": "No profile schemas found.", "zh-CN": "未找到画像模板。" }), + ); } else { for (const schema of schemas) { const planSuffix = schema.plan_version ? ` (${schema.plan_version})` : ""; - emitBare(`[${schema.profile_schema_id}] ${schema.name}${planSuffix}`); - if (schema.description) emitBare(` ${schema.description}`); - } - if (response.total !== undefined) { - emitBare(`\nTotal: ${response.total}`); + lines.push(`[${schema.profile_schema_id}] ${schema.name}${planSuffix}`); + if (schema.description) lines.push(` ${schema.description}`); } + if (response.total !== undefined) + lines.push( + ctx.localize({ + "en-US": `\nTotal: ${response.total}`, + "zh-CN": `\n总数: ${response.total}`, + }), + ); } - } else { - emitResult(response, format); + text = lines.join("\n"); } + ctx.output.result(response, text === undefined ? undefined : { text }); }, }); diff --git a/packages/commands/src/commands/memory/profile-show.ts b/packages/commands/src/commands/memory/profile-show.ts index 0cf9ae849..479242f71 100644 --- a/packages/commands/src/commands/memory/profile-show.ts +++ b/packages/commands/src/commands/memory/profile-show.ts @@ -2,11 +2,9 @@ import { defineCommand, memoryEndpoint, profileSchemaItemPath, - detectOutputFormat, type FlagsDef, type ProfileSchemaDetailResponse, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { buildQuery } from "../shared/params.ts"; import { MEMORY_LIBRARY_FLAG, @@ -55,13 +53,13 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const format = settings.output; const url = memoryEndpoint(resolveWorkspaceId(ctx), profileSchemaItemPath(flags.schemaId)) + buildQuery({ memory_library_id: flags.libraryId }); if (settings.dryRun) { - emitResult({ endpoint: url, method: "GET" }, format); + ctx.output.result({ endpoint: url, method: "GET" }); return; } @@ -70,23 +68,42 @@ export default defineCommand({ method: "GET", }); - if (settings.quiet || format === "text") { - emitBare(`Name: ${response.name ?? "(none)"}`); - if (response.description) emitBare(`Description: ${response.description}`); + let text: string | undefined; + if (format === "text") { + const name = response.name ?? ctx.localize({ "en-US": "(none)", "zh-CN": "(无)" }); + const lines = [ctx.localize({ "en-US": `Name: ${name}`, "zh-CN": `名称: ${name}` })]; + if (response.description) + lines.push( + ctx.localize({ + "en-US": `Description: ${response.description}`, + "zh-CN": `描述: ${response.description}`, + }), + ); const attributes = response.attributes ?? []; if (attributes.length === 0) { - emitBare("No attributes."); + lines.push(ctx.localize({ "en-US": "No attributes.", "zh-CN": "暂无属性。" })); } else { - emitBare("Attributes:"); + lines.push(ctx.localize({ "en-US": "Attributes:", "zh-CN": "属性:" })); for (const attribute of attributes) { - emitBare(` [${attribute.attribute_id}] ${attribute.name}`); - if (attribute.description) emitBare(` desc: ${attribute.description}`); + lines.push(` [${attribute.attribute_id}] ${attribute.name}`); + if (attribute.description) + lines.push( + ctx.localize({ + "en-US": ` desc: ${attribute.description}`, + "zh-CN": ` 描述: ${attribute.description}`, + }), + ); if (attribute.default_value !== undefined) - emitBare(` default: ${attribute.default_value}`); + lines.push( + ctx.localize({ + "en-US": ` default: ${attribute.default_value}`, + "zh-CN": ` 默认值: ${attribute.default_value}`, + }), + ); } } - } else { - emitResult(response, format); + text = lines.join("\n"); } + ctx.output.result(response, text === undefined ? undefined : { text }); }, }); diff --git a/packages/commands/src/commands/memory/profile-update.ts b/packages/commands/src/commands/memory/profile-update.ts index 035985652..dfff07b62 100644 --- a/packages/commands/src/commands/memory/profile-update.ts +++ b/packages/commands/src/commands/memory/profile-update.ts @@ -3,13 +3,11 @@ import { UsageError, memoryEndpoint, profileSchemaItemPath, - detectOutputFormat, type FlagsDef, type MemoryPlanVersion, type ProfileSchemaAttributeOperation, type ProfileSchemaUpdateRequest, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { MEMORY_LIBRARY_FLAG, MEMORY_WORKSPACE_NOTE, @@ -145,11 +143,11 @@ export default defineCommand({ if (flags.extractScene) body.extract_scene = flags.extractScene; if (flags.libraryId) body.memory_library_id = flags.libraryId; - const format = detectOutputFormat(settings.output); + const format = settings.output; const url = memoryEndpoint(resolveWorkspaceId(ctx), profileSchemaItemPath(flags.schemaId)); if (settings.dryRun) { - emitResult({ endpoint: url, method: "PATCH", request: body }, format); + ctx.output.result({ endpoint: url, method: "PATCH", request: body }); return; } @@ -159,10 +157,16 @@ export default defineCommand({ body, }); - if (settings.quiet || format === "text") { - emitBare(`Profile schema ${flags.schemaId} updated.`); - } else { - emitResult(response, format); - } + ctx.output.result( + response, + format === "text" + ? { + text: ctx.localize({ + "en-US": `Profile schema ${flags.schemaId} updated.`, + "zh-CN": `画像模板 ${flags.schemaId} 已更新。`, + }), + } + : undefined, + ); }, }); diff --git a/packages/commands/src/commands/memory/search.ts b/packages/commands/src/commands/memory/search.ts index f2f945278..a8fddf6c7 100644 --- a/packages/commands/src/commands/memory/search.ts +++ b/packages/commands/src/commands/memory/search.ts @@ -2,7 +2,6 @@ import { defineCommand, memoryEndpoint, memorySearchPath, - detectOutputFormat, type FlagsDef, type ParsedFlags, type MemoryMessage, @@ -11,7 +10,6 @@ import { type MemorySearchResponse, type MemoryType, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { MEMORY_LIBRARY_FLAG, MEMORY_RATE_LIMIT_NOTE, @@ -186,11 +184,11 @@ export default defineCommand({ if (memoryTypes?.length) body.memory_types = memoryTypes as MemoryType[]; if (flags.libraryId) body.memory_library_id = flags.libraryId; - const format = detectOutputFormat(settings.output); + const format = settings.output; const url = memoryEndpoint(resolveWorkspaceId(ctx), memorySearchPath()); if (settings.dryRun) { - emitResult({ endpoint: url, method: "POST", request: body }, format); + ctx.output.result({ endpoint: url, method: "POST", request: body }); return; } @@ -200,18 +198,22 @@ export default defineCommand({ body, }); - if (settings.quiet || format === "text") { + ctx.output.result(response, format === "text" ? { text: renderText0() } : undefined); + function renderText0(): string { + const lines: string[] = []; if (!response.memory_nodes || response.memory_nodes.length === 0) { - emitBare("No memory nodes found."); + lines.push( + ctx.localize({ "en-US": "No memory nodes found.", "zh-CN": "未找到记忆节点。" }), + ); } else { for (const node of response.memory_nodes) { const typePrefix = node.memory_type ? `[${node.memory_type}] ` : ""; const scoreSuffix = node.score !== undefined ? ` (score ${node.score})` : ""; - emitBare(`${typePrefix}[${node.memory_node_id}] ${node.content}${scoreSuffix}`); + lines.push(`${typePrefix}[${node.memory_node_id}] ${node.content}${scoreSuffix}`); } } - } else { - emitResult(response, format); + + return lines.join("\n"); } }, }); diff --git a/packages/commands/src/commands/memory/skill-export.ts b/packages/commands/src/commands/memory/skill-export.ts index 494196782..ee462d274 100644 --- a/packages/commands/src/commands/memory/skill-export.ts +++ b/packages/commands/src/commands/memory/skill-export.ts @@ -2,11 +2,9 @@ import { defineCommand, memoryEndpoint, memorySkillExportPath, - detectOutputFormat, type FlagsDef, type MemorySkillExportResponse, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { MEMORY_WORKSPACE_NOTE, WORKSPACE_FLAG, resolveWorkspaceId } from "./shared.ts"; const SKILL_EXPORT_FLAGS = { @@ -43,11 +41,11 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const format = settings.output; const url = memoryEndpoint(resolveWorkspaceId(ctx), memorySkillExportPath(flags.nodeId)); if (settings.dryRun) { - emitResult({ endpoint: url, method: "GET" }, format); + ctx.output.result({ endpoint: url, method: "GET" }); return; } @@ -56,15 +54,18 @@ export default defineCommand({ method: "GET", }); - if (settings.quiet || format === "text") { - const node = response.memory_node; - if (!node) { - emitBare("Skill memory node not found."); - return; - } - emitBare(node.content); - } else { - emitResult(response, format); - } + ctx.output.result( + response, + format === "text" + ? { + text: response.memory_node + ? String(response.memory_node.content) + : ctx.localize({ + "en-US": "Skill memory node not found.", + "zh-CN": "未找到技能记忆节点。", + }), + } + : undefined, + ); }, }); diff --git a/packages/commands/src/commands/memory/update.ts b/packages/commands/src/commands/memory/update.ts index 6f7e100d8..ad7495972 100644 --- a/packages/commands/src/commands/memory/update.ts +++ b/packages/commands/src/commands/memory/update.ts @@ -2,11 +2,9 @@ import { defineCommand, memoryEndpoint, memoryNodePath, - detectOutputFormat, type FlagsDef, type MemoryNodeUpdateRequest, } from "bailian-cli-core"; -import { emitResult, emitBare } from "bailian-cli-runtime"; import { MEMORY_LIBRARY_FLAG, MEMORY_WORKSPACE_NOTE, @@ -112,11 +110,11 @@ export default defineCommand({ } if (flags.libraryId) body.memory_library_id = flags.libraryId; - const format = detectOutputFormat(settings.output); + const format = settings.output; const url = memoryEndpoint(resolveWorkspaceId(ctx), memoryNodePath(nodeId)); if (settings.dryRun) { - emitResult({ endpoint: url, method: "PATCH", request: body }, format); + ctx.output.result({ endpoint: url, method: "PATCH", request: body }); return; } @@ -126,10 +124,16 @@ export default defineCommand({ body, }); - if (settings.quiet || format === "text") { - emitBare(`Memory node ${nodeId} updated.`); - } else { - emitResult(response, format); - } + ctx.output.result( + response, + format === "text" + ? { + text: ctx.localize({ + "en-US": `Memory node ${nodeId} updated.`, + "zh-CN": `记忆节点 ${nodeId} 已更新。`, + }), + } + : undefined, + ); }, }); diff --git a/packages/commands/src/commands/model/code.ts b/packages/commands/src/commands/model/code.ts index eb8995290..94cd5d8fc 100644 --- a/packages/commands/src/commands/model/code.ts +++ b/packages/commands/src/commands/model/code.ts @@ -3,13 +3,11 @@ import { ExitCode, anonymousConsoleCall, defineCommand, - detectOutputFormat, fetchModelDetail, type ModelGroupItem, type ModelSampleCodeV2, type ModelSampleSnippet, } from "bailian-cli-core"; -import { emitBare, emitResult } from "bailian-cli-runtime"; import { pickTrunkItems } from "./shared.ts"; /** `--api` values map onto the API-style keys used inside `sampleCodeV2`. */ @@ -136,6 +134,7 @@ export default defineCommand({ "zh-CN": "打印调用指定模型的 SDK 示例代码", }, auth: "none", + defaultOutput: "text", usageArgs: "--model [--sdk ] [--api