feat: 跨平台命令助手 kopsc 初始实现

- 意图命令按 OS/架构、Linux 发行版与容器运行时解析并执行对应底层命令
- 内置 docker.clearsafe、net.ports、pkg.refreshindex
- --dry-run 预览、list/explain/platform/version 辅助命令
- 纯 Go 跨平台构建脚本 scripts/build
This commit is contained in:
2026-10-31 13:01:39 +08:00
commit 7d9852958f
18 changed files with 2650 additions and 0 deletions
+109
View File
@@ -0,0 +1,109 @@
package registry
// Builtin returns the registry compiled into kopsc. Every variant is explicit
// about the platform it targets; resolution picks the most specific match.
func Builtin() *Registry {
return &Registry{Commands: []Command{
{
ID: "docker.clearsafe",
Aliases: []string{"docker.prunesafe"},
Group: "docker",
Summary: "安全清理未使用的容器、网络与构建缓存",
Action: "执行容器运行时的默认清理作用域:不清理全部未使用镜像(-a)," +
"不删除卷(--volumes),仅移除停止的容器、未使用的网络和悬空镜像",
Variants: []Variant{
{
Match: Match{OS: "linux", Distro: "centos", Runtime: "podman"},
Argv: []string{"podman", "system", "prune", "-f"},
Note: "CentOS 上的 Podman 运行时",
},
{
Match: Match{OS: "linux", Distro: "centos", Runtime: "docker"},
Argv: []string{"docker", "system", "prune", "-f"},
Note: "CentOS 上的 Docker 运行时",
},
{
Match: Match{OS: "linux", Runtime: "podman"},
Argv: []string{"podman", "system", "prune", "-f"},
Note: "任意 Linux 发行版上的 Podman 运行时",
},
{
Match: Match{OS: "linux", Runtime: "docker"},
Argv: []string{"docker", "system", "prune", "-f"},
Note: "任意 Linux 发行版上的 Docker 运行时",
},
{
Match: Match{OS: "darwin", Runtime: "docker"},
Argv: []string{"docker", "system", "prune", "-f"},
Note: "macOS 上的 Docker Desktop 运行时",
},
{
Match: Match{OS: "windows", Runtime: "docker"},
Argv: []string{"docker", "system", "prune", "-f"},
Note: "Windows 上的 Docker Desktop 运行时",
},
},
},
{
ID: "net.ports",
Group: "net",
Summary: "列出本机正在监听的 TCP/UDP 端口",
Action: "以只读方式列出监听中的套接字及持有进程",
Variants: []Variant{
{
Match: Match{OS: "linux"},
Argv: []string{"ss", "-tulnp"},
Note: "Linux 使用 iproute2 的 ss",
},
{
Match: Match{OS: "darwin"},
Argv: []string{"lsof", "-iTCP", "-sTCP:LISTEN", "-nP"},
Note: "macOS 使用 lsof",
},
{
Match: Match{OS: "windows"},
Argv: []string{"netstat", "-ano"},
Note: "Windows 使用 netstat",
},
},
},
{
ID: "pkg.refreshindex",
Group: "pkg",
Summary: "刷新系统包管理器的软件源索引",
Action: "只更新元数据索引,不升级或安装任何软件包",
Variants: []Variant{
{
Match: Match{OS: "linux", Distro: "centos"},
Argv: []string{"dnf", "makecache"},
Note: "CentOS 使用 dnf",
},
{
Match: Match{OS: "linux", DistroLike: []string{"rhel", "fedora"}},
Argv: []string{"dnf", "makecache"},
Note: "RHEL/Fedora 系发行版使用 dnf",
},
{
Match: Match{OS: "linux", DistroLike: []string{"debian", "ubuntu"}},
Argv: []string{"apt-get", "update"},
Note: "Debian/Ubuntu 系发行版使用 apt-get",
},
{
Match: Match{OS: "linux", Distro: "alpine"},
Argv: []string{"apk", "update"},
Note: "Alpine 使用 apk",
},
{
Match: Match{OS: "darwin"},
Argv: []string{"brew", "update"},
Note: "macOS 使用 Homebrew",
},
{
Match: Match{OS: "windows"},
Argv: []string{"winget", "source", "update"},
Note: "Windows 使用 winget",
},
},
},
}}
}
+120
View File
@@ -0,0 +1,120 @@
// Package registry holds kopsc's built-in command registry: platform-neutral
// intents, each with one or more platform-specific variants.
package registry
import (
"fmt"
"strings"
)
// Match constrains a Variant to environments. An empty field means "any".
// A Variant with a completely empty Match is a generic fallback.
type Match struct {
OS string // runtime.GOOS
Arch string // runtime.GOARCH
Distro string // exact os-release ID, e.g. "centos"
DistroLike []string // os-release ID_LIKE ancestry, e.g. "rhel"
Runtime string // container runtime required on PATH, e.g. "podman"
}
// Variant is one concrete implementation of a command.
type Variant struct {
Match Match
Argv []string // program followed by arguments
Shell bool // run through the platform shell instead of exec'ing Argv[0]
Note string // human-readable rationale shown by `kopsc explain`
}
// Command is a platform-neutral intent, e.g. "docker.clearsafe".
type Command struct {
ID string // hierarchical, dot-separated: "docker.clearsafe"
Aliases []string
Summary string
Group string // CLI grouping, e.g. "docker"
Action string // what the command does, including its safety contract
Variants []Variant
}
// Registry is an ordered set of commands.
type Registry struct {
Commands []Command
}
// Validate reports structural problems: empty or duplicate identifiers,
// empty aliases, commands without variants and variants without an argv.
func (r *Registry) Validate() error {
seenIDs := make(map[string]bool)
seenAliases := make(map[string]string)
for i, cmd := range r.Commands {
if strings.TrimSpace(cmd.ID) == "" {
return fmt.Errorf("command #%d: empty ID", i)
}
if seenIDs[cmd.ID] {
return fmt.Errorf("command #%d: duplicate ID %q", i, cmd.ID)
}
seenIDs[cmd.ID] = true
if strings.TrimSpace(cmd.Summary) == "" {
return fmt.Errorf("command %q: empty summary", cmd.ID)
}
if len(cmd.Variants) == 0 {
return fmt.Errorf("command %q: no variants", cmd.ID)
}
for _, alias := range cmd.Aliases {
if strings.TrimSpace(alias) == "" {
return fmt.Errorf("command %q: empty alias", cmd.ID)
}
if owner, ok := seenAliases[alias]; ok {
return fmt.Errorf("command %q: alias %q already used by %q", cmd.ID, alias, owner)
}
seenAliases[alias] = cmd.ID
}
for j, variant := range cmd.Variants {
if len(variant.Argv) == 0 || strings.TrimSpace(variant.Argv[0]) == "" {
return fmt.Errorf("command %q variant #%d: empty argv", cmd.ID, j)
}
if err := variant.Match.validate(); err != nil {
return fmt.Errorf("command %q variant #%d: %w", cmd.ID, j, err)
}
}
}
return nil
}
func (m Match) validate() error {
for _, field := range m.DistroLike {
if strings.TrimSpace(field) == "" {
return fmt.Errorf("empty DistroLike entry")
}
}
return nil
}
// Lookup finds a command by ID or alias.
func (r *Registry) Lookup(idOrAlias string) (*Command, bool) {
for i := range r.Commands {
cmd := &r.Commands[i]
if cmd.ID == idOrAlias {
return cmd, true
}
for _, alias := range cmd.Aliases {
if alias == idOrAlias {
return cmd, true
}
}
}
return nil, false
}
// IDs returns every command ID in registry order.
func (r *Registry) IDs() []string {
ids := make([]string, 0, len(r.Commands))
for _, cmd := range r.Commands {
ids = append(ids, cmd.ID)
}
return ids
}
+141
View File
@@ -0,0 +1,141 @@
package registry
import (
"reflect"
"strings"
"testing"
)
func TestBuiltinIsValid(t *testing.T) {
reg := Builtin()
if err := reg.Validate(); err != nil {
t.Fatalf("Builtin() is invalid: %v", err)
}
}
func TestBuiltinIDsStable(t *testing.T) {
reg := Builtin()
want := []string{"docker.clearsafe", "net.ports", "pkg.refreshindex"}
if got := reg.IDs(); !reflect.DeepEqual(got, want) {
t.Fatalf("IDs() = %v, want %v", got, want)
}
}
func TestBuiltinClearsafeIsSafe(t *testing.T) {
reg := Builtin()
cmd, ok := reg.Lookup("docker.clearsafe")
if !ok {
t.Fatal("docker.clearsafe not found")
}
if len(cmd.Variants) < 4 {
t.Fatalf("expected a variant per platform, got %d", len(cmd.Variants))
}
for _, variant := range cmd.Variants {
if len(variant.Argv) == 0 || (variant.Argv[0] != "docker" && variant.Argv[0] != "podman") {
t.Errorf("unexpected program %v", variant.Argv)
}
if strings.Join(variant.Argv, " ") != variant.Argv[0]+" system prune -f" {
t.Errorf("clearsafe must stay scoped to `system prune -f`, got %v", variant.Argv)
}
for _, forbidden := range []string{"-a", "--all", "--volumes", "-v", "--force=false"} {
for _, arg := range variant.Argv {
if arg == forbidden {
t.Errorf("variant %v must not pass %q", variant.Argv, forbidden)
}
}
}
}
}
func TestLookupAlias(t *testing.T) {
reg := Builtin()
cmd, ok := reg.Lookup("docker.prunesafe")
if !ok {
t.Fatal("alias lookup failed")
}
if cmd.ID != "docker.clearsafe" {
t.Fatalf("alias resolved to %q", cmd.ID)
}
if _, ok := reg.Lookup("does.not.exist"); ok {
t.Fatal("unexpected lookup hit")
}
}
func TestVariantPlatformsExplicit(t *testing.T) {
for _, cmd := range Builtin().Commands {
for _, variant := range cmd.Variants {
if variant.Match.OS == "" {
t.Errorf("command %q has a variant without an OS constraint: %v", cmd.ID, variant.Argv)
}
}
}
}
func plainCommand(id string) Command {
return Command{
ID: id,
Summary: "summary",
Variants: []Variant{{Argv: []string{"x"}}},
}
}
func TestValidateErrors(t *testing.T) {
withBlankDistroLike := plainCommand("a")
withBlankDistroLike.Variants = []Variant{{
Match: Match{DistroLike: []string{" "}},
Argv: []string{"x"},
}}
withEmptyArgv := plainCommand("a")
withEmptyArgv.Variants = []Variant{{}}
noVariants := plainCommand("a")
noVariants.Variants = nil
noSummary := plainCommand("a")
noSummary.Summary = ""
noID := plainCommand("")
noID.ID = ""
aliased := func(first, second []string) Registry {
a, b := plainCommand("a"), plainCommand("b")
a.Aliases, b.Aliases = first, second
return Registry{Commands: []Command{a, b}}
}
tests := []struct {
name string
registry Registry
wantErr string
}{
{"empty ID", Registry{Commands: []Command{noID}}, "empty ID"},
{"empty summary", Registry{Commands: []Command{noSummary}}, "empty summary"},
{"duplicate ID", Registry{Commands: []Command{plainCommand("a"), plainCommand("a")}}, "duplicate ID"},
{"duplicate alias", aliased([]string{"z"}, []string{"z"}), "already used"},
{"no variants", Registry{Commands: []Command{noVariants}}, "no variants"},
{"empty argv", Registry{Commands: []Command{withEmptyArgv}}, "empty argv"},
{"blank DistroLike", Registry{Commands: []Command{withBlankDistroLike}}, "empty DistroLike"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := tc.registry.Validate()
if err == nil {
t.Fatal("expected an error")
}
if !strings.Contains(err.Error(), tc.wantErr) {
t.Fatalf("error %q does not contain %q", err, tc.wantErr)
}
})
}
}
func TestMatchIsComparable(t *testing.T) {
a := Match{OS: "linux", Distro: "centos", DistroLike: []string{"rhel"}, Runtime: "docker"}
b := Match{OS: "linux", Distro: "centos", DistroLike: []string{"rhel"}, Runtime: "docker"}
if !reflect.DeepEqual(a, b) {
t.Fatal("Match should be a comparable plain struct value")
}
}