From 56c5fc6f267b86748a368c038f294e871d17cc5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=9A=B0=EC=B2=A0?= Date: Fri, 2 Oct 2026 17:08:05 +0900 Subject: [PATCH 1/2] fix(cli): support JSON output on the default alias command --- .../src/content/docs/ko/reference/cli.md | 4 +++ docs-site/src/content/docs/reference/cli.md | 4 +++ scripts/test-layout/layout.json | 1 + src/cli/alias.ts | 2 +- structure/runtime.md | 2 +- tests/cli/cli-alias-json.test.ts | 27 +++++++++++++++++++ tests/fixtures/test-layout-expected.json | 1 + 7 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 tests/cli/cli-alias-json.test.ts diff --git a/docs-site/src/content/docs/ko/reference/cli.md b/docs-site/src/content/docs/ko/reference/cli.md index da0ed2b0248..28b4edf3377 100644 --- a/docs-site/src/content/docs/ko/reference/cli.md +++ b/docs-site/src/content/docs/ko/reference/cli.md @@ -40,3 +40,7 @@ Windows x64 설치 관측은 [`attest` 명령](/ko/reference/cli/agents/)을 참 `ocx --version`, `ocx -v`, `ocx version`은 스크립트가 읽기 좋은 한 줄짜리 버전을 출력하고 종료합니다. 일반 도움말에는 두 개의 디스패치 대상이 의도적으로 빠져 있습니다. `__refresh-version [preview]`는 분리된 프로세스에서 업데이트 알림 캐시를 새로 고치고, `__gui-update-worker [latest|preview] [restart]`는 대시보드 업데이트 작업을 실행합니다. 이들은 구현 세부 사항일 뿐이며 안정적인 사용자 명령이 아닙니다. 대시보드는 worker PID를 기록하고, worker가 죽은 활성 작업은 복구하며, PID가 없는 오래된 활성 기록은 10분 뒤 오래된 것으로 취급하고, 살아 있는 worker를 동시 업데이트로부터 보호합니다. + +## Default alias listing + +`ocx alias --json`은 `ocx alias list --json`과 같습니다. `--json`은 명시한 alias 작업의 앞이나 뒤에 둘 수 있습니다. diff --git a/docs-site/src/content/docs/reference/cli.md b/docs-site/src/content/docs/reference/cli.md index dbfcd400066..88714d69ad9 100644 --- a/docs-site/src/content/docs/reference/cli.md +++ b/docs-site/src/content/docs/reference/cli.md @@ -156,3 +156,7 @@ refreshes the update-notification cache in a detached process, and implementation details, not stable user-facing commands. The dashboard records the worker PID, recovers an active job whose worker died, treats older PID-less active records as stale after ten minutes, and protects a live worker from concurrent updates. + +## Default alias listing + +`ocx alias --json` is equivalent to `ocx alias list --json`. The output flag can precede or follow an explicit alias action. diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index 48a167a139a..8fa72a3bb8c 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -126,6 +126,7 @@ "anthropic-account-pause.test.ts": "adapters/anthropic", "anthropic-account-threshold.test.ts": "adapters/anthropic", "anthropic-combo-account-cooldown.test.ts": "adapters/anthropic", + "cli-alias-json.test.ts": "cli", "cli-anthropic-account-threshold.test.ts": "cli", "anthropic-model-routes.test.ts": "adapters/anthropic", "anthropic-agentrouter-language-framing.test.ts": "adapters/anthropic", diff --git a/src/cli/alias.ts b/src/cli/alias.ts index 462d98e2299..6691b981c91 100644 --- a/src/cli/alias.ts +++ b/src/cli/alias.ts @@ -14,8 +14,8 @@ function selector(value: string): { provider: string; model?: string } { export async function handleAliasCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise { const args = [...argv]; - const action = (args.shift() ?? "list").toLowerCase(); const wantsJson = takeFlag(args, "--json"); + const action = (args.shift() ?? "list").toLowerCase(); if (action === "list") { rejectArgs(args, USAGE); const result = await runtimeRequest>("/api/aliases", {}, deps); diff --git a/structure/runtime.md b/structure/runtime.md index 714f0e144e5..51a68deeaf4 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -53,7 +53,7 @@ this wire projection does not change the usage ledger. ## CLI readiness diagnostics -Catalog-derived reasoning-level diagnostics are escaped only at the human-output boundary, which `src/cli/runtime-api.ts` owns alongside the human/JSON print split. Every CLI path that prints a hub-supplied catalog value renders it there: the first-time refusal in `src/cli/connect.ts` and the connected `ocx sync` refusal in `src/cli/dispatch.ts`. C0/C1 controls, DEL, and Unicode line/paragraph separators print as visible hexadecimal escapes; structured status retains the exact reason, and a rendered failure keeps the domain error as its `cause`. The ready/unverified/incompatible classification and exit policy are unchanged. +Catalog-derived reasoning-level diagnostics are escaped only at the human-output boundary, which `src/cli/runtime-api.ts` owns alongside the human/JSON print split. Every CLI path that prints a hub-supplied catalog value renders it there: the first-time refusal in `src/cli/connect.ts` and the connected `ocx sync` refusal in `src/cli/dispatch.ts`. C0/C1 controls, DEL, and Unicode line/paragraph separators print as visible hexadecimal escapes; structured status retains the exact reason, and a rendered failure keeps the domain error as its `cause`. The ready/unverified/incompatible classification and exit policy are unchanged. `src/cli/alias.ts` consumes the JSON output flag before choosing its default list action. `ocx alias --json` and both flag positions around an explicit action preserve the selected read or mutation. ## CLI resolve and stop contracts for embedding shells diff --git a/tests/cli/cli-alias-json.test.ts b/tests/cli/cli-alias-json.test.ts new file mode 100644 index 00000000000..e010b1e92aa --- /dev/null +++ b/tests/cli/cli-alias-json.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, spyOn, test } from "bun:test"; +import { handleAliasCommand } from "../../src/cli/alias"; + +describe("alias list JSON flags", () => { + test.each([["--json"], ["list", "--json"], ["--json", "list"]].map(args => [args]))("lists aliases for %j", async args => { + const out = spyOn(console, "log").mockImplementation(() => {}); + const paths: string[] = []; + const payload = { providers: { local: "Work" }, models: {} }; + try { + expect(await handleAliasCommand(args, { baseUrl: "http://cli.test", fetchImpl: async input => { + paths.push(String(input)); return Response.json(payload); + } })).toBe(0); + expect(paths).toEqual(["http://cli.test/api/aliases"]); + expect(JSON.parse(String(out.mock.calls[0]![0]))).toEqual(payload); + } finally { out.mockRestore(); } + }); + test("a leading JSON flag does not turn mutation into a list", async () => { + const out = spyOn(console, "log").mockImplementation(() => {}); + try { + let method: string | undefined; + expect(await handleAliasCommand(["--json", "set", "local", "Work"], { + baseUrl: "http://cli.test", fetchImpl: async (_input, init) => { method = init?.method; return Response.json({ ok: true }); }, + })).toBe(0); + expect(method).toBe("PUT"); + } finally { out.mockRestore(); } + }); +}); diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index 727a2556362..6f88cb4ce65 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -132,6 +132,7 @@ "anthropic-account-pause.test.ts": "adapters/anthropic", "anthropic-account-threshold.test.ts": "adapters/anthropic", "anthropic-combo-account-cooldown.test.ts": "adapters/anthropic", + "cli-alias-json.test.ts": "cli", "cli-anthropic-account-threshold.test.ts": "cli", "anthropic-model-routes.test.ts": "adapters/anthropic", "anthropic-agentrouter-language-framing.test.ts": "adapters/anthropic", From d54ce4964792dea5a3e6a0331733b5517228c085 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=9A=B0=EC=B2=A0?= Date: Fri, 2 Oct 2026 18:38:01 +0900 Subject: [PATCH 2/2] fix: address review feedback for PR #6434 --- src/cli/alias.ts | 1 + structure/runtime.md | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/cli/alias.ts b/src/cli/alias.ts index 6691b981c91..4680f21689b 100644 --- a/src/cli/alias.ts +++ b/src/cli/alias.ts @@ -12,6 +12,7 @@ function selector(value: string): { provider: string; model?: string } { return slash < 0 ? { provider: value } : { provider: value.slice(0, slash), model: value.slice(slash + 1) }; } +/** Manage model aliases, consuming JSON output selection before the default list action. */ export async function handleAliasCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise { const args = [...argv]; const wantsJson = takeFlag(args, "--json"); diff --git a/structure/runtime.md b/structure/runtime.md index 51a68deeaf4..c13abdef569 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -14,7 +14,7 @@ evidence remain late and cannot widen a captured static limit. Virtual models are the sole model-identity transition: the ordinary and compact paths preserve the selected public id in diagnostics, rewrite `route.modelId` to the upstream wire id, and atomically replace `route.staticPolicy` before adapter or capability decisions continue. Model aliases are -resolved before the route result is built, so their policy is already keyed by the native wire id. `src/router.ts` applies bare `blockedModelRedirects` keys after this resolution; ordinary targets substitute one upstream model id on the selected provider/account, including slash-valued targets, while only targets explicitly naming a different configured provider reroute. Qualified source keys take precedence only for those cross-provider targets. A cross-provider redirect sets `RouteResult.credentialDomainRewrite`, which Responses and Chat header scoping treat like combo/policy routes, so a caller credential for the source route is never forwarded. +resolved before the route result is built, so their policy is already keyed by the native wire id. `src/router.ts` applies bare `blockedModelRedirects` keys after this resolution; ordinary targets substitute one upstream model id on the selected provider/account, including slash-valued targets, while only targets explicitly naming a different configured provider reroute. Qualified source keys take precedence only for those cross-provider targets. A cross-provider redirect sets `RouteResult.credentialDomainRewrite`, which Responses and Chat header scoping treat like combo/policy routes, so a caller credential for the source route is never forwarded. `src/cli/alias.ts` consumes the JSON output flag before choosing its default list action. `ocx alias --json` and both flag positions around an explicit action preserve the selected read or mutation. Live selector hints obey the [credential-scoped cache contract](catalog.md): a selection change cannot reuse the previous credential's roster to choose an alias target. Passive OAuth observation neither refreshes credentials nor repairs their storage. Cross-provider redirects require an enabled destination with a resolved key, usable stored OAuth account, or local/keyless auth (never source-caller forward auth), and share one visited set and five-edge budget across alias, provider, combo and policy resolution; every policy redirect must end at an eligible declared candidate, including same-provider substitutions and candidate fallback retries, and pinned account selectors reject provider changes. Decision traces name the final provider/model while policy candidates retain selection evidence and combos retain their route kind and redirect reason. @@ -53,7 +53,7 @@ this wire projection does not change the usage ledger. ## CLI readiness diagnostics -Catalog-derived reasoning-level diagnostics are escaped only at the human-output boundary, which `src/cli/runtime-api.ts` owns alongside the human/JSON print split. Every CLI path that prints a hub-supplied catalog value renders it there: the first-time refusal in `src/cli/connect.ts` and the connected `ocx sync` refusal in `src/cli/dispatch.ts`. C0/C1 controls, DEL, and Unicode line/paragraph separators print as visible hexadecimal escapes; structured status retains the exact reason, and a rendered failure keeps the domain error as its `cause`. The ready/unverified/incompatible classification and exit policy are unchanged. `src/cli/alias.ts` consumes the JSON output flag before choosing its default list action. `ocx alias --json` and both flag positions around an explicit action preserve the selected read or mutation. +Catalog-derived reasoning-level diagnostics are escaped only at the human-output boundary, which `src/cli/runtime-api.ts` owns alongside the human/JSON print split. Every CLI path that prints a hub-supplied catalog value renders it there: the first-time refusal in `src/cli/connect.ts` and the connected `ocx sync` refusal in `src/cli/dispatch.ts`. C0/C1 controls, DEL, and Unicode line/paragraph separators print as visible hexadecimal escapes; structured status retains the exact reason, and a rendered failure keeps the domain error as its `cause`. The ready/unverified/incompatible classification and exit policy are unchanged. ## CLI resolve and stop contracts for embedding shells