Files
Enterprise-Knowledge-Base/cli/cmd/link/link.go
T
nullkey 19afd5eed9 feat(cli): add project-link foundation with init and link commands
v0.2 grounds the CLI's resource commands (kb / doc / chat / query) in a
per-project link file (.weknora/project.yaml) so users don't have to pass
--kb-id on every invocation. Mirrors npm/cargo/git: walk up the cwd tree
to find the project root, override via flag or env when needed.

This commit ships the foundation layer:

  - cli/internal/projectlink/  Discover (walk-up, depth=64) / Load / Save
  - cmdutil.Factory.ResolveKB  5-level fallback chain:
      --kb-id flag → --kb name (ListKnowledgeBases lookup) →
      WEKNORA_KB_ID env → walk-up project link → CodeKBIDRequired
  - cmdutil.ResolveKBNameToID   shared name→id helper used by init / link
                                / Factory.ResolveKB (was duplicated 3 ways
                                in early implementation; reuse review #2)
  - cli/cmd/init/               interactive (huh prompt) or flag-driven
                                first-time setup; refuses to overwrite
                                without --force
  - cli/cmd/link/               non-interactive update; --kb-id and --kb
                                are mutually exclusive and one is required

Also registers the v0.2 ErrorCode set (all codes for the eight new
commands) and AST-scan identToErrorCode mapping in one place — keeps the
acceptance/contract suite green across the v0.2 commit chain even before
later commits reference each code.
2026-05-12 13:20:42 +08:00

123 lines
3.8 KiB
Go

// Package linkcmd implements `weknora link` — re-points an existing
// .weknora/project.yaml at a different knowledge base. Distinct from
// `weknora init`: link assumes the directory is already a project (or you
// want one without going through the init wizard) and never prompts.
package linkcmd
import (
"context"
"fmt"
"os"
"path/filepath"
"time"
"github.com/spf13/cobra"
"github.com/Tencent/WeKnora/cli/internal/agent"
"github.com/Tencent/WeKnora/cli/internal/cmdutil"
"github.com/Tencent/WeKnora/cli/internal/format"
"github.com/Tencent/WeKnora/cli/internal/iostreams"
"github.com/Tencent/WeKnora/cli/internal/projectlink"
)
// Options captures `weknora link` flags.
type Options struct {
KBID string // --kb-id
KBName string // --kb
JSONOut bool // --json
}
// linkResult is the typed payload emitted under data. Keep schema-aligned
// with init.initResult so agents see the same shape regardless of entry point.
type linkResult struct {
Context string `json:"context"`
KBID string `json:"kb_id"`
KBName string `json:"kb_name,omitempty"`
ProjectLinkPath string `json:"project_link_path"`
}
// NewCmd builds the `weknora link` command.
func NewCmd(f *cmdutil.Factory) *cobra.Command {
opts := &Options{}
cmd := &cobra.Command{
Use: "link",
Short: "Update or create the project link in the current directory",
Long: `Writes (overwriting if present) .weknora/project.yaml in the current
working directory pointing at the supplied knowledge base. Unlike init, link
never prompts and never fails on an existing file — it is the non-interactive
re-link path used by scripts that switch between knowledge bases.`,
Example: ` weknora link --kb-id kb_abc
weknora link --kb staging`,
Args: cobra.NoArgs,
RunE: func(c *cobra.Command, _ []string) error {
return runLink(c.Context(), opts, f)
},
}
cmd.Flags().StringVar(&opts.KBID, "kb-id", "", "Knowledge base id to link")
cmd.Flags().StringVar(&opts.KBName, "kb", "", "Knowledge base name (resolved to id)")
cmd.Flags().BoolVar(&opts.JSONOut, "json", false, "Output JSON envelope")
cmd.MarkFlagsMutuallyExclusive("kb-id", "kb")
cmd.MarkFlagsOneRequired("kb-id", "kb")
agent.SetAgentHelp(cmd, "Re-points .weknora/project.yaml to a KB. Pass --kb-id (or --kb name); never prompts, always overwrites.")
return cmd
}
func runLink(ctx context.Context, opts *Options, f *cmdutil.Factory) error {
cwd, err := os.Getwd()
if err != nil {
return cmdutil.Wrapf(cmdutil.CodeLocalFileIO, err, "get cwd")
}
linkPath := filepath.Join(cwd, projectlink.DirName, projectlink.FileName)
cfg, err := f.Config()
if err != nil {
return err
}
ctxName := cfg.CurrentContext
if ctxName == "" {
return cmdutil.NewError(cmdutil.CodeAuthUnauthenticated, "no active context; run `weknora auth login` first")
}
kbID, kbName := opts.KBID, ""
if opts.KBName != "" {
cli, err := f.Client()
if err != nil {
return err
}
id, err := cmdutil.ResolveKBNameToID(ctx, cli, opts.KBName)
if err != nil {
return err
}
kbID, kbName = id, opts.KBName
}
link := &projectlink.Project{
Context: ctxName,
KBID: kbID,
CreatedAt: time.Now().UTC(),
}
if err := projectlink.Save(linkPath, link); err != nil {
return cmdutil.Wrapf(cmdutil.CodeLocalFileIO, err, "write project link")
}
r := linkResult{
Context: ctxName,
KBID: kbID,
KBName: kbName,
ProjectLinkPath: linkPath,
}
if opts.JSONOut {
return format.WriteEnvelope(iostreams.IO.Out, format.Success(r, &format.Meta{
Context: ctxName,
KBID: kbID,
}))
}
if kbName != "" {
fmt.Fprintf(iostreams.IO.Out, "✓ Linked %s to %s (kb=%s, id=%s)\n", linkPath, ctxName, kbName, kbID)
} else {
fmt.Fprintf(iostreams.IO.Out, "✓ Linked %s to %s (kb_id=%s)\n", linkPath, ctxName, kbID)
}
return nil
}