184 lines
4.1 KiB
Go
184 lines
4.1 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
func expandPath(path string) (string, error) {
|
|
if path == "~" || strings.HasPrefix(path, "~/") {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if path == "~" {
|
|
return home, nil
|
|
}
|
|
return filepath.Join(home, strings.TrimPrefix(path, "~/")), nil
|
|
}
|
|
|
|
return path, nil
|
|
}
|
|
|
|
func dirLooksLikeFrontendSource(path string) bool {
|
|
info, err := os.Stat(path)
|
|
if err != nil || !info.IsDir() {
|
|
return false
|
|
}
|
|
|
|
entries, err := os.ReadDir(path)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
continue
|
|
}
|
|
|
|
name := entry.Name()
|
|
if strings.HasSuffix(name, ".ts") || strings.HasSuffix(name, ".tsx") || strings.HasSuffix(name, ".js") || strings.HasSuffix(name, ".jsx") {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func dirExists(path string) bool {
|
|
info, err := os.Stat(path)
|
|
return err == nil && info.IsDir()
|
|
}
|
|
|
|
func samePath(a, b string) bool {
|
|
if a == "" || b == "" {
|
|
return false
|
|
}
|
|
|
|
absA, err := filepath.Abs(a)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
absB, err := filepath.Abs(b)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
infoA, errA := os.Stat(absA)
|
|
infoB, errB := os.Stat(absB)
|
|
if errA == nil && errB == nil && os.SameFile(infoA, infoB) {
|
|
return true
|
|
}
|
|
|
|
realA, errA := filepath.EvalSymlinks(absA)
|
|
realB, errB := filepath.EvalSymlinks(absB)
|
|
if errA == nil && errB == nil {
|
|
return realA == realB
|
|
}
|
|
return filepath.Clean(absA) == filepath.Clean(absB)
|
|
}
|
|
|
|
// consolidatedWorkspaceRoot locates the parent that owns the independent CMS,
|
|
// orchestrator, and check-safety repositories. The checkout may live anywhere
|
|
// (for example /srv/agent-work/blockninja), so discovery starts from runtime
|
|
// locations before retaining the historical ~/src/blockninja fallback.
|
|
func consolidatedWorkspaceRoot() string {
|
|
var candidates []string
|
|
if cwd, err := os.Getwd(); err == nil {
|
|
candidates = append(candidates, cwd)
|
|
}
|
|
if executable, err := os.Executable(); err == nil {
|
|
candidates = append(candidates, filepath.Dir(executable))
|
|
}
|
|
if _, sourceFile, _, ok := runtime.Caller(0); ok {
|
|
candidates = append(candidates, filepath.Dir(sourceFile))
|
|
}
|
|
if home, err := os.UserHomeDir(); err == nil {
|
|
candidates = append(candidates, filepath.Join(home, "src", "blockninja"))
|
|
}
|
|
|
|
seen := make(map[string]bool, len(candidates))
|
|
for _, candidate := range candidates {
|
|
root := findConsolidatedWorkspaceRoot(candidate)
|
|
if root == "" || seen[root] {
|
|
continue
|
|
}
|
|
seen[root] = true
|
|
return root
|
|
}
|
|
|
|
if home, err := os.UserHomeDir(); err == nil {
|
|
return filepath.Join(home, "src", "blockninja")
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func findConsolidatedWorkspaceRoot(start string) string {
|
|
current, err := filepath.Abs(start)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
if info, statErr := os.Stat(current); statErr == nil && !info.IsDir() {
|
|
current = filepath.Dir(current)
|
|
}
|
|
|
|
for {
|
|
if fileExists(filepath.Join(current, "cms", "backend", "go.mod")) &&
|
|
fileExists(filepath.Join(current, "orchestrator", "backend", "go.mod")) &&
|
|
fileExists(filepath.Join(current, "check-safety", "go.mod")) {
|
|
return current
|
|
}
|
|
parent := filepath.Dir(current)
|
|
if parent == current {
|
|
return ""
|
|
}
|
|
current = parent
|
|
}
|
|
}
|
|
|
|
func normalizeDisplayLabel(label string) string {
|
|
if label == "." {
|
|
return ""
|
|
}
|
|
return label
|
|
}
|
|
|
|
func fileExists(path string) bool {
|
|
info, err := os.Stat(path)
|
|
return err == nil && !info.IsDir()
|
|
}
|
|
|
|
func prefixDisplayPath(prefix, path string) string {
|
|
if prefix == "" || prefix == "." {
|
|
return path
|
|
}
|
|
if path == "" {
|
|
return prefix
|
|
}
|
|
return prefix + "/" + filepath.ToSlash(path)
|
|
}
|
|
|
|
func shortPluginLabel(path string) string {
|
|
cleaned := filepath.Clean(path)
|
|
base := filepath.Base(cleaned)
|
|
if base == "." || base == string(filepath.Separator) || base == "" {
|
|
return path
|
|
}
|
|
return base
|
|
}
|
|
|
|
func isPluginModuleRoot(root string) bool {
|
|
modulePath, err := readModulePath(root)
|
|
if err != nil {
|
|
return fileExists(filepath.Join(root, "plugin.mod"))
|
|
}
|
|
return strings.Contains(modulePath, "/internal/plugins/") || fileExists(filepath.Join(root, "plugin.mod"))
|
|
}
|
|
|
|
func containsString(values []string, target string) bool {
|
|
return slices.Contains(values, target)
|
|
}
|