From c0c6a1219c4a3d8cf65ea671ae0d4bcc3dfe25b8 Mon Sep 17 00:00:00 2001 From: dakjdakd <1428823446@qq.com> Date: Sat, 3 Oct 2026 03:00:46 +0800 Subject: [PATCH] fix(cli): honor json output for config commands --- .aoci/baseline.json | 24 ++++--- aoci.code.txt | 2 +- internal/cli/config.go | 110 +++++++++++++++++++++---------- internal/cli/config_json_test.go | 97 +++++++++++++++++++++++++++ 4 files changed, 189 insertions(+), 44 deletions(-) create mode 100644 internal/cli/config_json_test.go diff --git a/.aoci/baseline.json b/.aoci/baseline.json index bc93e209..022f228d 100644 --- a/.aoci/baseline.json +++ b/.aoci/baseline.json @@ -1,7 +1,7 @@ { "version": 1, "created_at": "2026-08-07T17:44:36Z", - "updated_at": "2026-09-29T16:15:50Z", + "updated_at": "2026-10-02T19:10:57Z", "files": { ".gitattributes": { "role": "index", @@ -137,9 +137,9 @@ }, "aoci.code.txt": { "role": "index", - "sha256": "8567b2fde0aa491340d77d6dd2022c01cb0d12e566ba262678e36f7222f98206", - "size": 195231, - "normalized_sha256": "8567b2fde0aa491340d77d6dd2022c01cb0d12e566ba262678e36f7222f98206" + "sha256": "979a39b5d832a94c186d55de87c77594cb627085d4d00c71e3c5dfeabe9408c8", + "size": 195187, + "normalized_sha256": "979a39b5d832a94c186d55de87c77594cb627085d4d00c71e3c5dfeabe9408c8" }, "aoci.meta.txt": { "role": "index", @@ -800,10 +800,10 @@ }, "internal/cli/config.go": { "role": "index", - "sha256": "a8cd1f352a109f2b38ec1204f0abcde03bd316e248482a468037765c5183f6f1", - "size": 8820, - "normalized_sha256": "a8cd1f352a109f2b38ec1204f0abcde03bd316e248482a468037765c5183f6f1", - "format_sha256": "a8cd1f352a109f2b38ec1204f0abcde03bd316e248482a468037765c5183f6f1", + "sha256": "8efdc62b33ee232093b13e63f3dece3cbe0a345b763adb565d33f939de1ed9ad", + "size": 9821, + "normalized_sha256": "8efdc62b33ee232093b13e63f3dece3cbe0a345b763adb565d33f939de1ed9ad", + "format_sha256": "8efdc62b33ee232093b13e63f3dece3cbe0a345b763adb565d33f939de1ed9ad", "format_kind": "gofmt" }, "internal/cli/config_automation_test.go": { @@ -822,6 +822,14 @@ "format_sha256": "60f1f2051cb2cf9c0bcd586e385ff46d6a1fbf3920da9aea7d0c25264458ac88", "format_kind": "gofmt" }, + "internal/cli/config_json_test.go": { + "role": "observe", + "sha256": "0d2f693496f2ea29f672eddad18dbec1b242240bee02ad64f4d4833a15457679", + "size": 2990, + "normalized_sha256": "0d2f693496f2ea29f672eddad18dbec1b242240bee02ad64f4d4833a15457679", + "format_sha256": "0d2f693496f2ea29f672eddad18dbec1b242240bee02ad64f4d4833a15457679", + "format_kind": "gofmt" + }, "internal/cli/config_overview_delivery_test.go": { "role": "observe", "sha256": "6ed50cb28c05901eae849240d8494c0f3c7a89bb58c876eb947c1264f6b64655", diff --git a/aoci.code.txt b/aoci.code.txt index 116b18c8..35e557ef 100644 --- a/aoci.code.txt +++ b/aoci.code.txt @@ -96,7 +96,7 @@ cognition_migration.go[CG7L]: F:Implements governed Legacy-to-Volumes migration cognition_onboard.go[CG8M]: F:Exposes persistent model-authored onboarding from session start and evidence batching through preview, preparation, Apply, resume, status, and abort | R:code:internal/onboarding/workflow.go,code:internal/bootstrapapply/prepare.go,code:internal/migrationapply/prepare.go | A:aoci cognition onboard | S:Review preparation emits a human TTY digest-confirmation boundary; policy-bound auto preparation does not make the model an approver cognition_plan.go[CG7S]: F:Routes deterministic cognition bootstrap, migration, and candidate-validation planning commands and writes planner artifacts to operator-chosen paths | R:code:internal/fs/atomic.go | A:writePlannerArtifact,validatePlannerOutput | S:An artifact path is validated before any human is asked, because a spent confirmation cannot be reused; the file is created only when absent and is owner-readable while it can stand in for that human command_localization.go[CG7M]: F:Applies localized command names, help text, and errors without changing machine tokens | R:- | A:- | S:A flag absent from localizedFlagNames silently renders its English usage under every locale, and two message values that are byte-identical make --help fail closed instead -config.go[CG7M]: F:Implements team configuration list, merged-value get, and governed base-layer set commands | R:code:internal/config/config.go,code:textassets/catalog.go | A:aoci config list,aoci config get,aoci config set | S:Set loads only team base configuration so local endpoint overrides cannot leak; locale changes create migration state and need an MCP restart; batch cap and transport budget are range-checked here +config.go[CG7M]: F:Implements team configuration list, typed JSON get and set output, and governed base-layer updates | R:code:internal/config/config.go,code:textassets/catalog.go | A:aoci config list,aoci config get,aoci config set,--json | S:JSON mode uses Cobra output and typed values while human mode stays localized; team config remains isolated from local endpoint overrides database.go[CG7M]: F:Registers database evidence commands and manages credential-free source declarations, readiness checks, and read-only catalog inspection | R:code:internal/dbevidence/config.go,code:internal/dbevidence/access.go,code:internal/dbevidence/collector.go | A:aoci database,aoci database source | S:Config stores only the credential environment-variable name, never a secret; source add/list are offline; missing-variable guidance requires Host-environment provisioning outside Agent inputs database_cognition.go[CG7S]: F:Implements evidence-bound Database Cognition status and bootstrap workflows with a stable outer error and redacted failure diagnostics | R:code:internal/databasebootstrap/prepare.go,code:internal/dbcognition/state.go,code:internal/machinecontract/status.go | A:aoci database cognition status,aoci database cognition bootstrap,database_bootstrap_stopped,cause_code,safe_next_action | S:Stopped-bootstrap Details never include wrapped errors, source IDs, paths, endpoints, or credentials; unknown failures collapse to the generic stopped cause database_evidence.go[CG7S]: F:Implements Database Evidence bundle access, independent baseline acceptance, and Code-context projection for table cognition | R:code:internal/dbevidence/store.go,code:internal/databasebootstrap/transaction.go,code:internal/cognition/impact.go | A:aoci database evidence,aoci database baseline accept | S:Evidence bundles expose catalog structure only and report business_data_read=false; baseline acceptance may auto-bootstrap missing Database Cognition only through its governed transaction diff --git a/internal/cli/config.go b/internal/cli/config.go index 0e9faaf0..38bc0fd9 100644 --- a/internal/cli/config.go +++ b/internal/cli/config.go @@ -37,9 +37,15 @@ func init() { if err != nil { return &ExitError{Code: ExitConfig, Msg: err.Error()} } - out, _ := json.MarshalIndent(cfg, "", " ") - fmt.Println(string(out)) - return nil + if flagJSON { + return writePlannerJSON(cmd, cfg) + } + out, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + return err + } + _, err = fmt.Fprintln(cmd.OutOrStdout(), string(out)) + return err }, } @@ -57,34 +63,8 @@ func init() { return &ExitError{Code: ExitConfig, Msg: err.Error()} } - switch args[0] { - case "exclude_dirs": - fmt.Println(strings.Join(cfg.ExcludeDirs, ",")) - case "exclude_files": - fmt.Println(strings.Join(cfg.ExcludeFiles, ",")) - case "curation_exclude": - fmt.Println(strings.Join(cfg.CurationExclude, ",")) - case "index_path": - fmt.Println(cfg.IndexPath) - case "locale": - fmt.Println(cfg.Locale) - case "hook_strict": - fmt.Println(cfg.HookStrict) - case "ledger_enabled": - fmt.Println(cfg.LedgerEnabled) - case "installed_agents": - fmt.Println(strings.Join(cfg.InstalledAgents, ",")) - case "automation_mode": - fmt.Println(cfg.EffectiveAutomationMode()) - case "cognition_refresh_threshold": - fmt.Println(cfg.CognitionRefreshThreshold) - case "overview_delivery.chunk_tokens": - fmt.Println(cfg.OverviewDelivery.ChunkTokens) - case "code_cognition_batch_entries": - fmt.Println(cfg.CodeCognitionBatchLimit()) - case "maintain_transport_budget_bytes": - fmt.Println(cfg.MaintainTransportBudget()) - default: + value, ok := configValue(cfg, args[0], flagJSON) + if !ok { return &ExitError{ Code: ExitConfig, Msg: cliMessage( @@ -94,7 +74,11 @@ func init() { ), } } - return nil + if flagJSON { + return writePlannerJSON(cmd, value) + } + _, err = fmt.Fprintln(cmd.OutOrStdout(), value) + return err }, } @@ -252,18 +236,29 @@ func init() { return &ExitError{Code: ExitConfig, Msg: err.Error()} } } + if flagJSON { + value, ok := configValue(cfg, key, true) + if !ok { + return &ExitError{Code: ExitConfig, Msg: cliMessage("config.unknown_key", key, "")} + } + return writePlannerJSON(cmd, struct { + OK bool `json:"ok"` + Key string `json:"key"` + Value any `json:"value"` + }{OK: true, Key: key, Value: value}) + } if !flagQuiet { - fmt.Println(cliMessage("config.saved", key)) + _, _ = fmt.Fprintln(cmd.OutOrStdout(), cliMessage("config.saved", key)) if key == "locale" { if cfg.LocaleMigration != nil { - fmt.Println(cliMessage( + _, _ = fmt.Fprintln(cmd.OutOrStdout(), cliMessage( "config.locale_migration_pending", cfg.LocaleMigration.HeaderPending, len(cfg.LocaleMigration.EntryPaths), len(cfg.LocaleMigration.CurationPaths), )) } - fmt.Println(cliMessage("config.restart_mcp")) + _, _ = fmt.Fprintln(cmd.OutOrStdout(), cliMessage("config.restart_mcp")) } } return nil @@ -274,6 +269,51 @@ func init() { registerCommand(cmd) } +func configValue(cfg *config.Config, key string, jsonMode bool) (any, bool) { + switch key { + case "exclude_dirs": + if jsonMode { + return cfg.ExcludeDirs, true + } + return strings.Join(cfg.ExcludeDirs, ","), true + case "exclude_files": + if jsonMode { + return cfg.ExcludeFiles, true + } + return strings.Join(cfg.ExcludeFiles, ","), true + case "curation_exclude": + if jsonMode { + return cfg.CurationExclude, true + } + return strings.Join(cfg.CurationExclude, ","), true + case "index_path": + return cfg.IndexPath, true + case "locale": + return cfg.Locale, true + case "hook_strict": + return cfg.HookStrict, true + case "ledger_enabled": + return cfg.LedgerEnabled, true + case "installed_agents": + if jsonMode { + return cfg.InstalledAgents, true + } + return strings.Join(cfg.InstalledAgents, ","), true + case "automation_mode": + return cfg.EffectiveAutomationMode(), true + case "cognition_refresh_threshold": + return cfg.CognitionRefreshThreshold, true + case "overview_delivery.chunk_tokens": + return cfg.OverviewDelivery.ChunkTokens, true + case "code_cognition_batch_entries": + return cfg.CodeCognitionBatchLimit(), true + case "maintain_transport_budget_bytes": + return cfg.MaintainTransportBudget(), true + default: + return nil, false + } +} + func splitCSV(raw string) []string { out := []string{} for _, part := range strings.Split(raw, ",") { diff --git a/internal/cli/config_json_test.go b/internal/cli/config_json_test.go new file mode 100644 index 00000000..646593bd --- /dev/null +++ b/internal/cli/config_json_test.go @@ -0,0 +1,97 @@ +package cli + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/aoci-spec/aoci-code/internal/config" +) + +func TestConfigCommandsHonorJSONOutput(t *testing.T) { + root := t.TempDir() + if err := config.Save(root, legacyTestConfig()); err != nil { + t.Fatal(err) + } + + t.Run("list", func(t *testing.T) { + stdout, stderr := runConfigJSON(t, root, "list") + var value map[string]any + if err := json.Unmarshal(stdout, &value); err != nil { + t.Fatalf("config list did not return JSON: %v\n%s", err, stdout) + } + if _, ok := value["locale"]; !ok { + t.Fatalf("config list JSON omitted locale: %s", stdout) + } + if len(stderr) != 0 { + t.Fatalf("config list wrote diagnostics on success: %s", stderr) + } + }) + + t.Run("get", func(t *testing.T) { + stdout, stderr := runConfigJSON(t, root, "get", "hook_strict") + var value bool + if err := json.Unmarshal(stdout, &value); err != nil { + t.Fatalf("config get did not return a JSON scalar: %v\n%s", err, stdout) + } + if value != legacyTestConfig().HookStrict { + t.Fatalf("config get returned %v, want %v", value, legacyTestConfig().HookStrict) + } + if len(stderr) != 0 { + t.Fatalf("config get wrote diagnostics on success: %s", stderr) + } + }) + + t.Run("set", func(t *testing.T) { + stdout, stderr := runConfigJSON(t, root, "set", "ledger_enabled", "false") + var result struct { + OK bool `json:"ok"` + Key string `json:"key"` + Value bool `json:"value"` + } + if err := json.Unmarshal(stdout, &result); err != nil { + t.Fatalf("config set did not return JSON: %v\n%s", err, stdout) + } + if !result.OK || result.Key != "ledger_enabled" || result.Value { + t.Fatalf("unexpected config set result: %+v", result) + } + if len(stderr) != 0 { + t.Fatalf("config set wrote diagnostics on success: %s", stderr) + } + updated, err := config.LoadBase(root) + if err != nil { + t.Fatal(err) + } + if updated.LedgerEnabled { + t.Fatal("config set did not persist the requested value") + } + }) +} + +func TestConfigGetUsesCobraOutputWriter(t *testing.T) { + root := t.TempDir() + if err := config.Save(root, legacyTestConfig()); err != nil { + t.Fatal(err) + } + + var stdout, stderr bytes.Buffer + if code := executeCLI([]string{"--repo", root, "config", "get", "locale"}, &stdout, &stderr); code != ExitOK { + t.Fatalf("config get failed: code=%d stdout=%s stderr=%s", code, stdout.String(), stderr.String()) + } + if stdout.Len() == 0 { + t.Fatal("config get did not write to the command output writer") + } + if stderr.Len() != 0 { + t.Fatalf("config get wrote diagnostics on success: %s", stderr.String()) + } +} + +func runConfigJSON(t *testing.T, root string, args ...string) ([]byte, []byte) { + t.Helper() + var stdout, stderr bytes.Buffer + arguments := append([]string{"--repo", root, "--json", "config"}, args...) + if code := executeCLI(arguments, &stdout, &stderr); code != ExitOK { + t.Fatalf("config command failed: code=%d stdout=%s stderr=%s", code, stdout.String(), stderr.String()) + } + return stdout.Bytes(), stderr.Bytes() +}