WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
// Package skillscmd holds the `weknora skills` command tree: list the bundled
|
||||
// Agent Skills and install (write) them to an agent's skills directory. The
|
||||
// skill markdown is embedded in the binary (see cli/skills), so install works
|
||||
// without a source checkout — replacing the manual symlink-from-checkout MVP.
|
||||
//
|
||||
// Package name `skillscmd` avoids colliding with the embed package `skills`.
|
||||
package skillscmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/Tencent/WeKnora/cli/internal/cmdutil"
|
||||
"github.com/Tencent/WeKnora/cli/internal/iostreams"
|
||||
"github.com/Tencent/WeKnora/cli/internal/output"
|
||||
skillsfs "github.com/Tencent/WeKnora/cli/skills"
|
||||
)
|
||||
|
||||
// NewCmd builds the `weknora skills` parent command.
|
||||
func NewCmd(f *cmdutil.Factory) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "skills",
|
||||
Short: "List and install the bundled Agent Skills",
|
||||
}
|
||||
cmd.AddCommand(newCmdList())
|
||||
cmd.AddCommand(newCmdInstall())
|
||||
return cmd
|
||||
}
|
||||
|
||||
var skillsListFields = []string{"name", "description", "files"}
|
||||
|
||||
func newCmdList() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List the Agent Skills bundled in this binary",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(c *cobra.Command, _ []string) error {
|
||||
fopts, err := cmdutil.CheckFormatFlag(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fopts.ResolveDefault(iostreams.IO.IsStdoutTTY())
|
||||
skills, err := skillsfs.List()
|
||||
if err != nil {
|
||||
return cmdutil.Wrapf(cmdutil.CodeInternalError, err, "read embedded skills")
|
||||
}
|
||||
if fopts.WantsJSON() {
|
||||
return fopts.Emit(iostreams.IO.Out, skills, &output.Meta{Count: output.IntPtr(len(skills))})
|
||||
}
|
||||
tw := tabwriter.NewWriter(iostreams.IO.Out, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(tw, "NAME\tFILES\tDESCRIPTION")
|
||||
for _, s := range skills {
|
||||
fmt.Fprintf(tw, "%s\t%d\t%s\n", s.Name, len(s.Files), s.Description)
|
||||
}
|
||||
return tw.Flush()
|
||||
},
|
||||
}
|
||||
cmdutil.AddFormatFlag(cmd, skillsListFields...)
|
||||
cmdutil.SetAgentHelp(cmd, cmdutil.AgentHelp{
|
||||
UsedFor: "list the Agent Skills bundled in this CLI binary (name, description, file count) — discover what `weknora skills install` would write",
|
||||
Examples: []string{"weknora skills list --format json"},
|
||||
Output: "envelope.data is an array of {name, description, files[]}",
|
||||
})
|
||||
return cmd
|
||||
}
|
||||
|
||||
type installResult struct {
|
||||
Dir string `json:"dir"`
|
||||
Installed []string `json:"installed"`
|
||||
Skills []string `json:"skills"`
|
||||
}
|
||||
|
||||
func newCmdInstall() *cobra.Command {
|
||||
var dir string
|
||||
var force bool
|
||||
var dryRun bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "install",
|
||||
Short: "Write the bundled Agent Skills to an agent's skills directory",
|
||||
Long: `Write the embedded Agent Skills to disk so an agent (e.g. Claude Code) can
|
||||
load them. Defaults to ~/.claude/skills; override with --dir for other agents.
|
||||
Existing files are left untouched unless --force is passed.`,
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(c *cobra.Command, _ []string) error {
|
||||
fopts, err := cmdutil.CheckFormatFlag(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fopts.ResolveDefault(iostreams.IO.IsStdoutTTY())
|
||||
|
||||
target, err := resolveDir(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
skills, err := skillsfs.List()
|
||||
if err != nil {
|
||||
return cmdutil.Wrapf(cmdutil.CodeInternalError, err, "read embedded skills")
|
||||
}
|
||||
|
||||
var planned []string
|
||||
names := make([]string, 0, len(skills))
|
||||
for _, s := range skills {
|
||||
names = append(names, s.Name)
|
||||
for _, rel := range s.Files {
|
||||
planned = append(planned, filepath.Join(target, rel))
|
||||
}
|
||||
}
|
||||
|
||||
if handled, err := cmdutil.HandleDryRun(c, dryRun, cmdutil.DryRunPlan{
|
||||
Action: "skills.install",
|
||||
Args: map[string]any{
|
||||
"dir": target,
|
||||
"skills": names,
|
||||
"files": planned,
|
||||
},
|
||||
}); handled {
|
||||
return err
|
||||
}
|
||||
|
||||
installed, err := writeSkills(skills, target, force)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res := installResult{Dir: target, Installed: installed, Skills: names}
|
||||
if fopts.WantsJSON() {
|
||||
return fopts.Emit(iostreams.IO.Out, res, nil)
|
||||
}
|
||||
fmt.Fprintf(iostreams.IO.Out, "✓ Installed %d skill(s) to %s\n", len(names), target)
|
||||
for _, p := range installed {
|
||||
fmt.Fprintf(iostreams.IO.Out, " %s\n", p)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&dir, "dir", "", "Target skills directory (default: ~/.claude/skills)")
|
||||
cmd.Flags().BoolVar(&force, "force", false, "Overwrite existing skill files")
|
||||
cmdutil.AddFormatFlag(cmd, "dir", "installed", "skills")
|
||||
cmdutil.AddDryRunFlag(cmd, &dryRun)
|
||||
cmdutil.SetAgentHelp(cmd, cmdutil.AgentHelp{
|
||||
UsedFor: "write the bundled Agent Skills to an agent's skills directory (default ~/.claude/skills) so the agent can load them; --dry-run previews the file list",
|
||||
Examples: []string{"weknora skills install --dry-run --format json", "weknora skills install --dir ~/.claude/skills --force"},
|
||||
Output: "envelope.data is {dir, installed:[paths], skills:[names]}",
|
||||
})
|
||||
return cmd
|
||||
}
|
||||
|
||||
// resolveDir returns the explicit --dir or the default ~/.claude/skills. A
|
||||
// leading ~ in --dir is expanded to the home directory — otherwise a quoted
|
||||
// `--dir '~/foo'` (which the shell leaves literal) would create a bogus "~"
|
||||
// directory tree.
|
||||
func resolveDir(dir string) (string, error) {
|
||||
if dir != "" {
|
||||
return expandTilde(dir)
|
||||
}
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", cmdutil.NewError(cmdutil.CodeInputInvalidArgument,
|
||||
"could not determine home directory; pass --dir explicitly")
|
||||
}
|
||||
return filepath.Join(home, ".claude", "skills"), nil
|
||||
}
|
||||
|
||||
// expandTilde resolves a leading "~" or "~/" path segment to the user's home
|
||||
// directory. Other forms (including "~user" and a ~ that isn't the first
|
||||
// segment) are returned unchanged — matching the common shell behavior a CLI
|
||||
// is expected to reproduce when it receives an unexpanded literal tilde.
|
||||
func expandTilde(path string) (string, error) {
|
||||
if path != "~" && !strings.HasPrefix(path, "~/") {
|
||||
return path, nil
|
||||
}
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", cmdutil.NewError(cmdutil.CodeInputInvalidArgument,
|
||||
"could not expand ~ (home directory unknown); pass an absolute --dir")
|
||||
}
|
||||
if path == "~" {
|
||||
return home, nil
|
||||
}
|
||||
return filepath.Join(home, path[len("~/"):]), nil
|
||||
}
|
||||
|
||||
// writeSkills writes every embedded skill file under target, creating parent
|
||||
// dirs. Without --force, an existing file is skipped (not overwritten). Returns
|
||||
// the paths actually written.
|
||||
func writeSkills(skills []skillsfs.Skill, target string, force bool) ([]string, error) {
|
||||
var written []string
|
||||
for _, s := range skills {
|
||||
for _, rel := range s.Files {
|
||||
dst := filepath.Join(target, rel)
|
||||
if !force {
|
||||
if _, err := os.Stat(dst); err == nil {
|
||||
continue // exists; leave it
|
||||
}
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
||||
return written, cmdutil.Wrapf(cmdutil.CodeLocalFileIO, err, "create dir for %s", dst)
|
||||
}
|
||||
b, err := skillsfs.ReadFile(rel)
|
||||
if err != nil {
|
||||
return written, cmdutil.Wrapf(cmdutil.CodeInternalError, err, "read embedded %s", rel)
|
||||
}
|
||||
if err := os.WriteFile(dst, b, 0o644); err != nil {
|
||||
return written, cmdutil.Wrapf(cmdutil.CodeLocalFileIO, err, "write %s", dst)
|
||||
}
|
||||
written = append(written, dst)
|
||||
}
|
||||
}
|
||||
return written, nil
|
||||
}
|
||||
Reference in New Issue
Block a user