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.
247 lines
7.1 KiB
Go
247 lines
7.1 KiB
Go
// phrocs is a Insights-branded dev process runner built with Bubble Tea.
|
|
//
|
|
// It is a drop-in replacement for mprocs: it reads the same YAML config
|
|
// that `insightscli dev:generate` produces and renders a customisable TUI with a
|
|
// per-process sidebar, scrollable output panes, and insightscli-aware keybindings.
|
|
//
|
|
// Usage:
|
|
//
|
|
// phrocs [--debug] [--config <config.yaml>] # interactive TUI
|
|
// phrocs -d | --detach [--config <config.yaml>] # detached mode (no TUI)
|
|
// phrocs wait [--timeout N] [--json] # block on readiness
|
|
// phrocs stop [--timeout N] # stop detached process via IPC
|
|
// phrocs attach # polling TUI against a detached process
|
|
// phrocs --version
|
|
//
|
|
// Flags:
|
|
//
|
|
// --config Path to the YAML config file (default to ./mprocs.yaml if it exists)
|
|
// --debug Write a debug log to /tmp/phrocs-debug.log (key inputs, proc
|
|
// selection changes, status transitions, etc.)
|
|
// --version Print version information and exit
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
|
|
tea "charm.land/bubbletea/v2"
|
|
"github.com/charmbracelet/x/term"
|
|
"github.com/insights/insights/phrocs/internal/config"
|
|
"github.com/insights/insights/phrocs/internal/ipc"
|
|
"github.com/insights/insights/phrocs/internal/process"
|
|
"github.com/insights/insights/phrocs/internal/tui"
|
|
)
|
|
|
|
var (
|
|
Version = "dev"
|
|
Commit = "unknown"
|
|
BuildDate = "unknown"
|
|
)
|
|
|
|
// detachedChildEnv is set on the re-exec'd child so it knows to skip the fork
|
|
// and run the detached main loop instead.
|
|
const detachedChildEnv = "PHROCS_DETACHED_CHILD"
|
|
|
|
// parsedArgs captures flags common to every mode. `subcommand` is the first
|
|
// non-flag token if it matches a known name; empty means "interactive or detached
|
|
// based on flag presence".
|
|
type parsedArgs struct {
|
|
subcommand string
|
|
configPath string
|
|
debug bool
|
|
detach bool
|
|
timeout int
|
|
asJSON bool
|
|
}
|
|
|
|
func printUsage() {
|
|
fmt.Println(`phrocs — Insights dev process runner
|
|
|
|
Usage:
|
|
phrocs [--config PATH] [--debug] interactive TUI (default)
|
|
phrocs -d | --detach [--config PATH] detached mode (no TUI)
|
|
phrocs wait [--timeout N] [--json] block until ready, crashed, or timeout
|
|
phrocs stop [--timeout N] stop the detached process gracefully
|
|
phrocs attach polling status client against a detached process
|
|
phrocs --version`)
|
|
}
|
|
|
|
func parseArgs(args []string) (parsedArgs, error) {
|
|
pa := parsedArgs{timeout: 300}
|
|
i := 0
|
|
// First pass: if the first arg is a known subcommand, consume it.
|
|
if len(args) > 0 {
|
|
switch args[0] {
|
|
case "wait", "stop", "attach", "detach":
|
|
pa.subcommand = args[0]
|
|
i = 1
|
|
}
|
|
}
|
|
for ; i < len(args); i++ {
|
|
switch args[i] {
|
|
case "--version":
|
|
fmt.Printf("phrocs %s (%s, %s)\n", Version, Commit, BuildDate)
|
|
os.Exit(0)
|
|
case "-h", "--help":
|
|
printUsage()
|
|
os.Exit(0)
|
|
case "--config":
|
|
if i+1 >= len(args) {
|
|
return pa, fmt.Errorf("--config requires a value")
|
|
}
|
|
pa.configPath = args[i+1]
|
|
i++
|
|
case "--debug":
|
|
pa.debug = true
|
|
case "-d", "--detach":
|
|
pa.detach = true
|
|
case "--timeout":
|
|
if i+1 >= len(args) {
|
|
return pa, fmt.Errorf("--timeout requires a value")
|
|
}
|
|
var n int
|
|
if _, err := fmt.Sscanf(args[i+1], "%d", &n); err != nil {
|
|
return pa, fmt.Errorf("--timeout: %v", err)
|
|
}
|
|
pa.timeout = n
|
|
i++
|
|
case "--json":
|
|
pa.asJSON = true
|
|
default:
|
|
return pa, fmt.Errorf("unknown flag: %s", args[i])
|
|
}
|
|
}
|
|
return pa, nil
|
|
}
|
|
|
|
func main() {
|
|
pa, err := parseArgs(os.Args[1:])
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "phrocs: %v\n", err)
|
|
os.Exit(2)
|
|
}
|
|
|
|
switch pa.subcommand {
|
|
case "wait":
|
|
os.Exit(runWait(pa.timeout, pa.asJSON))
|
|
case "stop":
|
|
os.Exit(runStop(pa.timeout))
|
|
case "attach":
|
|
os.Exit(runAttach())
|
|
case "detach":
|
|
// `phrocs detach` is an alias for `phrocs -d`
|
|
pa.detach = true
|
|
}
|
|
|
|
// Detached mode: either the user passed -d explicitly, or the re-exec'd
|
|
// child landed here with PHROCS_DETACHED_CHILD set.
|
|
if pa.detach || os.Getenv(detachedChildEnv) == "1" {
|
|
os.Exit(runDetached(pa.configPath))
|
|
}
|
|
|
|
os.Exit(runInteractive(pa.configPath, pa.debug))
|
|
}
|
|
|
|
// runInteractive is the original TUI entry point — kept byte-for-byte
|
|
// compatible with pre-subcommand phrocs.
|
|
func runInteractive(configPath string, debug bool) int {
|
|
var logger *log.Logger
|
|
if debug {
|
|
f, err := os.OpenFile("/tmp/phrocs-debug.log", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "phrocs: open debug log: %v\n", err)
|
|
return 1
|
|
}
|
|
logger = log.New(f, "", log.LstdFlags|log.Lmicroseconds)
|
|
logger.Println("debug logging started")
|
|
}
|
|
|
|
resolved, err := config.ResolveConfigPath(configPath)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "phrocs: %v\n", err)
|
|
return 1
|
|
}
|
|
|
|
cfg, err := config.Load(resolved)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "phrocs: load config: %v\n", err)
|
|
return 1
|
|
}
|
|
|
|
mgr := process.NewManager(cfg)
|
|
m := tui.New(mgr, cfg, resolved, logger)
|
|
|
|
// If stdout isn't a TTY (e.g. Zed task runner, wrapped launches), open
|
|
// /dev/tty directly so Bubble Tea can query terminal size and render.
|
|
var opts []tea.ProgramOption
|
|
if !term.IsTerminal(os.Stdout.Fd()) {
|
|
ttyIn, ttyOut, err := tea.OpenTTY()
|
|
if err == nil {
|
|
opts = append(opts, tea.WithInput(ttyIn), tea.WithOutput(ttyOut))
|
|
defer func() {
|
|
_ = ttyIn.Close()
|
|
_ = ttyOut.Close()
|
|
}()
|
|
}
|
|
}
|
|
p := tea.NewProgram(m, opts...)
|
|
|
|
// Catch SIGTERM/SIGHUP so child processes are cleaned up even if phrocs
|
|
// is killed externally (e.g. by the OS or another process manager).
|
|
// Bubble Tea handles SIGINT (Ctrl+C) via the TUI's quit handler.
|
|
sigCh := make(chan os.Signal, 1)
|
|
signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGHUP)
|
|
go func() {
|
|
<-sigCh
|
|
mgr.StopAll()
|
|
p.Kill()
|
|
}()
|
|
|
|
// StartAll is launched in a goroutine so it doesn't block: p.Send() inside
|
|
// Start() will block briefly on the Bubble Tea channel until p.Run() starts
|
|
// its event loop, at which point everything unblocks naturally. Calling
|
|
// StartAll synchronously before p.Run() deadlocks because the main goroutine
|
|
// would be stuck in p.Send() and p.Run() would never be reached.
|
|
mgr.SetSend(p.Send)
|
|
go mgr.StartAll()
|
|
|
|
wd, err := os.Getwd()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "phrocs: getwd: %v\n", err)
|
|
return 1
|
|
}
|
|
socketPath := ipc.SocketPathFor(wd)
|
|
|
|
ln, err := ipc.Listen(socketPath)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "phrocs: ipc: %v\n", err)
|
|
return 1
|
|
}
|
|
ownerInode := ipc.SocketInode(socketPath)
|
|
defer func() {
|
|
_ = ln.Close()
|
|
ipc.RemoveOwnedSocket(socketPath, ownerInode)
|
|
}()
|
|
go func() {
|
|
// Accept returns an error when the listener is closed on exit; ignore it.
|
|
_ = ipc.Serve(ln, mgr)
|
|
}()
|
|
|
|
if _, err := p.Run(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "phrocs: %v\n", err)
|
|
return 1
|
|
}
|
|
// Quitting phrocs stops its supervised processes, but docker compose runs
|
|
// detached (`up -d`), so containers keep running by design: the compose
|
|
// stack is shared across worktrees. Say so, since nothing else does.
|
|
// (printDockerTeardownHint itself stays quiet in sandboxes.)
|
|
if _, ok := cfg.Procs["docker-compose"]; ok {
|
|
printDockerTeardownHint()
|
|
}
|
|
return 0
|
|
}
|