mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-10-15 12:15:11 +00:00
feat: plugindev sdk version management subcommand
- Add 'plugindev sdk' subcommand with list/install/use/path/current/latest - Replace runtime.Caller(0) path detection with managed SDK store - SDK stored at ~/.homeagent/plugindev/sdk/<version>/ - Active version tracked via current file - Lazy initialization to avoid blocking other subcommands
This commit is contained in:
@ -4,25 +4,12 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
)
|
)
|
||||||
|
|
||||||
// sdkRoot is the HomeAgent SDK root directory, computed at init time from source location.
|
|
||||||
var sdkRoot string
|
|
||||||
|
|
||||||
const cabiVersion = 1
|
const cabiVersion = 1
|
||||||
|
|
||||||
func init() {
|
|
||||||
_, filename, _, ok := runtime.Caller(0)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// filename: <sdk_root>/tools/plugindev/cmd_init.go
|
|
||||||
sdkRoot = filepath.Dir(filepath.Dir(filepath.Dir(filename)))
|
|
||||||
}
|
|
||||||
|
|
||||||
type PlgConfig struct {
|
type PlgConfig struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
NameZh string `json:"name_zh"`
|
NameZh string `json:"name_zh"`
|
||||||
@ -152,11 +139,8 @@ func cmdInit(args []string) {
|
|||||||
|
|
||||||
// detectSDKInfo reads the HomeAgent SDK's go.mod to get module path and go version.
|
// detectSDKInfo reads the HomeAgent SDK's go.mod to get module path and go version.
|
||||||
func detectSDKInfo() (modulePath, goVersion, sdkPath string) {
|
func detectSDKInfo() (modulePath, goVersion, sdkPath string) {
|
||||||
if sdkRoot == "" {
|
root := activeSDKRoot()
|
||||||
fmt.Printf("error: cannot detect SDK root (built outside SDK tree?)\n")
|
gomodPath := filepath.Join(root, "go.mod")
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
gomodPath := filepath.Join(sdkRoot, "go.mod")
|
|
||||||
data, err := os.ReadFile(gomodPath)
|
data, err := os.ReadFile(gomodPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("error: cannot read SDK go.mod at %s: %v\n", gomodPath, err)
|
fmt.Printf("error: cannot read SDK go.mod at %s: %v\n", gomodPath, err)
|
||||||
@ -179,7 +163,7 @@ func detectSDKInfo() (modulePath, goVersion, sdkPath string) {
|
|||||||
if goVersion == "" {
|
if goVersion == "" {
|
||||||
goVersion = "1.21"
|
goVersion = "1.21"
|
||||||
}
|
}
|
||||||
return modulePath, goVersion, sdkRoot
|
return modulePath, goVersion, root
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeTemplate(path, content string, data TemplateData) {
|
func writeTemplate(path, content string, data TemplateData) {
|
||||||
|
|||||||
375
tools/plugindev/cmd_sdk.go
Normal file
375
tools/plugindev/cmd_sdk.go
Normal file
@ -0,0 +1,375 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const sdkDirName = "plugindev/sdk"
|
||||||
|
|
||||||
|
// sdkStore returns the root directory for stored SDK versions.
|
||||||
|
func sdkStore() string {
|
||||||
|
if v := os.Getenv("HOMEAGENT_SDK_DIR"); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error: cannot determine home directory: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return filepath.Join(home, ".homeagent", sdkDirName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sdkCurrentDir() string {
|
||||||
|
return filepath.Join(sdkStore(), "current")
|
||||||
|
}
|
||||||
|
|
||||||
|
func sdkVersionDir(version string) string {
|
||||||
|
return filepath.Join(sdkStore(), version)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sdkRepoURL = "https://gitcode.com/JianFeeeee/homeagent-sdk.git"
|
||||||
|
|
||||||
|
func cmdSDK(args []string) {
|
||||||
|
if len(args) < 1 {
|
||||||
|
sdkHelp()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch args[0] {
|
||||||
|
case "list":
|
||||||
|
cmdSDKList()
|
||||||
|
case "install":
|
||||||
|
if len(args) < 2 {
|
||||||
|
fmt.Println("Usage: plugindev sdk install <version>")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
cmdSDKInstall(args[1])
|
||||||
|
case "use":
|
||||||
|
if len(args) < 2 {
|
||||||
|
fmt.Println("Usage: plugindev sdk use <version>")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
cmdSDKUse(args[1])
|
||||||
|
case "path":
|
||||||
|
cmdSDKPath()
|
||||||
|
case "current":
|
||||||
|
cmdSDKCurrent()
|
||||||
|
case "latest":
|
||||||
|
cmdSDKLatest()
|
||||||
|
default:
|
||||||
|
sdkHelp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sdkHelp() {
|
||||||
|
fmt.Print(`Usage: plugindev sdk <command>
|
||||||
|
|
||||||
|
Manage installed HomeAgent SDK versions.
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
list List installed SDK versions
|
||||||
|
install <version> Download and install an SDK version (tag or branch)
|
||||||
|
use <version> Switch to the specified SDK version for new projects
|
||||||
|
path Show the current active SDK directory
|
||||||
|
current Show the current active SDK version
|
||||||
|
latest Show the latest available version from remote
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
plugindev sdk install v0.7.1
|
||||||
|
plugindev sdk install latest
|
||||||
|
plugindev sdk use v0.7.1
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cmdSDKList lists installed SDK versions.
|
||||||
|
func cmdSDKList() {
|
||||||
|
store := sdkStore()
|
||||||
|
entries, err := os.ReadDir(store)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
fmt.Println("No SDK versions installed.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("error: read SDK store %s: %v\n", store, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
current := resolveCurrentVersion(store)
|
||||||
|
|
||||||
|
var versions []string
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.IsDir() && e.Name() != "current" && !strings.HasPrefix(e.Name(), ".") {
|
||||||
|
versions = append(versions, e.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Sort(sort.Reverse(sort.StringSlice(versions)))
|
||||||
|
|
||||||
|
if len(versions) == 0 {
|
||||||
|
fmt.Println("No SDK versions installed.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("Installed SDK versions:")
|
||||||
|
for _, v := range versions {
|
||||||
|
mark := " "
|
||||||
|
if v == current {
|
||||||
|
mark = "*"
|
||||||
|
}
|
||||||
|
fmt.Printf(" %s %s\n", mark, v)
|
||||||
|
}
|
||||||
|
if current == "" {
|
||||||
|
fmt.Println("\nNo version active. Use 'plugindev sdk use <version>' to set one.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cmdSDKInstall downloads and installs an SDK version.
|
||||||
|
func cmdSDKInstall(version string) {
|
||||||
|
store := sdkStore()
|
||||||
|
if err := os.MkdirAll(store, 0755); err != nil {
|
||||||
|
fmt.Printf("error: create SDK store %s: %v\n", store, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if version == "latest" {
|
||||||
|
tag, err := fetchLatestTag()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error: fetch latest tag: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
version = tag
|
||||||
|
fmt.Printf("Latest version: %s\n", version)
|
||||||
|
}
|
||||||
|
|
||||||
|
dest := sdkVersionDir(version)
|
||||||
|
if _, err := os.Stat(dest); err == nil {
|
||||||
|
fmt.Printf("SDK version %s already installed at %s\n", version, dest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Downloading SDK version %s...\n", version)
|
||||||
|
|
||||||
|
tmpDir, err := os.MkdirTemp("", "homeagent-sdk-*")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error: create temp dir: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
cloneArgs := []string{"clone", "--depth", "1", "--branch", version, sdkRepoURL, tmpDir}
|
||||||
|
cmd := exec.Command("git", cloneArgs...)
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
fmt.Printf("error: failed to clone SDK version %s: %v\n", version, err)
|
||||||
|
fmt.Println("Make sure git is installed and the version tag exists.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Rename(tmpDir, dest); err != nil {
|
||||||
|
fmt.Printf("error: move SDK to store: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("SDK version %s installed at %s\n", version, dest)
|
||||||
|
|
||||||
|
// Auto-switch to newly installed version if no version is currently active
|
||||||
|
if resolveCurrentVersion(store) == "" {
|
||||||
|
setCurrentVersion(store, version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cmdSDKUse switches the active SDK version.
|
||||||
|
func cmdSDKUse(version string) {
|
||||||
|
store := sdkStore()
|
||||||
|
verDir := sdkVersionDir(version)
|
||||||
|
if _, err := os.Stat(verDir); os.IsNotExist(err) {
|
||||||
|
fmt.Printf("SDK version %s is not installed.\n", version)
|
||||||
|
fmt.Printf("Install it first: plugindev sdk install %s\n", version)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
setCurrentVersion(store, version)
|
||||||
|
fmt.Printf("Active SDK version set to %s\n", version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cmdSDKPath prints the active SDK directory.
|
||||||
|
func cmdSDKPath() {
|
||||||
|
store := sdkStore()
|
||||||
|
current := resolveCurrentVersion(store)
|
||||||
|
if current == "" {
|
||||||
|
fmt.Println("No active SDK version set.")
|
||||||
|
fmt.Println("Use 'plugindev sdk use <version>' to set one.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println(sdkVersionDir(current))
|
||||||
|
}
|
||||||
|
|
||||||
|
// cmdSDKCurrent prints the active SDK version.
|
||||||
|
func cmdSDKCurrent() {
|
||||||
|
store := sdkStore()
|
||||||
|
current := resolveCurrentVersion(store)
|
||||||
|
if current == "" {
|
||||||
|
fmt.Println("No active SDK version set.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println(current)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cmdSDKLatest fetches the latest tag from the remote SDK repo.
|
||||||
|
func cmdSDKLatest() {
|
||||||
|
tag, err := fetchLatestTag()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error: fetch latest tag: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveCurrentVersion reads the current active version.
|
||||||
|
func resolveCurrentVersion(store string) string {
|
||||||
|
currentFile := filepath.Join(store, "current")
|
||||||
|
data, err := os.ReadFile(currentFile)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
v := strings.TrimSpace(string(data))
|
||||||
|
if v == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
verDir := filepath.Join(store, v)
|
||||||
|
if _, err := os.Stat(verDir); os.IsNotExist(err) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// setCurrentVersion writes the version name to the current file.
|
||||||
|
func setCurrentVersion(store, version string) {
|
||||||
|
currentFile := filepath.Join(store, "current")
|
||||||
|
if err := os.WriteFile(currentFile, []byte(version+"\n"), 0644); err != nil {
|
||||||
|
fmt.Printf("error: write current version: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchLatestTag uses git ls-remote to find the latest semver tag.
|
||||||
|
func fetchLatestTag() (string, error) {
|
||||||
|
cmd := exec.Command("git", "ls-remote", "--tags", sdkRepoURL)
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("git ls-remote failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tags := parseTags(string(out))
|
||||||
|
if len(tags) == 0 {
|
||||||
|
return "", fmt.Errorf("no tags found in remote repository")
|
||||||
|
}
|
||||||
|
return tags[len(tags)-1], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseTags extracts semver tags from git ls-remote output and sorts them.
|
||||||
|
func parseTags(output string) []string {
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
var tags []string
|
||||||
|
for _, line := range strings.Split(output, "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.Fields(line)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ref := parts[1]
|
||||||
|
// Only refs/tags/v*, skip refs/tags/v*-^{}
|
||||||
|
if !strings.HasPrefix(ref, "refs/tags/v") || strings.HasSuffix(ref, "^{}") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tag := strings.TrimPrefix(ref, "refs/tags/")
|
||||||
|
if seen[tag] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[tag] = true
|
||||||
|
tags = append(tags, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(tags, func(i, j int) bool {
|
||||||
|
return compareSemver(tags[i], tags[j]) < 0
|
||||||
|
})
|
||||||
|
return tags
|
||||||
|
}
|
||||||
|
|
||||||
|
// compareSemver compares two semver tags (vX.Y.Z). Returns -1, 0, or 1.
|
||||||
|
func compareSemver(a, b string) int {
|
||||||
|
an := parseSemver(a)
|
||||||
|
bn := parseSemver(b)
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
if an[i] != bn[i] {
|
||||||
|
if an[i] < bn[i] {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSemver extracts [major, minor, patch] from a vX.Y.Z string.
|
||||||
|
func parseSemver(tag string) [3]int {
|
||||||
|
var v [3]int
|
||||||
|
s := strings.TrimPrefix(tag, "v")
|
||||||
|
parts := strings.SplitN(s, ".", 3)
|
||||||
|
for i, p := range parts {
|
||||||
|
if i >= 3 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
n := 0
|
||||||
|
fmt.Sscanf(p, "%d", &n)
|
||||||
|
v[i] = n
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// activeSDKRoot returns the path to the active SDK root.
|
||||||
|
// It replaces the old runtime.Caller(0) approach so plugindev can work
|
||||||
|
// independently of its own build location.
|
||||||
|
func activeSDKRoot() string {
|
||||||
|
store := sdkStore()
|
||||||
|
current := resolveCurrentVersion(store)
|
||||||
|
if current == "" {
|
||||||
|
fmt.Printf("error: no active SDK version set\n")
|
||||||
|
fmt.Printf(" Install one: plugindev sdk install latest\n")
|
||||||
|
fmt.Printf(" Or set one: plugindev sdk use <version>\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
root := sdkVersionDir(current)
|
||||||
|
if _, err := os.Stat(root); os.IsNotExist(err) {
|
||||||
|
fmt.Printf("error: active SDK version %s not found at %s\n", current, root)
|
||||||
|
fmt.Printf(" Reinstall: plugindev sdk install %s\n", current)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
// readMetaVersion reads the Version string from the SDK's meta/meta.go.
|
||||||
|
// If the file is missing or unreadable, returns "0.0.0".
|
||||||
|
func readMetaVersion(sdkRoot string) string {
|
||||||
|
metaPath := filepath.Join(sdkRoot, "meta", "meta.go")
|
||||||
|
data, err := os.ReadFile(metaPath)
|
||||||
|
if err != nil {
|
||||||
|
return "0.0.0"
|
||||||
|
}
|
||||||
|
for _, line := range strings.Split(string(data), "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(line, "Version = ") {
|
||||||
|
v := strings.TrimPrefix(line, "Version = ")
|
||||||
|
v = strings.Trim(v, `"`)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "0.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -19,19 +19,22 @@ func main() {
|
|||||||
cmdClean(os.Args[2:])
|
cmdClean(os.Args[2:])
|
||||||
case "debug":
|
case "debug":
|
||||||
cmdDebug(os.Args[2:])
|
cmdDebug(os.Args[2:])
|
||||||
|
case "sdk":
|
||||||
|
cmdSDK(os.Args[2:])
|
||||||
default:
|
default:
|
||||||
help()
|
help()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func help() {
|
func help() {
|
||||||
fmt.Println(`HomeAgent Plugin Dev Tool
|
fmt.Print(`HomeAgent Plugin Dev Tool
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
plugindev init <name> Scaffold a new plugin project
|
plugindev init <name> Scaffold a new plugin project
|
||||||
plugindev build [flags] Compile and package plugin
|
plugindev build [flags] Compile and package plugin
|
||||||
plugindev clean Clean build/dist artifacts
|
plugindev clean Clean build/dist artifacts
|
||||||
plugindev debug [dir] Interpret and debug plugin source
|
plugindev debug [dir] Interpret and debug plugin source
|
||||||
|
plugindev sdk <command> Manage SDK versions
|
||||||
|
|
||||||
Flags:
|
Flags:
|
||||||
--outdir Output directory (default: dist)
|
--outdir Output directory (default: dist)
|
||||||
|
|||||||
Reference in New Issue
Block a user