move core logic to internal/core package, bump to 0.1.3

This commit is contained in:
2026-03-05 14:56:28 +00:00
parent bc02c93acc
commit 0215a53fcf
8 changed files with 232 additions and 230 deletions
+260
View File
@@ -0,0 +1,260 @@
package core
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
)
// RepoPath returns the absolute path to the sigil repository.
// It uses SIGIL_REPO environment variable if set, otherwise defaults to ~/.dotfiles.
func RepoPath() (string, error) {
if override := os.Getenv("SIGIL_REPO"); override != "" {
return filepath.Abs(ExpandHome(override))
}
return filepath.Abs(ExpandHome("~/.dotfiles"))
}
// ExpandHome expands a leading ~ to the user's home directory.
// Returns the original path unchanged if expansion fails or path doesn't start with ~.
func ExpandHome(path string) string {
if path == "~" {
home, err := os.UserHomeDir()
if err != nil {
return path
}
return home
}
if strings.HasPrefix(path, "~/") {
home, err := os.UserHomeDir()
if err != nil {
return path
}
return filepath.Join(home, path[2:])
}
return path
}
// CompressHome compresses the user's home directory path to ~.
// Returns the original path if it doesn't start with the home directory.
func CompressHome(path string) string {
home, err := os.UserHomeDir()
if err != nil {
return path
}
clean := filepath.Clean(path)
homeClean := filepath.Clean(home)
if clean == homeClean {
return "~"
}
if strings.HasPrefix(clean, homeClean+string(os.PathSeparator)) {
rel := strings.TrimPrefix(clean, homeClean+string(os.PathSeparator))
return filepath.Join("~", rel)
}
return path
}
// SplitPackageSpec splits a package spec like "pkg:path/to/file" into (pkg, path).
// If no colon is present, returns (spec, "", nil).
func SplitPackageSpec(spec string) (string, string, error) {
if spec == "" {
return "", "", errors.New("missing package")
}
parts := strings.SplitN(spec, ":", 2)
pkg := parts[0]
rel := ""
if len(parts) == 2 {
rel = parts[1]
}
pkg = strings.Trim(pkg, "/")
rel = strings.TrimPrefix(rel, "/")
if pkg == "" {
return "", "", errors.New("invalid package")
}
return pkg, rel, nil
}
// resolvePackageSpec resolves a package spec (name, path, or repo path) to (package, relative path).
// It handles specs like "pkg:path", absolute paths within the repo, and absolute paths in the target.
func ResolvePackageSpec(spec string) (string, string, error) {
repo, err := RepoPath()
if err != nil {
return "", "", err
}
repoAbs, err := filepath.Abs(repo)
if err != nil {
return "", "", err
}
spec = ExpandHome(spec)
if filepath.IsAbs(spec) {
return ResolvePathSpec(spec, repoAbs)
}
if strings.Contains(spec, string(os.PathSeparator)) {
return ResolvePathSpec(spec, repoAbs)
}
clean := filepath.Clean(spec)
if strings.HasPrefix(clean, ".") || strings.HasPrefix(clean, string(os.PathSeparator)) {
return ResolvePathSpec(clean, repoAbs)
}
return SplitPackageSpec(spec)
}
// ResolvePathSpec resolves an absolute path spec to a package and relative path.
// It checks if the path is within the repo or matches a package's target directory.
func ResolvePathSpec(pathSpec, repoAbs string) (string, string, error) {
absPath, err := filepath.Abs(pathSpec)
if err != nil {
return "", "", err
}
if strings.HasPrefix(absPath, repoAbs+string(os.PathSeparator)) || absPath == repoAbs {
rel, err := filepath.Rel(repoAbs, absPath)
if err != nil {
return "", "", err
}
parts := strings.Split(rel, string(os.PathSeparator))
if len(parts) >= 1 {
pkg := parts[0]
if len(parts) >= 2 && parts[1] == FilesDirName {
relPath := filepath.Join(parts[2:]...)
return pkg, relPath, nil
}
return pkg, filepath.Join(parts[1:]...), nil
}
}
entries, err := os.ReadDir(repoAbs)
if err != nil {
return "", "", err
}
for _, entry := range entries {
if !entry.IsDir() || strings.HasPrefix(entry.Name(), ".") {
continue
}
pkgDir := filepath.Join(repoAbs, entry.Name())
configPath := filepath.Join(pkgDir, ConfigFileName)
cfg, err := LoadConfig(configPath)
if err != nil {
continue
}
targetRoot, err := SelectTarget(cfg)
if err != nil {
if errors.Is(err, ErrTargetDisabled) {
continue
}
continue
}
absTarget, err := filepath.Abs(ExpandHome(targetRoot))
if err != nil {
continue
}
if strings.HasPrefix(absPath, absTarget+string(os.PathSeparator)) || absPath == absTarget {
rel, err := filepath.Rel(absTarget, absPath)
if err != nil {
return "", "", err
}
return entry.Name(), rel, nil
}
}
return "", "", fmt.Errorf("could not resolve %s to a package", pathSpec)
}
// FindPackageByTarget finds a package name that has the given target root.
// Returns empty string if no package matches.
func FindPackageByTarget(repo, targetRoot string) (string, error) {
repoEntries, err := os.ReadDir(repo)
if err != nil {
return "", err
}
absTarget, err := filepath.Abs(ExpandHome(targetRoot))
if err != nil {
return "", err
}
for _, entry := range repoEntries {
if !entry.IsDir() || strings.HasPrefix(entry.Name(), ".") {
continue
}
pkgDir := filepath.Join(repo, entry.Name())
configPath := filepath.Join(pkgDir, ConfigFileName)
cfg, err := LoadConfig(configPath)
if err != nil {
continue
}
target, err := SelectTarget(cfg)
if err != nil {
if errors.Is(err, ErrTargetDisabled) {
continue
}
continue
}
absPkgTarget, err := filepath.Abs(ExpandHome(target))
if err != nil {
continue
}
if absPkgTarget == absTarget {
return entry.Name(), nil
}
}
return "", nil
}
// RepoPathForTarget returns the repo file path that a target symlink points to.
// Returns empty string if the target is not a symlink to the repo.
func RepoPathForTarget(targetPath, repo string) (string, error) {
info, err := os.Lstat(targetPath)
if err != nil {
return "", err
}
if info.Mode()&os.ModeSymlink == 0 {
return "", nil
}
src, err := os.Readlink(targetPath)
if err != nil {
return "", err
}
if !filepath.IsAbs(src) {
src = filepath.Join(filepath.Dir(targetPath), src)
}
src = filepath.Clean(src)
repoAbs, err := filepath.Abs(repo)
if err != nil {
return "", err
}
rel, err := filepath.Rel(repoAbs, src)
if err != nil {
return "", err
}
if strings.HasPrefix(rel, "..") {
return "", nil
}
parts := strings.Split(rel, string(os.PathSeparator))
if len(parts) < 3 {
return "", nil
}
if parts[1] != FilesDirName {
return "", nil
}
return filepath.Join(repoAbs, rel), nil
}