mirror of
https://github.com/hanzoai/insights.git
synced 2026-10-30 16:15:31 +00:00
The tree was a debranded snapshot of upstream taken around 2026-02-20, and upstream has moved about five months since. This replaces it with upstream master, mechanically re-derived: bin/debrand does the rename, and the pipeline and squashed migrations are restored from the shipped tree. Not a merge. The previous import replaced the tree wholesale in a single commit, so there is no shared content history to merge against. Reproducing the rename on the new tree is what actually converges. What survives: .hanzo/workflows, the Dockerfiles and bin/docker* entrypoints, compose files, LLM.md, and insights/migrations. The squashed initial migration is load-bearing — the live database records it by name, so upstream's ~918 individual migrations cannot come back. Nine products are dropped as unreferenced. Twenty-one more that we do not run are kept anyway because the core app still imports them; unpicking those is a refactor and does not belong in a rebase. This base does not build yet, and the reasons are not in the rename: upstream now wants Python 3.13.13 against our 3.12.12 and Django 5.2 against our 4.2, and the dependency manifests still name upstream packages that we republish under our own names. Those are decisions, not renames.
226 lines
7.0 KiB
Go
226 lines
7.0 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// CapabilityGroupKey is the reserved key under ProcConfig.Groups that carries
|
|
// the capability dimension inferred from ProcConfig.Capability
|
|
const CapabilityGroupKey = "capability"
|
|
|
|
// Mirrors a single entry under mprocs.yaml's "procs" key
|
|
type ProcConfig struct {
|
|
Shell string `yaml:"shell"`
|
|
Capability string `yaml:"capability"`
|
|
Cmd []string `yaml:"cmd"`
|
|
Cwd string `yaml:"cwd"`
|
|
Autostart *bool `yaml:"autostart"`
|
|
Autorestart bool `yaml:"autorestart"`
|
|
Stop string `yaml:"stop"` // "SIGINT", "SIGTERM", "SIGKILL", or "hard-kill"
|
|
AskSkip bool `yaml:"ask_skip"`
|
|
Env map[string]string `yaml:"env"`
|
|
ReadyPattern string `yaml:"ready_pattern"`
|
|
Groups map[string]string `yaml:"groups"` // user-defined grouping dimensions, using map here so new dimensions need no code changes
|
|
}
|
|
|
|
// Reports whether the process should start automatically
|
|
// Defaults to true when the field is absent from the YAML
|
|
func (p ProcConfig) ShouldAutostart() bool {
|
|
return p.Autostart == nil || *p.Autostart
|
|
}
|
|
|
|
// Top-level mprocs.yaml document
|
|
type Config struct {
|
|
Shell string `yaml:"shell"`
|
|
Procs map[string]ProcConfig `yaml:"procs"`
|
|
GroupOrder map[string][]string `yaml:"group_order"` // display order per dimension
|
|
DefaultGroup string `yaml:"default_group"` // grouping dimension active on startup ("" = ungrouped)
|
|
HideKeymapWindow bool `yaml:"hide_keymap_window"`
|
|
MouseScrollSpeed int `yaml:"mouse_scroll_speed"`
|
|
ProcListWidth int `yaml:"proc_list_width"`
|
|
Scrollback int `yaml:"scrollback"`
|
|
}
|
|
|
|
// ResolveConfigPath returns the config file path to use. If explicit is
|
|
// non-empty it is returned as-is. Otherwise, the function checks for an
|
|
// mprocs.yaml in the current directory and returns its path, or an error
|
|
// if no config can be found.
|
|
func ResolveConfigPath(explicit string) (string, error) {
|
|
if explicit != "" {
|
|
return explicit, nil
|
|
}
|
|
info, err := os.Stat("mprocs.yaml")
|
|
if err == nil && info.Mode().IsRegular() {
|
|
return "mprocs.yaml", nil
|
|
}
|
|
if err == nil {
|
|
return "", fmt.Errorf("mprocs.yaml exists but is not a regular file")
|
|
}
|
|
if os.IsNotExist(err) {
|
|
return "", fmt.Errorf("no config: pass --config or place an mprocs.yaml in the current directory")
|
|
}
|
|
return "", fmt.Errorf("stat mprocs.yaml: %w", err)
|
|
}
|
|
|
|
// Reads and parses an mprocs-compatible YAML config file
|
|
func Load(path string) (*Config, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var cfg Config
|
|
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
|
return nil, err
|
|
}
|
|
if cfg.Scrollback == 0 {
|
|
cfg.Scrollback = 10_000
|
|
}
|
|
if cfg.MouseScrollSpeed == 0 {
|
|
cfg.MouseScrollSpeed = 3
|
|
}
|
|
inferGroupFromCapability(&cfg)
|
|
return &cfg, nil
|
|
}
|
|
|
|
// inferGroupFromCapability copies each proc's "capability:" field into
|
|
// "groups:" map, so the TUI's grouping dimension cycle ("g" key) picks
|
|
// it up alongside user-declared dimensions.
|
|
// An explicit Groups[CapabilityGroupKey] in YAML takes precedence.
|
|
func inferGroupFromCapability(cfg *Config) {
|
|
for name, pc := range cfg.Procs {
|
|
if pc.Capability == "" {
|
|
// Procs without a capability fall under "Ungrouped"
|
|
continue
|
|
}
|
|
if _, ok := pc.Groups[CapabilityGroupKey]; ok {
|
|
// Any explicit entry is respected, setting to "" leads to "Ungrouped"
|
|
continue
|
|
}
|
|
if pc.Groups == nil {
|
|
pc.Groups = make(map[string]string)
|
|
}
|
|
pc.Groups[CapabilityGroupKey] = pc.Capability
|
|
cfg.Procs[name] = pc
|
|
}
|
|
}
|
|
|
|
// Intent is a minimal representation of an intent from intent-map.yaml.
|
|
type Intent struct {
|
|
Name string
|
|
Description string
|
|
}
|
|
|
|
// IntentMapConfig holds just enough of intent-map.yaml to display intents in the TUI.
|
|
type IntentMapConfig struct {
|
|
Intents []Intent
|
|
}
|
|
|
|
// LoadIntentMap finds and parses devenv/intent-map.yaml by walking up from
|
|
// the working directory. Returns the parsed intents sorted by name.
|
|
func LoadIntentMap() (*IntentMapConfig, error) {
|
|
path, err := findIntentMapPath()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var raw struct {
|
|
Intents map[string]struct {
|
|
Description string `yaml:"description"`
|
|
} `yaml:"intents"`
|
|
}
|
|
if err := yaml.Unmarshal(data, &raw); err != nil {
|
|
return nil, err
|
|
}
|
|
if raw.Intents == nil {
|
|
return nil, fmt.Errorf("intent-map missing 'intents' mapping")
|
|
}
|
|
intents := make([]Intent, 0, len(raw.Intents))
|
|
for name, fields := range raw.Intents {
|
|
intents = append(intents, Intent{Name: name, Description: fields.Description})
|
|
}
|
|
sort.Slice(intents, func(i, j int) bool { return intents[i].Name < intents[j].Name })
|
|
return &IntentMapConfig{Intents: intents}, nil
|
|
}
|
|
|
|
// findFileUpward walks up from the working directory looking for relPath.
|
|
// Returns the absolute path if found, or an error if the filesystem root
|
|
// is reached without a match.
|
|
func findFileUpward(relPath string) (string, error) {
|
|
dir, err := os.Getwd()
|
|
if err != nil {
|
|
return "", fmt.Errorf("get working directory: %w", err)
|
|
}
|
|
for {
|
|
candidate := filepath.Join(dir, relPath)
|
|
if _, err := os.Stat(candidate); err == nil {
|
|
return candidate, nil
|
|
}
|
|
parent := filepath.Dir(dir)
|
|
if parent == dir {
|
|
return "", fmt.Errorf("%s not found", relPath)
|
|
}
|
|
dir = parent
|
|
}
|
|
}
|
|
|
|
func findIntentMapPath() (string, error) {
|
|
return findFileUpward(filepath.Join("devenv", "intent-map.yaml"))
|
|
}
|
|
|
|
// InsightsConfig represents the _insights section embedded in generated mprocs configs.
|
|
type InsightsConfig struct {
|
|
Intents []string `yaml:"intents"`
|
|
ExcludeUnits []string `yaml:"exclude_units"`
|
|
}
|
|
|
|
// LoadInsightsConfig reads a generated mprocs.yaml and extracts the _insights section.
|
|
// Returns nil (no error) if the section is absent.
|
|
func LoadInsightsConfig(configPath string) (*InsightsConfig, error) {
|
|
data, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var wrapper struct {
|
|
Insights *InsightsConfig `yaml:"_insights"`
|
|
}
|
|
if err := yaml.Unmarshal(data, &wrapper); err != nil {
|
|
return nil, err
|
|
}
|
|
return wrapper.Insights, nil
|
|
}
|
|
|
|
// LoadRegistry finds and parses the full process registry (bin/mprocs.yaml)
|
|
// by walking up from the working directory. Returns nil (no error) if the
|
|
// registry cannot be found.
|
|
func LoadRegistry() (*Config, error) {
|
|
path, err := findFileUpward(filepath.Join("bin", "mprocs.yaml"))
|
|
if err != nil {
|
|
return nil, nil
|
|
}
|
|
return Load(path)
|
|
}
|
|
|
|
// OrderedNames returns process names in a stable, predictable order.
|
|
// "info" is always first (if present), then remaining names sorted alphabetically.
|
|
func (c *Config) OrderedNames() []string {
|
|
names := make([]string, 0, len(c.Procs))
|
|
for name := range c.Procs {
|
|
if name != "info" {
|
|
names = append(names, name)
|
|
}
|
|
}
|
|
sort.Strings(names)
|
|
if _, ok := c.Procs["info"]; ok {
|
|
names = append([]string{"info"}, names...)
|
|
}
|
|
return names
|
|
}
|