go/info.go
Snider 7e2783dcf5 feat: add core.Path() + core.Env() fallthrough + PathGlob/PathIsAbs/CleanPath
Path() builds OS-aware absolute paths using Env("DS") — single point
of responsibility for filesystem paths. Relative paths anchor to
DIR_HOME. cleanPath resolves .. and double separators.

Env() now falls through to os.Getenv for unknown keys — universal
replacement for os.Getenv. Core keys (OS, DIR_HOME, etc.) take
precedence, arbitrary env vars pass through.

New exports: Path, PathBase, PathDir, PathExt, PathIsAbs, PathGlob,
CleanPath. Info init moved to init() so Path() can be used during
population without init cycle. DIR_HOME respects CORE_HOME env var
override for agent workspace sandboxing.

13 path tests, 17 env tests — all passing.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 09:50:50 +00:00

134 lines
3.8 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
// System information registry for the Core framework.
// Read-only key-value store of environment facts, populated once at init.
// Env is environment. Config is ours.
//
// System keys:
//
// core.Env("OS") // "darwin"
// core.Env("ARCH") // "arm64"
// core.Env("GO") // "go1.26"
// core.Env("DS") // "/" (directory separator)
// core.Env("PS") // ":" (path list separator)
// core.Env("HOSTNAME") // "cladius"
// core.Env("USER") // "snider"
// core.Env("PID") // "12345"
// core.Env("NUM_CPU") // "10"
//
// Directory keys:
//
// core.Env("DIR_HOME") // "/Users/snider"
// core.Env("DIR_CONFIG") // "~/Library/Application Support"
// core.Env("DIR_CACHE") // "~/Library/Caches"
// core.Env("DIR_DATA") // "~/Library" (platform-specific)
// core.Env("DIR_TMP") // "/tmp"
// core.Env("DIR_CWD") // current working directory
// core.Env("DIR_DOWNLOADS") // "~/Downloads"
// core.Env("DIR_CODE") // "~/Code"
//
// Timestamp keys:
//
// core.Env("CORE_START") // "2026-03-22T14:30:00Z"
package core
import (
"os"
"runtime"
"strconv"
"time"
)
// SysInfo holds read-only system information, populated once at init.
type SysInfo struct {
values map[string]string
}
// systemInfo is declared empty — populated in init() so Path() can be used
// without creating an init cycle.
var systemInfo = &SysInfo{values: make(map[string]string)}
func init() {
i := systemInfo
// System
i.values["OS"] = runtime.GOOS
i.values["ARCH"] = runtime.GOARCH
i.values["GO"] = runtime.Version()
i.values["DS"] = string(os.PathSeparator)
i.values["PS"] = string(os.PathListSeparator)
i.values["PID"] = strconv.Itoa(os.Getpid())
i.values["NUM_CPU"] = strconv.Itoa(runtime.NumCPU())
i.values["USER"] = Username()
if h, err := os.Hostname(); err == nil {
i.values["HOSTNAME"] = h
}
// Directories — DS and DIR_HOME set first so Path() can use them.
// CORE_HOME overrides os.UserHomeDir() (e.g., agent workspaces).
if d := os.Getenv("CORE_HOME"); d != "" {
i.values["DIR_HOME"] = d
} else if d, err := os.UserHomeDir(); err == nil {
i.values["DIR_HOME"] = d
}
// Derived directories via Path() — single point of responsibility
i.values["DIR_DOWNLOADS"] = Path("Downloads")
i.values["DIR_CODE"] = Path("Code")
if d, err := os.UserConfigDir(); err == nil {
i.values["DIR_CONFIG"] = d
}
if d, err := os.UserCacheDir(); err == nil {
i.values["DIR_CACHE"] = d
}
i.values["DIR_TMP"] = os.TempDir()
if d, err := os.Getwd(); err == nil {
i.values["DIR_CWD"] = d
}
// Platform-specific data directory
switch runtime.GOOS {
case "darwin":
i.values["DIR_DATA"] = Path(Env("DIR_HOME"), "Library")
case "windows":
if d := os.Getenv("LOCALAPPDATA"); d != "" {
i.values["DIR_DATA"] = d
}
default:
if xdg := os.Getenv("XDG_DATA_HOME"); xdg != "" {
i.values["DIR_DATA"] = xdg
} else if Env("DIR_HOME") != "" {
i.values["DIR_DATA"] = Path(Env("DIR_HOME"), ".local", "share")
}
}
// Timestamps
i.values["CORE_START"] = time.Now().UTC().Format(time.RFC3339)
}
// Env returns a system information value by key.
// Core keys (OS, DIR_HOME, DS, etc.) are pre-populated at init.
// Unknown keys fall through to os.Getenv — making Env a universal
// replacement for os.Getenv.
//
// core.Env("OS") // "darwin" (pre-populated)
// core.Env("DIR_HOME") // "/Users/snider" (pre-populated)
// core.Env("FORGE_TOKEN") // falls through to os.Getenv
func Env(key string) string {
if v := systemInfo.values[key]; v != "" {
return v
}
return os.Getenv(key)
}
// EnvKeys returns all available environment keys.
//
// keys := core.EnvKeys()
func EnvKeys() []string {
keys := make([]string, 0, len(systemInfo.values))
for k := range systemInfo.values {
keys = append(keys, k)
}
return keys
}