19afd5eed9
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.
123 lines
3.8 KiB
Go
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
|
|
}
|
|
|