cc8254f862
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.
146 lines
5.3 KiB
Go
146 lines
5.3 KiB
Go
package contextcmd
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"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"
|
|
"github.com/Tencent/WeKnora/cli/internal/prompt"
|
|
"github.com/Tencent/WeKnora/cli/internal/secrets"
|
|
)
|
|
|
|
type RemoveOptions struct {
|
|
Yes bool // sourced from the global -y/--yes persistent flag (matches `kb delete`)
|
|
}
|
|
|
|
// contextRemoveFields enumerates the fields surfaced for `--json` discovery on
|
|
// `context remove`. The result reports the disposition of the removed entry.
|
|
var contextRemoveFields = []string{
|
|
"name", "removed", "was_current",
|
|
}
|
|
|
|
// removeResult is the typed payload emitted under data on success.
|
|
type removeResult struct {
|
|
Name string `json:"name"`
|
|
Removed bool `json:"removed"`
|
|
WasCurrent bool `json:"was_current"`
|
|
}
|
|
|
|
// NewCmdRemove builds `weknora context remove`. Drops the entry from
|
|
// config.yaml and best-effort clears keyring references. Removing a
|
|
// non-current context is low-friction (no prompt). Removing the *current*
|
|
// context triggers the destructive-write confirmation protocol (exit 10),
|
|
// because subsequent commands will have no default connection target.
|
|
func NewCmdRemove(f *cmdutil.Factory) *cobra.Command {
|
|
opts := &RemoveOptions{}
|
|
cmd := &cobra.Command{
|
|
Use: "remove <name>",
|
|
Short: "Remove a context (drops entry, clears keyring refs)",
|
|
Long: `Deletes the named context from config.yaml and best-effort clears any
|
|
keyring references it owned (matches ` + "`weknora auth logout`" + `).
|
|
|
|
Removing the current context also clears CurrentContext — subsequent commands
|
|
will error until you select another with ` + "`weknora context use <name>`" + ` or pick
|
|
one up via the global ` + "`--context`" + ` flag. Because that change is observable in
|
|
every later command, removing the current context requires explicit -y/--yes
|
|
in scripted / --json invocations (exit code 10; see cli/AGENTS.md).`,
|
|
Example: ` weknora context remove staging # remove non-current → no prompt
|
|
weknora context remove production -y # remove current → confirm`,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(c *cobra.Command, args []string) error {
|
|
jopts, err := cmdutil.CheckJSONFlags(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
opts.Yes, _ = c.Flags().GetBool("yes")
|
|
store, err := f.Secrets()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return runRemove(opts, jopts, args[0], store, f.Prompter())
|
|
},
|
|
}
|
|
cmdutil.AddJSONFlags(cmd, contextRemoveFields)
|
|
aiclient.SetAgentHelp(cmd, "Removes a context entry. Removing the current context requires -y in non-TTY/--json (exit-10). Always best-effort clears keyring refs.")
|
|
return cmd
|
|
}
|
|
|
|
func runRemove(opts *RemoveOptions, jopts *cmdutil.JSONOptions, name string, store secrets.Store, p prompt.Prompter) error {
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ctx, exists := cfg.Contexts[name]
|
|
if !exists {
|
|
return notFoundError(name, cfg)
|
|
}
|
|
wasCurrent := name == cfg.CurrentContext
|
|
risk := riskForRemove(name, wasCurrent)
|
|
|
|
jsonOut := jopts.Enabled()
|
|
// Confirmation only fires for removing the current context — non-current
|
|
// remove uses the same low-friction policy as `auth logout`.
|
|
if wasCurrent {
|
|
if err := cmdutil.ConfirmDestructive(p, opts.Yes, jsonOut, "current context", name); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Config first, secrets after: a crash in between leaves an orphan
|
|
// keyring entry but no dangling config ref (same ordering as auth logout).
|
|
delete(cfg.Contexts, name)
|
|
if wasCurrent {
|
|
cfg.CurrentContext = ""
|
|
}
|
|
if err := config.Save(cfg); err != nil {
|
|
return cmdutil.Wrapf(cmdutil.CodeLocalFileIO, err, "save config")
|
|
}
|
|
clearContextSecrets(store, ctx, name)
|
|
|
|
_ = risk // risk classification dropped in v0.4; exit code already signals
|
|
result := removeResult{Name: name, Removed: true, WasCurrent: wasCurrent}
|
|
if jsonOut {
|
|
return format.WriteJSONFiltered(iostreams.IO.Out, result, jopts.Fields, jopts.JQ)
|
|
}
|
|
if wasCurrent {
|
|
fmt.Fprintf(iostreams.IO.Out, "✓ Removed context %s (current context cleared — run `weknora context use <name>` to pick another)\n", name)
|
|
} else {
|
|
fmt.Fprintf(iostreams.IO.Out, "✓ Removed context %s\n", name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// riskForRemove returns the operation risk: high-risk-write only when the
|
|
// target is the currently-active context (subsequent commands will have no
|
|
// default --context until the user picks one).
|
|
func riskForRemove(name string, wasCurrent bool) *format.Risk {
|
|
if wasCurrent {
|
|
return &format.Risk{
|
|
Level: format.RiskHighRiskWrite,
|
|
Action: fmt.Sprintf("remove context %s (the current context — subsequent commands will need a new --context)", name),
|
|
}
|
|
}
|
|
return &format.Risk{Level: format.RiskWrite, Action: fmt.Sprintf("remove context %s", name)}
|
|
}
|
|
|
|
// clearContextSecrets mirrors auth/logout.go: best-effort delete every secret
|
|
// slot the context references. Errors are swallowed so a missing keyring
|
|
// entry doesn't block remove (logout has had the same policy since v0.2).
|
|
func clearContextSecrets(store secrets.Store, c config.Context, name string) {
|
|
if c.TokenRef != "" {
|
|
_ = store.Delete(name, "access")
|
|
}
|
|
if c.RefreshRef != "" {
|
|
_ = store.Delete(name, "refresh")
|
|
}
|
|
if c.APIKeyRef != "" {
|
|
_ = store.Delete(name, "api_key")
|
|
}
|
|
}
|
|
|