feat(daemon-beta t2): template 代裝——embed v1.18.0 快照(36檔)/冪等不覆寫/daemon 啟動自動鋪/template-install 子命令;scan 加 SkipDirNames 防 system-dev 與 CLAUDE.md 被當知識掃

- 測試 3 支新增全綠(首鋪/冪等護用戶檔/掃描跳 template 產物——第三支先紅抓到 CLAUDE.md 洩入再修綠)
- CLI 實跑:新鋪 36 檔→重跑 0 新檔(冪等證據)
This commit is contained in:
2026-07-24 12:18:37 +08:00
parent b355165180
commit 73a5a67b72
41 changed files with 2917 additions and 1 deletions
+116
View File
@@ -0,0 +1,116 @@
// template_install.go — daemon 代裝 system-dev-templatedaemon-beta task 2)。
//
// leo 四步定稿第 1 步:「幫你在指定資料夾安裝我們現有的 template,這個測過很多次,
// 但要讓 daemon 可用」——即**不走 git clone**template 快照(templatefs/vendored
// 打包進二進位,daemon 直接鋪檔。
//
// 冪等鐵律:**已存在的檔案一律不覆寫**(用戶的 status.md/wiki 是他的資產;
// 升級 template 版本=另案 update 流程,不在本函式)。
package main
import (
"embed"
"fmt"
"io/fs"
"os"
"path/filepath"
)
// templateFS 是 system-dev-template 的 vendored 快照(版本見 templatefs/system-dev/VERSION)。
//
//go:embed all:templatefs
var templateFS embed.FS
const templateFSRoot = "templatefs"
// TemplateInstallResult 是一次代裝的結果帳目。
type TemplateInstallResult struct {
Installed []string `json:"installed"` // 本次新鋪的檔(相對路徑)
Skipped []string `json:"skipped"` // 已存在故跳過的檔
Version string `json:"version"` // 快照版本(templatefs/system-dev/VERSION
}
// TemplateVersion 讀出內嵌快照的版本號。
func TemplateVersion() string {
data, err := templateFS.ReadFile(templateFSRoot + "/system-dev/VERSION")
if err != nil {
return "unknown"
}
v := string(data)
for len(v) > 0 && (v[len(v)-1] == '\n' || v[len(v)-1] == '\r') {
v = v[:len(v)-1]
}
return v
}
// InstallTemplate 把內嵌 template 鋪進 root。冪等:既有檔案跳過不覆寫。
func InstallTemplate(root string) (*TemplateInstallResult, error) {
res := &TemplateInstallResult{Version: TemplateVersion()}
absRoot, err := filepath.Abs(root)
if err != nil {
return nil, err
}
if err := os.MkdirAll(absRoot, 0o755); err != nil {
return nil, fmt.Errorf("建目標資料夾失敗:%w", err)
}
err = fs.WalkDir(templateFS, templateFSRoot, func(p string, d fs.DirEntry, werr error) error {
if werr != nil {
return werr
}
if d.IsDir() {
return nil
}
rel, rerr := filepath.Rel(templateFSRoot, p)
if rerr != nil {
return rerr
}
dest := filepath.Join(absRoot, rel)
if _, serr := os.Stat(dest); serr == nil {
res.Skipped = append(res.Skipped, rel)
return nil // 冪等:不覆寫用戶既有檔
}
if merr := os.MkdirAll(filepath.Dir(dest), 0o755); merr != nil {
return merr
}
data, derr := templateFS.ReadFile(p)
if derr != nil {
return derr
}
if werr2 := os.WriteFile(dest, data, 0o644); werr2 != nil {
return werr2
}
res.Installed = append(res.Installed, rel)
return nil
})
if err != nil {
return nil, err
}
return res, nil
}
// TemplateInstalled 判斷 root 是否已鋪過(以 wiki 標記檔存在為準)。
func TemplateInstalled(root string) bool {
_, err := os.Stat(filepath.Join(root, "system-dev", "wiki", "status.md"))
return err == nil
}
// runTemplateInstall 是 `collector template-install` 子命令主體。
func runTemplateInstall(args []string) int {
fs2 := newFlagSet()
folder := fs2.String("folder", "", "要鋪 template 的資料夾(必填)")
if err := fs2.Parse(args); err != nil {
return 2
}
if *folder == "" {
fmt.Fprintln(os.Stderr, "錯誤:--folder 為必填")
return 2
}
res, err := InstallTemplate(*folder)
if err != nil {
fmt.Fprintln(os.Stderr, "collector template-install:", err)
return 1
}
fmt.Printf("template v%s:新鋪 %d 檔、跳過既有 %d 檔 → %s\n",
res.Version, len(res.Installed), len(res.Skipped), *folder)
return 0
}