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

141 lines
5.0 KiB
Go

package auth
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/format"
"github.com/Tencent/WeKnora/cli/internal/iostreams"
)
// authTokenFields lists fields available for `auth token --json=` projection.
// Single-resource shape: filter applies to the bare token object directly.
var authTokenFields = []string{"token", "mode", "context"}
type tokenResult struct {
Token string `json:"token"`
Mode string `json:"mode"` // ModeBearer (JWT) or ModeAPIKey
Context string `json:"context"`
}
// NewCmdToken builds `weknora auth token`. Prints the active context's
// credential to stdout for use in shell pipelines, e.g.
//
// WEKNORA_TOKEN=$(weknora auth token)
// curl -H "Authorization: Bearer $WEKNORA_TOKEN" ... # JWT mode
// curl -H "X-API-Key: $WEKNORA_TOKEN" ... # api-key mode
//
// The user is responsible for constructing the appropriate header —
// `auth list` shows which mode each context uses.
//
// Default output: raw token on stdout, no trailing newline (clean $(...)).
// `--json[=fields]` wraps in envelope {token, mode, context}.
func NewCmdToken(f *cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "token",
Short: "Print the active context's credential to stdout",
Long: `Print the active context's credential to stdout, with no trailing
newline, suitable for shell command substitution.
The credential is the long-lived API key (mode: api-key) or the access JWT
(mode: bearer), depending on how the context was created. Run ` + "`weknora auth list`" + `
to see which mode each context uses, and construct the matching HTTP header:
Authorization: Bearer <token> # bearer mode
X-API-Key: <token> # api-key mode
` + "`--context <name>`" + ` (global flag) selects a non-active context to read from.`,
Example: ` WEKNORA_TOKEN=$(weknora auth token)
weknora auth token --context staging
weknora auth token --json`,
Args: cobra.NoArgs,
RunE: func(c *cobra.Command, _ []string) error {
jopts, err := cmdutil.CheckJSONFlags(c)
if err != nil {
return err
}
return runToken(f, jopts)
},
}
cmdutil.AddJSONFlags(cmd, authTokenFields)
aiclient.SetAgentHelp(cmd, "Prints the active context's credential to stdout for scripting. Default: raw secret, no trailing newline. With --json: envelope {token, mode, context}. Errors: auth.unauthenticated when no active context or no stored credential (run `auth login`); local.keychain_denied when the keyring rejects the read.")
return cmd
}
func runToken(f *cmdutil.Factory, jopts *cmdutil.JSONOptions) error {
cfg, err := f.Config()
if err != nil {
return err
}
ctxName := cfg.CurrentContext
if f.ContextOverride != "" {
ctxName = f.ContextOverride
}
if ctxName == "" {
return cmdutil.NewError(cmdutil.CodeAuthUnauthenticated,
"no current context configured")
}
ctx, ok := cfg.Contexts[ctxName]
if !ok {
return cmdutil.NewError(cmdutil.CodeLocalContextNotFound,
fmt.Sprintf("context %q not found", ctxName))
}
store, err := f.Secrets()
if err != nil {
return cmdutil.Wrapf(cmdutil.CodeLocalKeychainDenied, err, "init secrets store")
}
// Resolve the stored credential. Prefer bearer (JWT) when both refs
// are present — JWT is the more capable mode and what `buildClient`
// uses for the Authorization header (see factory.go buildClient).
var token, mode string
switch {
case ctx.TokenRef != "":
v, ferr := cmdutil.LoadSecret(store, ctxName, "access")
if ferr != nil {
return ferr
}
token, mode = v, ModeBearer
case ctx.APIKeyRef != "":
v, ferr := cmdutil.LoadSecret(store, ctxName, "api_key")
if ferr != nil {
return ferr
}
token, mode = v, ModeAPIKey
default:
return cmdutil.NewError(cmdutil.CodeAuthUnauthenticated,
fmt.Sprintf("context %q has no stored credential; run `weknora auth login`", ctxName))
}
if token == "" {
return cmdutil.NewError(cmdutil.CodeAuthUnauthenticated,
fmt.Sprintf("context %q credential is empty in keyring; run `weknora auth login`", ctxName))
}
if jopts.Enabled() {
return format.WriteJSONFiltered(iostreams.IO.Out,
tokenResult{Token: token, Mode: mode, Context: ctxName},
jopts.Fields, jopts.JQ)
}
// No trailing newline — clean $(weknora auth token) substitution.
fmt.Fprint(iostreams.IO.Out, token)
// Defensive hint to stderr when stdout is an interactive terminal:
// the user likely didn't mean to display the secret on screen.
// stderr-only so scripts (always non-TTY) are unaffected. Mode-specific
// because api-key tokens are long-lived and rotation is the only
// recourse on leak — bearer tokens self-expire via refresh.
if iostreams.IO.IsStdoutTTY() {
fmt.Fprintln(iostreams.IO.Err)
fmt.Fprintln(iostreams.IO.Err, "hint: pipe to $(weknora auth token) to capture; this terminal scrollback now contains the secret")
if mode == ModeAPIKey {
fmt.Fprintln(iostreams.IO.Err, "note: api-key credentials are long-lived — rotate via your auth provider if exposed (no `auth refresh` path)")
}
}
return nil
}