refactor(toolchain)!: 工具链 plugindev 更名为 hmapdev,module path 改回 gitcode

- 目录 tools/plugindev → tools/hmapdev,可执行文件名/平台产物名同步
  (hmapdev_linux_amd64 等;包格式仍叫 .hmap)
- module path github.com/JianFeeeee/homeagent-sdk/tools/... → gitcode.com/...
  (与仓库实际托管一致;核心仓不依赖该 path,改动无外部影响)
- SDK 存储目录 ~/.homeagent/plugindev/sdk → ~/.homeagent/hmapdev/sdk
  新目录不存在而旧目录存在时沿用旧目录 → 已装 SDK 版本不会丢失
- 命令表/usage/--help/生成项目 README/示例 README/NSIS 安装器/
  package/build.sh/build-examples.sh 全部同步;PLUGINDEV 环境变量保留兼容
- sdk/ 目录零改动(公开接口不变)

验证:
- go build ./... ok;go test ./tools/hmapdev/ ok(含模板接线守卫 TestProcTemplate_CoversAllCoreMethods)
- bash -n package/{build,build-examples}.sh ok
- 端到端:hmapdev init demo && hmapdev build → dist/demo_bundle.hmap(linux+darwin)
- 本机安装 /usr/local/bin/hmapdev,旧名以软链保留;sdk list/current 正常
This commit is contained in:
JianFeeeee
2026-09-12 12:30:20 +08:00
parent 69ff3089a4
commit b237787c90
33 changed files with 135 additions and 122 deletions

524
tools/hmapdev/cmd_sdk.go Normal file
View File

@ -0,0 +1,524 @@
package main
import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"time"
)
const sdkDirName = "hmapdev/sdk"
// legacySDKDirName 是改名前的存储目录。工具链在 1.2.0 从 plugindev 改名 hmapdev
// 已装过旧版的机器上 SDK 仍在旧路径,直接换名会让它找不到已装 SDK
// (表现为「没有活动版本」)。新目录不存在而旧目录存在时沿用旧目录。
const legacySDKDirName = "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)
}
dir := filepath.Join(home, ".homeagent", sdkDirName)
if _, err := os.Stat(dir); err != nil {
if legacy := filepath.Join(home, ".homeagent", legacySDKDirName); legacy != "" {
if _, err := os.Stat(legacy); err == nil {
return legacy
}
}
}
return dir
}
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"
const sdkDownloadURL = "https://gitcode.com/JianFeeeee/homeagent-sdk/-/archive/%s/homeagent-sdk-%s.tar.gz"
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: hmapdev sdk install <version>")
os.Exit(1)
}
cmdSDKInstall(args[1])
case "use":
if len(args) < 2 {
fmt.Println("Usage: hmapdev 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: hmapdev 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:
hmapdev sdk install v0.7.1
hmapdev sdk install latest
hmapdev 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 'hmapdev sdk use <version>' to set one.")
}
}
// cmdSDKInstall downloads and installs an SDK version from Release archive.
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
}
url := fmt.Sprintf(sdkDownloadURL, version, version)
fmt.Printf("Downloading SDK %s from Release archive...\n", version)
tmpDir, err := os.MkdirTemp("", "homeagent-sdk-extract-*")
if err != nil {
fmt.Printf("error: create temp dir: %v\n", err)
os.Exit(1)
}
defer os.RemoveAll(tmpDir)
if err := installFromArchive(url, tmpDir); err != nil {
fmt.Printf("warn: archive download failed (%v), falling back to git clone...\n", err)
if err := installFromGit(version, tmpDir); err != nil {
fmt.Printf("error: install SDK %s: %v\n", version, err)
os.Exit(1)
}
}
if err := os.Rename(tmpDir, dest); err != nil {
// Cross-filesystem rename fallback
if err := copyDir(tmpDir, dest); err != nil {
fmt.Printf("error: move SDK to store: %v\n", err)
os.Exit(1)
}
os.RemoveAll(tmpDir)
}
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)
}
}
func openFile(path string) (*os.File, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
return f, nil
}
// installFromArchive downloads the SDK release archive and extracts it to tmpDir.
func installFromArchive(url, tmpDir string) error {
tmpFile, err := os.CreateTemp("", "homeagent-sdk-*.tar.gz")
if err != nil {
return err
}
tmpPath := tmpFile.Name()
defer os.Remove(tmpPath)
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("download SDK %s: HTTP %d", url, resp.StatusCode)
}
if _, err := io.Copy(tmpFile, resp.Body); err != nil {
return err
}
tmpFile.Close()
f, err := openFile(tmpPath)
if err != nil {
return err
}
gzr, err := gzip.NewReader(f)
if err != nil {
f.Close()
return err
}
defer gzr.Close()
defer f.Close()
tr := tar.NewReader(gzr)
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
// Strip top-level directory from archive path
parts := strings.SplitN(header.Name, "/", 2)
if len(parts) < 2 {
continue
}
relPath := parts[1]
if relPath == "" {
continue
}
target := filepath.Join(tmpDir, relPath)
switch header.Typeflag {
case tar.TypeDir:
os.MkdirAll(target, os.FileMode(header.Mode))
case tar.TypeReg:
os.MkdirAll(filepath.Dir(target), 0755)
f, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY, os.FileMode(header.Mode))
if err != nil {
return err
}
if _, err := io.Copy(f, tr); err != nil {
f.Close()
return err
}
f.Close()
}
}
return nil
}
// installFromGit clones the SDK repo at the given tag/branch into tmpDir.
func installFromGit(version, tmpDir string) error {
cmd := exec.Command("git", "clone", "--depth", "1", "--branch", version, sdkRepoURL, tmpDir)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("git clone: %v", err)
}
return os.RemoveAll(filepath.Join(tmpDir, ".git"))
}
// 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: hmapdev 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 'hmapdev 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[-pre] string.
// Prerelease tags parse to the same major.minor.patch as their release (ignoring prerelease).
func parseSemver(tag string) [3]int {
var v [3]int
s := strings.TrimPrefix(tag, "v")
// Strip prerelease suffix (-...)
if idx := strings.IndexByte(s, '-'); idx >= 0 {
s = s[:idx]
}
parts := strings.SplitN(s, ".", 3)
for i := 0; i < 3 && i < len(parts); i++ {
n := 0
fmt.Sscanf(parts[i], "%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 hmapdev 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: hmapdev sdk install latest\n")
fmt.Printf(" Or set one: hmapdev 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: hmapdev sdk install %s\n", current)
os.Exit(1)
}
return root
}
// copyDir recursively copies src to dst (cross-filesystem rename fallback).
func copyDir(src, dst string) error {
if err := os.MkdirAll(dst, 0755); err != nil {
return err
}
entries, err := os.ReadDir(src)
if err != nil {
return err
}
for _, e := range entries {
srcPath := filepath.Join(src, e.Name())
dstPath := filepath.Join(dst, e.Name())
if e.IsDir() {
if err := copyDir(srcPath, dstPath); err != nil {
return err
}
} else {
data, err := os.ReadFile(srcPath)
if err != nil {
return err
}
if err := os.WriteFile(dstPath, data, 0644); err != nil {
return err
}
}
}
return nil
}
// 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"
}