Files
WeKnora/cli/cmd/context/use.go
T
nullkey cc8254f862 refactor(cli): drop --dry-run + introduce bare-JSON output path
Two intertwined mainstream-alignment moves bundled because they share
the migration target (every command's --json path):

1. Drop --dry-run entirely. Survey of comparable API-wrapper CLIs
   (gh, aws, stripe, lark): none expose --dry-run. The mainstream that
   does (kubectl/git/helm/ansible) operates on declarative manifests
   or local state where the preview is materially different from the
   executed action. WeKnora's CLI just echoed the same parameters
   that would have gone on the wire — the preview added no real
   signal over `--help` + reading the call site. Removes:
   - root --dry-run persistent flag + cmdutil/dryrun.go
   - DryRun fields + EmitDryRun calls in 12 write commands
   - format.Envelope.DryRun field
   - 8 corresponding *_test.go cases
   - --dry-run mention from README.md and CHANGELOG.md
   - "dry_run":false from 16 golden envelopes

2. Migrate every --json output to bare data:
   - New format.WriteJSON / WriteJSONFiltered helpers
     (cli/internal/format/bare.go) share filterArrayItems /
     filterObjectKeys / writeJQ with the (still-live for now) envelope
     filter helpers.
   - Read commands (kb/doc/session list+view, search chunks/docs/
     sessions/kb, auth list/status, agent list/view, context list,
     doctor) emit bare arrays / objects on stdout.
   - Write commands (kb create/edit/delete/pin/empty, doc upload/
     upload_recursive/delete, session delete, auth login/logout/
     refresh/token, link/unlink, context add/use/remove, agent
     invoke, chat, api, version) emit bare result objects. Risk
     classification dropped — the resource + exit code already
     convey the action.

Per-command shape changes:
   list / search       → []T   (was {ok, data:{items:[…]}})
   view                → T     (was {ok, data:T, _meta:…})
   create / edit       → T
   delete / pin / etc. → {id, …action result…}
   doctor              → {summary, checks}
   api                 → {status, headers, body}

_meta dropped on the read path:
   pagination (page/page_size/total/has_more) — agents iterate with
   --all-pages or accept --limit (gh CLI parity);
   kb_id / context echo — caller already knows what it asked for.

Acceptance contract goldens regenerated for the new bare shape.
Error envelope on stdout (PrintErrorEnvelope) stays live for now —
the envelope-infra deletion lands in the next commit.
2026-05-15 12:03:56 +08:00

146 lines
3.9 KiB
Go

package contextcmd
import (
"fmt"
"sort"
"github.com/spf13/cobra"
"github.com/Tencent/WeKnora/cli/internal/aiclient"
"github.com/Tencent/WeKnora/cli/internal/cmdutil"
"github.com/Tencent/WeKnora/cli/internal/config"
"github.com/Tencent/WeKnora/cli/internal/format"
"github.com/Tencent/WeKnora/cli/internal/iostreams"
)
// NewCmdUse builds the `weknora context use <name>` command.
func NewCmdUse(f *cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "use <name>",
Short: "Switch the default context for subsequent commands",
Long: `Switches the default context written in config.yaml. Names are case-sensitive.
The active context is what every subsequent command uses for auth + host. The
global --context flag (e.g. weknora --context staging kb list) overrides for
one command without writing to disk.
AI agents: Do NOT switch the active context unless the user explicitly asked
you to. Context selection is a user preference; one-shot overrides should use
the global --context flag instead, which writes nothing to disk.`,
Example: ` weknora context use staging # persist switch
weknora --context staging kb list # one-shot override (no disk write)
weknora context use --help # this help`,
Args: cobra.ExactArgs(1),
RunE: func(c *cobra.Command, args []string) error {
return runUse(args[0])
},
}
aiclient.SetAgentHelp(cmd, "Switches default CLI context. Returns previous_context + current_context. Errors with hint when name unknown.")
return cmd
}
type useResult struct {
CurrentContext string `json:"current_context"`
PreviousContext string `json:"previous_context,omitempty"`
}
func runUse(name string) error {
cfg, err := config.Load()
if err != nil {
return err
}
if _, ok := cfg.Contexts[name]; !ok {
return notFoundError(name, cfg)
}
prev := cfg.CurrentContext
cfg.CurrentContext = name
if err := config.Save(cfg); err != nil {
return err
}
return format.WriteJSON(iostreams.IO.Out, useResult{
CurrentContext: name,
PreviousContext: prev,
})
}
func notFoundError(name string, cfg *config.Config) error {
if len(cfg.Contexts) == 0 {
return &cmdutil.Error{
Code: cmdutil.CodeLocalContextNotFound,
Message: fmt.Sprintf("context not found: %s", name),
Hint: "no contexts registered — run `weknora auth login` first",
}
}
keys := contextKeys(cfg.Contexts)
candidate := closestMatch(name, keys)
var hint string
if candidate != "" && candidate != name {
hint = fmt.Sprintf("did you mean: %q?", candidate)
} else {
hint = fmt.Sprintf("available contexts: %v", keys)
}
return &cmdutil.Error{
Code: cmdutil.CodeLocalContextNotFound,
Message: fmt.Sprintf("context not found: %s", name),
Hint: hint,
}
}
func contextKeys(m map[string]config.Context) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
return out
}
// closestMatch returns the candidate with min levenshtein distance ≤ 2,
// or "" if none qualifies. Ties broken by lexicographic order so the hint
// is deterministic across map-iteration orderings (Go randomizes range over
// map; without this, did-you-mean output is flaky for equally-close
// candidates).
func closestMatch(target string, candidates []string) string {
sorted := append([]string(nil), candidates...)
sort.Strings(sorted)
best := ""
bestD := 3
for _, c := range sorted {
d := levenshtein(target, c)
if d < bestD {
bestD = d
best = c
}
}
if bestD > 2 {
return ""
}
return best
}
func levenshtein(a, b string) int {
la, lb := len(a), len(b)
if la == 0 {
return lb
}
if lb == 0 {
return la
}
prev := make([]int, lb+1)
curr := make([]int, lb+1)
for j := 0; j <= lb; j++ {
prev[j] = j
}
for i := 1; i <= la; i++ {
curr[0] = i
for j := 1; j <= lb; j++ {
cost := 1
if a[i-1] == b[j-1] {
cost = 0
}
curr[j] = min(curr[j-1]+1, prev[j]+1, prev[j-1]+cost)
}
prev, curr = curr, prev
}
return prev[lb]
}