Files
Enterprise-Knowledge-Base/cli/cmd/auth/refresh.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

121 lines
4.3 KiB
Go

package auth
import (
"context"
"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/format"
"github.com/Tencent/WeKnora/cli/internal/iostreams"
sdk "github.com/Tencent/WeKnora/client"
)
type RefreshOptions struct {
Name string // --name: target context (defaults to current)
}
// authRefreshFields enumerates the fields surfaced for `--json` discovery
// on `auth refresh`. Token values are intentionally omitted — see refreshResult.
var authRefreshFields = []string{"context"}
// refreshResult is the typed payload emitted under data on success. Token
// values are intentionally NOT included — emitting them would leak secrets
// into stdout / agent transcripts. Agents needing to verify the new token
// can re-run `weknora auth status` (live API check).
type refreshResult struct {
Context string `json:"context"`
}
// NewCmdRefresh builds `weknora auth refresh`. Renews the JWT access
// token by spending the stored refresh_token via POST /auth/refresh —
// the standard OAuth refresh-token grant.
//
// API-key contexts are rejected — they have no refresh semantic;
// rotate the key via the server UI instead.
func NewCmdRefresh(f *cmdutil.Factory) *cobra.Command {
opts := &RefreshOptions{}
cmd := &cobra.Command{
Use: "refresh",
Short: "Renew the JWT access token using the stored refresh token",
Long: `Reads the refresh token previously stored by ` + "`weknora auth login`" + ` and
exchanges it for a new access + refresh token pair via POST /api/v1/auth/refresh.
Both new tokens replace the existing entries in the OS keyring.
API-key contexts are rejected with input.invalid_argument — they have no
refresh semantic. Rotate the key in the server UI instead.`,
Example: ` weknora auth refresh # refresh the current context
weknora auth refresh --name staging # refresh a specific context`,
Args: cobra.NoArgs,
RunE: func(c *cobra.Command, _ []string) error {
jopts, err := cmdutil.CheckJSONFlags(c)
if err != nil {
return err
}
return runRefresh(c.Context(), opts, jopts, f, defaultRefresher)
},
}
cmd.Flags().StringVar(&opts.Name, "name", "", "Context to refresh (defaults to the current context)")
cmdutil.AddJSONFlags(cmd, authRefreshFields)
aiclient.SetAgentHelp(cmd, "Renews the access token using the stored refresh token. Errors with auth.token_expired when refresh itself is rejected — surface the hint to re-run auth login.")
return cmd
}
// defaultRefresher constructs a fresh, unauthenticated SDK client targeting
// host — the /auth/refresh endpoint reads the refresh token from the body,
// so no bearer / api-key header is needed.
func defaultRefresher(host string) cmdutil.Refresher {
return sdk.NewClient(host)
}
func runRefresh(ctx context.Context, opts *RefreshOptions, jopts *cmdutil.JSONOptions, f *cmdutil.Factory, refresherFor func(host string) cmdutil.Refresher) error {
cfg, err := f.Config()
if err != nil {
return err
}
name := opts.Name
if name == "" {
name = cfg.CurrentContext
}
if name == "" {
return cmdutil.NewError(cmdutil.CodeAuthUnauthenticated,
"no current context configured; run `weknora auth login` to set one up")
}
c, ok := cfg.Contexts[name]
if !ok {
return cmdutil.NewError(cmdutil.CodeLocalContextNotFound,
fmt.Sprintf("context not found: %s", name))
}
if c.Host == "" {
return cmdutil.NewError(cmdutil.CodeLocalConfigCorrupt,
fmt.Sprintf("context %q has no host", name))
}
if c.RefreshRef == "" {
hint := "api-key contexts can't be refreshed — rotate the key in the server UI and run `weknora auth login --with-token`"
if c.APIKeyRef == "" {
hint = "no refresh token stored — run `weknora auth login` to authenticate"
}
return &cmdutil.Error{
Code: cmdutil.CodeInputInvalidArgument,
Message: fmt.Sprintf("context %q has no refresh token", name),
Hint: hint,
}
}
store, err := f.Secrets()
if err != nil {
return err
}
if _, err := cmdutil.RefreshAndPersist(ctx, store, refresherFor(c.Host), name); err != nil {
return err
}
if jopts.Enabled() {
return format.WriteJSONFiltered(iostreams.IO.Out, refreshResult{Context: name}, jopts.Fields, jopts.JQ)
}
fmt.Fprintf(iostreams.IO.Out, "✓ Refreshed access token for context %s\n", name)
return nil
}