Files
wehub-resource-sync f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

281 lines
7.6 KiB
Go

// ABOUTME: `skills` command group: install and list the AgentsView skill
// ABOUTME: files that teach coding-agent harnesses to search session history.
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"time"
"github.com/spf13/cobra"
gitrepo "go.kenn.io/kit/git/repo"
"go.kenn.io/agentsview/internal/skills"
)
// skillFileName is the file every harness's skill directory installs, as
// documented on skills.TargetDir.
const skillFileName = "SKILL.md"
func newSkillsCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "skills",
Short: "Install and list AgentsView skills for coding-agent harnesses",
GroupID: groupMeta,
SilenceUsage: true,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
return cmd.Help()
},
}
cmd.AddCommand(newSkillsInstallCommand())
cmd.AddCommand(newSkillsListCommand())
return cmd
}
func newSkillsInstallCommand() *cobra.Command {
var (
harnessNames []string
project bool
force bool
)
cmd := &cobra.Command{
Use: "install",
Short: "Install AgentsView skill files for coding-agent harnesses",
Args: cobra.NoArgs,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, _ []string) error {
harnesses, err := resolveSkillHarnesses(harnessNames)
if err != nil {
return err
}
base, err := skillsBaseDir(cmd.Context(), project)
if err != nil {
return err
}
return runSkillsInstall(cmd.OutOrStdout(), harnesses, base, force)
},
}
flags := cmd.Flags()
flags.StringArrayVar(&harnessNames, "harness", nil,
"Harness to install for (claude or agents); repeatable, default both")
flags.BoolVar(&project, "project", false,
"Install into the project (git root of the current directory) "+
"instead of the user home directory")
flags.BoolVar(&force, "force", false,
"Overwrite files that were modified or were not generated by agentsview")
return cmd
}
// runSkillsInstall renders and writes each harness's skill file under base,
// printing one line per target. It processes every target before returning
// so a refusal on one harness never blocks another, then reports a non-nil
// error if any target was refused.
func runSkillsInstall(out io.Writer, harnesses []skills.Harness, base string, force bool) error {
var refused []string
for _, h := range harnesses {
rendered, err := skills.Render(h, version)
if err != nil {
return err
}
dir := skills.TargetDir(h, base)
path := filepath.Join(dir, skillFileName)
existing, err := readSkillFile(path)
if err != nil {
return err
}
state := skills.Classify(existing, rendered)
if !force && (state == skills.StateModified || state == skills.StateForeign) {
fmt.Fprintf(out, "%s was modified (or not generated); use --force to overwrite\n", path)
refused = append(refused, path)
continue
}
if err := os.MkdirAll(dir, 0o755); err != nil {
return fmt.Errorf("skills: create %s: %w", dir, err)
}
if err := os.WriteFile(path, []byte(rendered.Content), 0o644); err != nil {
return fmt.Errorf("skills: write %s: %w", path, err)
}
switch state {
case skills.StateMissing:
fmt.Fprintf(out, "installed %s\n", path)
case skills.StateCurrent:
fmt.Fprintf(out, "up to date %s\n", path)
default: // StateStale, or StateModified/StateForeign forced
fmt.Fprintf(out, "updated %s\n", path)
}
}
if len(refused) > 0 {
return fmt.Errorf(
"skills install: %d target(s) refused; rerun with --force to overwrite",
len(refused),
)
}
return nil
}
func newSkillsListCommand() *cobra.Command {
var project bool
cmd := &cobra.Command{
Use: "list",
Short: "List AgentsView skill files and their install state",
Args: cobra.NoArgs,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, _ []string) error {
base, err := skillsBaseDir(cmd.Context(), project)
if err != nil {
return err
}
rows, err := listSkillRows(base, project)
if err != nil {
return err
}
if outputFormat(cmd) == "json" {
return json.NewEncoder(cmd.OutOrStdout()).Encode(rows)
}
return printSkillListHuman(cmd.OutOrStdout(), rows)
},
}
flags := cmd.Flags()
flags.BoolVar(&project, "project", false,
"List project-level (git root of the current directory) installs "+
"instead of the user home directory")
registerFormatFlags(flags)
return cmd
}
// skillListRow is one row of `skills list` output, in both human and JSON form.
type skillListRow struct {
Harness string `json:"harness"`
Level string `json:"level"`
State string `json:"state"`
Path string `json:"path"`
}
// listSkillRows classifies every harness's skill file under base against a
// fresh render.
func listSkillRows(base string, project bool) ([]skillListRow, error) {
level := "user"
if project {
level = "project"
}
harnesses := skills.AllHarnesses()
rows := make([]skillListRow, 0, len(harnesses))
for _, h := range harnesses {
rendered, err := skills.Render(h, version)
if err != nil {
return nil, err
}
dir := skills.TargetDir(h, base)
path := filepath.Join(dir, skillFileName)
existing, err := readSkillFile(path)
if err != nil {
return nil, err
}
state := skills.Classify(existing, rendered)
rows = append(rows, skillListRow{
Harness: string(h),
Level: level,
State: skillStateString(state),
Path: path,
})
}
return rows, nil
}
func printSkillListHuman(w io.Writer, rows []skillListRow) error {
fmt.Fprintf(w, "%-8s %-8s %-8s %s\n", "HARNESS", "LEVEL", "STATE", "PATH")
for _, r := range rows {
fmt.Fprintf(w, "%-8s %-8s %-8s %s\n", r.Harness, r.Level, r.State, r.Path)
}
return nil
}
func skillStateString(s skills.InstalledState) string {
switch s {
case skills.StateMissing:
return "missing"
case skills.StateCurrent:
return "current"
case skills.StateStale:
return "stale"
case skills.StateModified:
return "modified"
case skills.StateForeign:
return "foreign"
default:
return "unknown"
}
}
// readSkillFile reads path, returning nil (not an empty slice) when the file
// does not exist so the result matches skills.Classify's "missing" contract.
func readSkillFile(path string) ([]byte, error) {
content, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("skills: read %s: %w", path, err)
}
return content, nil
}
// resolveSkillHarnesses maps --harness flag values to skills.Harness,
// defaulting to every harness when none were given.
func resolveSkillHarnesses(names []string) ([]skills.Harness, error) {
if len(names) == 0 {
return skills.AllHarnesses(), nil
}
out := make([]skills.Harness, 0, len(names))
for _, name := range names {
switch name {
case string(skills.HarnessClaude):
out = append(out, skills.HarnessClaude)
case string(skills.HarnessAgents):
out = append(out, skills.HarnessAgents)
default:
return nil, fmt.Errorf("skills: unknown --harness %q (want claude or agents)", name)
}
}
return out, nil
}
// skillsBaseDir resolves the install base: the user home directory by
// default, or the git root of the current directory (falling back to the
// current directory itself outside a repo) when project is true.
func skillsBaseDir(ctx context.Context, project bool) (string, error) {
if !project {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("skills: resolve home directory: %w", err)
}
return home, nil
}
cwd, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("skills: resolve working directory: %w", err)
}
opCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
root, err := gitrepo.Root(opCtx, cwd)
if err != nil || root == "" {
return cwd, nil
}
return root, nil
}