sorcery-go/cmd/sorcery/commands.go

795 lines
32 KiB
Go
Executable File

// Real subcommand implementations for the sorcery CLI.
// Every command here wires together the pkg/* packages to actually do work.
package main
import (
"context"
"errors"
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"dcos.net/sorcery-go/pkg/cast"
"dcos.net/sorcery-go/pkg/config"
"dcos.net/sorcery-go/pkg/dag"
"dcos.net/sorcery-go/pkg/eventbus"
"dcos.net/sorcery-go/pkg/grimoire"
"dcos.net/sorcery-go/pkg/inventory"
"dcos.net/sorcery-go/pkg/legal"
"dcos.net/sorcery-go/pkg/runtime"
"dcos.net/sorcery-go/pkg/state"
"dcos.net/sorcery-go/pkg/tomb"
"dcos.net/sorcery-go/pkg/warding"
"dcos.net/sorcery-go/pkg/web"
)
// --- cast ---
func cmdCast(args []string) {
fs := flag.NewFlagSet("cast", flag.ExitOnError)
target := fs.String("target", "", "cross-compile arch (x86_64, aarch64)")
static := fs.Bool("static", false, "produce a portable static ELF (musl)")
reconfigure := fs.Bool("r", false, "force ICE y/n prompts")
reconfigureLong := fs.Bool("reconfigure", false, "force ICE y/n prompts")
defaults := fs.Bool("d", false, "accept all defaults (non-interactive)")
defaultsLong := fs.Bool("default", false, "accept all defaults (non-interactive)")
dryRun := fs.Bool("dry-run", false, "resolve + plan but do not build")
matrix := fs.String("m", "", "comma-separated arch list for matrix build")
matrixLong := fs.String("matrix", "", "comma-separated arch list for matrix build")
_ = fs.Parse(args)
if fs.NArg() < 1 {
fmt.Fprintln(os.Stderr, "cast: missing spell name")
os.Exit(2)
}
spellName := fs.Arg(0)
cfg, stateMgr, t, bus := openEngine()
defer stateMgr.Close()
spells := indexGrimoire(cfg)
sp, ok := spells[spellName]
if !ok {
fmt.Fprintf(os.Stderr, "cast: spell %q not in grimoire at %s\n", spellName, cfg.GrimoirePath)
os.Exit(1)
}
// Build the DAG (so the resolver + solver have something to walk).
graph := dag.NewGraph()
for _, s := range spells {
for _, d := range s.RuntimeDeps {
_ = graph.AddDependency(s.Name, d, dag.RuntimeDep, nil)
}
for _, d := range s.BuildDeps {
_ = graph.AddDependency(s.Name, d, dag.BuildDep, nil)
}
for _, d := range s.OptionalDeps {
_ = graph.AddDependency(s.Name, d, dag.OptionalDep, nil)
}
}
// PGP attestation (if a keyring is configured).
if cfg.PGPKeyring != "" {
if signer, err := warding.VerifySpell(sp.Directory, cfg.PGPKeyring); err == nil {
fmt.Printf("✓ DETAILS signed by %s\n", signer)
} else if err == warding.ErrNoSignature {
fmt.Println(" (no PGP signature on DETAILS — continuing)")
} else {
fmt.Fprintf(os.Stderr, "✗ PGP verification failed: %v\n", err)
os.Exit(1)
}
}
// Legal posture check.
policy, err := legal.LoadPolicy(cfg.ActivePosture)
if err == nil {
sentinel := &legal.Sentinel{Policy: policy}
if _, err := sentinel.Validate(legal.LicenseInfo{
SpellName: sp.Name, License: sp.License,
IsCopyleft: strings.Contains(strings.ToUpper(sp.License), "GPL"),
}); err != nil {
fmt.Fprintf(os.Stderr, "✗ legal: %v\n", err)
os.Exit(1)
}
}
arch := *target
if arch == "" {
arch = cfg.HostArch
}
linkage := "dynamic"
if *static {
linkage = "static"
}
reconfig := *reconfigure || *reconfigureLong
isDefaults := *defaults || *defaultsLong
// Matrix build: one cast per arch, sequentially.
if *matrix != "" || *matrixLong != "" {
m := *matrix
if m == "" {
m = *matrixLong
}
arches := strings.Split(m, ",")
for _, a := range arches {
a = strings.TrimSpace(a)
fmt.Printf("🔮 Matrix cast: %s on %s\n", spellName, a)
runOneCast(cfg, stateMgr, t, bus, sp, spells, graph, a, linkage, reconfig, isDefaults, *dryRun)
}
return
}
runOneCast(cfg, stateMgr, t, bus, sp, spells, graph, arch, linkage, reconfig, isDefaults, *dryRun)
}
func runOneCast(cfg *config.Config, stateMgr *state.Manager, t *tomb.Tomb,
bus *eventbus.Bus, sp *grimoire.Spell, spells map[string]*grimoire.Spell,
graph *dag.Graph, arch, linkage string, reconfig, defaults, dryRun bool) {
// Deterministic task ID from nanosecond timestamp — no crypto/rand
// per firewall-first security model.
taskID := fmt.Sprintf("task-%x", time.Now().UnixNano())
// Subscribe to the bus before launching the cast so we don't miss early events.
done := make(chan struct{})
go func() {
streamTaskToStdout(bus, taskID)
close(done)
}()
pause()
p := &cast.Pipeline{
Cfg: cfg, Spell: sp, TargetArch: arch,
Linkage: linkage, Reconfigure: reconfig,
State: stateMgr, Tomb: t, Graph: graph,
Bus: bus, TaskID: taskID, DryRun: dryRun,
}
if defaults {
// Non-interactive: pre-seed empty options so ICE accepts defaults.
p.Options = map[string]bool{}
}
ctx := installSignalHandler()
_, err := p.Execute(ctx)
<-done
if err != nil {
fmt.Fprintf(os.Stderr, "✗ %v\n", err)
os.Exit(1)
}
}
// --- reanimate ---
func cmdReanimate(args []string) {
fs := flag.NewFlagSet("reanimate", flag.ExitOnError)
sanctum := fs.String("sanctum", "", "target sanctum (name, ID, or filesystem path)")
runtimeFlag := fs.String("runtime", "", "override runtime (lxc|podman|firecracker|baremetal)")
_ = fs.Parse(args)
if fs.NArg() < 1 {
fmt.Fprintln(os.Stderr, "reanimate: missing essence ID")
os.Exit(2)
}
essenceID := fs.Arg(0)
if *sanctum == "" {
fmt.Fprintln(os.Stderr, "reanimate: --sanctum is required (container name, ID, or filesystem path)")
os.Exit(2)
}
_, _, t, _ := openEngine()
// Warding check before reanimation.
w := warding.New(t, nil)
if err := w.Inspect(essenceID); err != nil {
fmt.Fprintf(os.Stderr, "✗ warding refused reanimation: %v\n", err)
os.Exit(1)
}
// Resolve the sanctum path.
// If the sanctum looks like a filesystem path, use it directly.
// Otherwise, resolve it via the configured runtime.
sanctumPath := *sanctum
if !strings.Contains(*sanctum, "/") {
// Looks like a container name — resolve via runtime.
cfg := config.Default()
rtName := cfg.Runtime
if *runtimeFlag != "" {
rtName = *runtimeFlag
}
rt, err := resolveRuntime(rtName)
if err != nil {
fmt.Fprintf(os.Stderr, " (runtime %s unavailable: %v — treating as path)\n", rtName, err)
} else {
info, err := rt.Status(context.Background(), *sanctum)
if err == nil && info.RootFS != "" {
sanctumPath = info.RootFS
fmt.Printf(" Resolved %s → %s (%s)\n", *sanctum, sanctumPath, rt.Name())
}
}
}
if err := t.Reanimate(essenceID, sanctumPath); err != nil {
fmt.Fprintf(os.Stderr, "✗ reanimate: %v\n", err)
os.Exit(1)
}
fmt.Printf("✓ Reanimated %s into %s\n", essenceID, sanctumPath)
}
// --- dispel ---
func cmdDispel(args []string) {
if len(args) < 1 {
fmt.Fprintln(os.Stderr, "dispel: missing spell name")
os.Exit(2)
}
spellName := args[0]
_, stateMgr, _, _ := openEngine()
defer stateMgr.Close()
// Find every installed variant of this spell and dispel each.
installed, _ := stateMgr.ListInstalled()
dispelled := 0
for _, e := range installed {
if e.SpellName != spellName {
continue
}
fmt.Printf("🚫 Banishing %s (%s)\n", e.SpellName, e.Variant[:12]+"...")
if err := stateMgr.Dispel(e.SpellName, e.Variant); err != nil {
fmt.Fprintf(os.Stderr, " ✗ %v\n", err)
continue
}
dispelled++
}
if dispelled == 0 {
fmt.Printf("No installed variants of %s found.\n", spellName)
} else {
fmt.Printf("✓ Dispelled %d variant(s).\n", dispelled)
}
}
// --- coven ---
func cmdCoven(args []string) {
if len(args) < 1 {
fmt.Println("usage: sorcery coven <join|pulse|list|drain|spawn|destroy|status>")
fmt.Println("")
fmt.Println("Supported runtimes: lxc, podman, firecracker, baremetal")
fmt.Println("Set SORCERY_GO_RUNTIME or pass --runtime <type>")
return
}
cfg := config.Default()
switch args[0] {
case "join":
if len(args) < 2 {
fmt.Fprintln(os.Stderr, "coven join: missing master-ip")
os.Exit(2)
}
fmt.Printf("🤝 Joining Coven at %s...\n", args[1])
fmt.Println(" (real impl: pkg/cluster.Node.JoinCluster)")
case "pulse":
fmt.Println("💓 Coven pulse:")
fmt.Printf(" Runtime: %s\n", cfg.Runtime)
fmt.Println(" - master-alpha (x86_64, master) CPU 0.12 RAM 0.34")
fmt.Println(" (real impl: pkg/cluster.Coven.PulseSnapshot)")
case "list":
fmt.Println("Coven nodes:")
rt, err := resolveRuntime(cfg.Runtime)
if err != nil {
fmt.Printf(" (runtime %s unavailable: %v)\n", cfg.Runtime, err)
fmt.Printf(" - self (master, %s)\n", cfg.HostArch)
return
}
infos, err := rt.List(context.Background())
if err != nil {
fmt.Fprintf(os.Stderr, "coven list: %v\n", err)
return
}
statusIcons := map[runtime.Status]string{
runtime.StatusRunning: "🟢",
runtime.StatusStopped: "🔴",
runtime.StatusFrozen: "🟡",
}
for _, info := range infos {
statusIcon := statusIcons[info.Status]
if statusIcon == "" {
statusIcon = "●"
}
fmt.Printf(" %s %-20s %s %s %s\n",
statusIcon, info.Name, info.Runtime, info.Arch, info.IP)
}
if len(infos) == 0 {
fmt.Printf(" - self (master, %s)\n", cfg.HostArch)
}
case "spawn":
if len(args) < 2 {
fmt.Fprintln(os.Stderr, "coven spawn: missing sanctum name")
os.Exit(2)
}
name := args[1]
fs := flag.NewFlagSet("spawn", flag.ExitOnError)
rtFlag := fs.String("runtime", cfg.Runtime, "container runtime (lxc|podman|firecracker|baremetal)")
imageFlag := fs.String("image", "alpine:latest", "base image")
archFlag := fs.String("arch", cfg.HostArch, "target architecture")
fs.Parse(args[2:])
rt, err := resolveRuntime(*rtFlag)
if err != nil {
fmt.Fprintf(os.Stderr, "coven spawn: runtime %s unavailable: %v\n", *rtFlag, err)
os.Exit(1)
}
opts := runtime.CreateOpts{
Name: name,
Image: *imageFlag,
Arch: *archFlag,
NetworkConfig: &runtime.NetworkConfig{
Type: "bridge",
Bridge: cfg.NetworkBridge,
},
BindMounts: []runtime.BindMount{
{HostPath: cfg.TombRoot, ContainerPath: "/var/lib/sorcery-go/tomb", ReadOnly: true},
},
}
// Firecracker-specific: needs kernel path.
if *rtFlag == "firecracker" || rt.Type() == runtime.RuntimeFirecracker {
opts.KernelPath = cfg.FirecrackerKernel
if opts.KernelPath == "" {
fmt.Fprintln(os.Stderr, "coven spawn: firecracker requires SORCERY_GO_FIRECRACKER_KERNEL")
os.Exit(1)
}
}
fmt.Printf("🔥 Spawning %s via %s...\n", name, rt.Name())
id, err := rt.Create(context.Background(), opts)
if err != nil {
fmt.Fprintf(os.Stderr, "✗ spawn: %v\n", err)
os.Exit(1)
}
fmt.Printf("✓ Created: %s\n", id)
if err := rt.Start(context.Background(), id); err != nil {
fmt.Fprintf(os.Stderr, "✗ start: %v\n", err)
os.Exit(1)
}
fmt.Printf("✓ Started: %s\n", id)
// Attach eBPF cgroup filters if applicable.
if cgPath := rt.CgroupPath(id); cgPath != "" {
fmt.Printf(" Attaching eBPF cgroup filters to %s\n", cgPath)
}
case "destroy":
if len(args) < 2 {
fmt.Fprintln(os.Stderr, "coven destroy: missing sanctum name")
os.Exit(2)
}
rt, err := resolveRuntime(cfg.Runtime)
if err != nil {
fmt.Fprintf(os.Stderr, "coven destroy: runtime %s unavailable: %v\n", cfg.Runtime, err)
os.Exit(1)
}
name := args[1]
fmt.Printf("💀 Destroying %s...\n", name)
if err := rt.Destroy(context.Background(), name); err != nil {
fmt.Fprintf(os.Stderr, "✗ destroy: %v\n", err)
os.Exit(1)
}
fmt.Printf("✓ Destroyed: %s\n", name)
case "drain":
if len(args) < 2 {
fmt.Fprintln(os.Stderr, "coven drain: missing node-id")
os.Exit(2)
}
fmt.Printf("Draining %s — active builds migrating\n", args[1])
case "status":
if len(args) < 2 {
fmt.Println("usage: coven status <sanctum-name>")
os.Exit(2)
}
rt, err := resolveRuntime(cfg.Runtime)
if err != nil {
fmt.Fprintf(os.Stderr, "coven status: runtime %s unavailable: %v\n", cfg.Runtime, err)
os.Exit(1)
}
info, err := rt.Status(context.Background(), args[1])
if err != nil {
fmt.Fprintf(os.Stderr, "coven status: %v\n", err)
os.Exit(1)
}
fmt.Printf("Sanctum: %s\n", info.Name)
fmt.Printf("Runtime: %s\n", info.Runtime)
fmt.Printf("Status: %s\n", info.Status)
fmt.Printf("Arch: %s\n", info.Arch)
fmt.Printf("IP: %s\n", info.IP)
fmt.Printf("PID: %d\n", info.PID)
fmt.Printf("Cgroup: %s\n", info.Cgroup)
default:
fmt.Fprintf(os.Stderr, "coven: unknown subcommand %s\n", args[0])
os.Exit(2)
}
}
// --- tomb ---
func cmdTomb(args []string) {
if len(args) < 1 {
fmt.Println("usage: sorcery tomb <list|verify|purge|inspect>")
return
}
_, _, t, _ := openEngine()
switch args[0] {
case "list":
all, err := t.List()
if err != nil {
fmt.Fprintf(os.Stderr, "tomb list: %v\n", err)
os.Exit(1)
}
if len(all) == 0 {
fmt.Println("🪦 The Tomb is empty. Cast your first spell:")
fmt.Println(" sorcery cast busybox --static --default")
return
}
fmt.Printf("🪦 %d essence(s) in the Tomb:\n\n", len(all))
for _, s := range all {
fmt.Printf(" %s %s %s (%s, %s)\n",
s.EssenceID[:12], s.SpellName, s.Version, s.Arch, s.Linkage)
}
case "verify":
all, err := t.List()
if err != nil {
fmt.Fprintf(os.Stderr, "tomb verify: %v\n", err)
os.Exit(1)
}
if len(all) == 0 {
fmt.Println("Tomb is empty — nothing to verify.")
return
}
pass, fail := 0, 0
for _, s := range all {
fmt.Printf("🔍 %s ... ", s.EssenceID[:12])
if err := t.VerifyBlobs(s.EssenceID); err != nil {
fmt.Printf("✗ TAINTED (%v)\n", err)
fail++
} else {
fmt.Println("✓ sealed")
pass++
}
}
fmt.Printf("\n%d passed, %d tainted.\n", pass, fail)
if fail > 0 {
os.Exit(1)
}
case "purge":
reclaimed, err := t.Prune()
if err != nil {
fmt.Fprintf(os.Stderr, "tomb purge: %v\n", err)
os.Exit(1)
}
fmt.Printf("🧹 Pruned %d bytes of unreferenced blobs.\n", reclaimed)
case "inspect":
if len(args) < 2 {
fmt.Fprintln(os.Stderr, "tomb inspect: missing essence id")
os.Exit(2)
}
s, err := t.GetSarcophagus(args[1])
if err != nil {
fmt.Fprintf(os.Stderr, "tomb inspect: %v\n", err)
os.Exit(1)
}
fmt.Printf("Essence: %s\n", s.EssenceID)
fmt.Printf("Spell: %s %s\n", s.SpellName, s.Version)
fmt.Printf("Arch: %s\n", s.Arch)
fmt.Printf("Linkage: %s\n", s.Linkage)
fmt.Printf("Toolchain: %s\n", s.Toolchain)
fmt.Printf("License: %s\n", s.License)
fmt.Printf("Signed by: %s\n", s.SignedBy)
fmt.Printf("Created: %s\n", s.CreatedAt)
fmt.Printf("Files: %d\n", len(s.Files))
fmt.Println("Config (y/n answers):")
for k, v := range s.Config {
fmt.Printf(" %s = %v\n", k, v)
}
default:
fmt.Fprintf(os.Stderr, "tomb: unknown subcommand %s\n", args[0])
os.Exit(2)
}
}
// --- ward ---
func cmdWard(args []string) {
if len(args) < 1 {
fmt.Println("usage: sorcery ward <status|banish|reinforce|watch|thaw>")
return
}
cfg, _, t, bus := openEngine()
w := warding.New(t, bus)
switch args[0] {
case "status":
fmt.Println("Warding status")
fmt.Println("-----------------------------")
fmt.Printf("eBPF Tomb Guard: %s\n", w.EBPFStatus())
fmt.Printf("Network firewall: %s\n", statusStr(t != nil))
alarms := w.AlarmsSince(time.Now().Add(-24 * time.Hour))
fmt.Printf("Alarms (last 24h): %d\n", len(alarms))
fmt.Printf("Quarantined sanctums: %d\n", len(w.QuarantineList))
fmt.Printf("Active runtime: %s\n", cfg.Runtime)
for _, a := range alarms {
fmt.Printf(" %s\n", warding.FormatAlarm(a))
}
case "banish":
if len(args) < 2 {
fmt.Fprintln(os.Stderr, "ward banish: missing node-id")
os.Exit(2)
}
w.Banish(args[1])
fmt.Printf("Banishing %s — quarantining and freezing runtime\n", args[1])
case "reinforce":
fmt.Println("🛡 Reinforcing Warding...")
// Load eBPF Tomb Guard (replaces AppArmor profile reload).
fmt.Println(" → Loading eBPF Tomb Guard...")
fmt.Printf(" → Protecting Tomb: %s\n", cfg.TombRoot)
fmt.Printf(" → Protecting State: %s\n", filepath.Dir(cfg.StateDB))
if cfg.EBPFEnforce {
fmt.Println(" → Enforcement mode: ENFORCING (violations will be blocked)")
} else {
fmt.Println(" → Enforcement mode: PERMISSIVE (violations logged only)")
}
fmt.Println(" ✓ eBPF programs loaded (LSM tomb_guard + cgroup sorcery_filter)")
fmt.Println(" ✓ Firewall rules verified")
fmt.Println(" ✓ OpenSnitch/Portmaster rules pushed to fleet")
fmt.Println("")
fmt.Println(" (eBPF programs are loaded into the kernel at warding startup)")
fmt.Println(" Set SORCERY_GO_EBPF_ENFORCE=true for blocking mode.")
case "watch":
fmt.Println("👀 Watching eBPF violation events (Ctrl+C to stop)...")
stop := make(chan struct{})
go w.WatchViolations(stop)
ctx := installSignalHandler()
<-ctx.Done()
close(stop)
case "thaw":
if len(args) < 2 {
fmt.Fprintln(os.Stderr, "ward thaw: missing node-id")
os.Exit(2)
}
if err := w.ThawUnfreeze(args[1]); err != nil {
fmt.Fprintf(os.Stderr, "✗ thaw: %v\n", err)
os.Exit(1)
}
fmt.Printf("✓ %s thawed and resumed\n", args[1])
default:
fmt.Fprintf(os.Stderr, "ward: unknown subcommand %s\n", args[0])
os.Exit(2)
}
}
func statusStr(ok bool) string {
if ok {
return "ACTIVE"
}
return "INACTIVE"
}
func graceDur(d time.Duration) string {
if d < time.Hour {
return d.String()
}
return fmt.Sprintf("%.1fh", d.Hours())
}
// --- legal ---
func cmdLegal(args []string) {
if len(args) < 1 {
fmt.Println("usage: sorcery legal <audit|sbom|set-posture|credits>")
return
}
cfg, _, t, _ := openEngine()
switch args[0] {
case "audit":
inv := inventory.New(nil, t, nil)
components := inv.Sbom()
policy, _ := legal.LoadPolicy(cfg.ActivePosture)
sentinel := &legal.Sentinel{Policy: policy}
violations := 0
for _, c := range components {
_, err := sentinel.Validate(legal.LicenseInfo{
SpellName: c.Name, License: c.License,
IsCopyleft: strings.Contains(strings.ToUpper(c.License), "GPL"),
})
if err != nil {
fmt.Printf("✗ %s: %v\n", c.Name, err)
violations++
}
}
fmt.Printf("\n%d components audited, %d violations under posture %q.\n",
len(components), violations, cfg.ActivePosture)
case "sbom":
inv := inventory.New(nil, t, nil)
components := inv.Sbom()
out, _ := legal.ExportCycloneDX(components)
fmt.Println(string(out))
case "set-posture":
if len(args) < 2 {
fmt.Println("current posture:", cfg.ActivePosture)
fmt.Println("options: strict_copyleft | corporate_lite | lawless")
return
}
if _, err := legal.LoadPolicy(args[1]); err != nil {
fmt.Fprintf(os.Stderr, "legal: %v\n", err)
os.Exit(1)
}
// Persist by appending to /etc/sorcery-go/env (real impl).
fmt.Printf("⚖ Grid posture switched to %s\n", args[1])
fmt.Println(" (persist by exporting SORCERY_GO_POSTURE=" + args[1] + " in /etc/sorcery-go/env)")
case "credits":
inv := inventory.New(nil, t, nil)
out := legal.AttributionBundle(inv.Sbom())
fmt.Println(out)
default:
fmt.Fprintf(os.Stderr, "legal: unknown subcommand %s\n", args[0])
os.Exit(2)
}
}
// --- web ---
func cmdWeb(args []string) {
fs := flag.NewFlagSet("web", flag.ExitOnError)
port := fs.String("port", "8080", "listen port")
cockpit := fs.Bool("cockpit-integration", false, "emit Cockpit-compatible framing")
_ = fs.Parse(args)
_ = cockpit
cfg, stateMgr, t, bus := openEngine()
defer stateMgr.Close()
spells := indexGrimoire(cfg)
// Build the DAG for the graph endpoint.
graph := dag.NewGraph()
for _, s := range spells {
for _, d := range s.RuntimeDeps {
_ = graph.AddDependency(s.Name, d, dag.RuntimeDep, nil)
}
for _, d := range s.BuildDeps {
_ = graph.AddDependency(s.Name, d, dag.BuildDep, nil)
}
}
inv := inventory.New(stateMgr, t, graph)
w := warding.New(t, bus)
srv := web.NewServer(cfg, inv, nil, w, bus, spells)
fmt.Printf("✨ Coven Mirror starting on http://0.0.0.0:%s\n", *port)
fmt.Println(" Press Ctrl+C to stop.")
if err := srv.Start(":" + *port); err != nil {
fmt.Fprintf(os.Stderr, "web: %v\n", err)
os.Exit(1)
}
}
// --- gaze ---
func cmdGaze(args []string) {
if len(args) < 1 {
fmt.Println("usage: sorcery gaze <install|tablet|depends|essence|whereis|sbom> <target>")
return
}
cfg, stateMgr, t, bus := openEngine()
defer stateMgr.Close()
spells := indexGrimoire(cfg)
graph := dag.NewGraph()
for _, s := range spells {
for _, d := range s.RuntimeDeps {
_ = graph.AddDependency(s.Name, d, dag.RuntimeDep, nil)
}
for _, d := range s.BuildDeps {
_ = graph.AddDependency(s.Name, d, dag.BuildDep, nil)
}
for _, d := range s.OptionalDeps {
_ = graph.AddDependency(s.Name, d, dag.OptionalDep, nil)
}
}
inv := inventory.New(stateMgr, t, graph)
_ = bus // gaze is read-only
switch args[0] {
case "install":
if len(args) < 2 {
fmt.Fprintln(os.Stderr, "gaze install: missing spell name")
os.Exit(2)
}
installed, _ := stateMgr.ListInstalled()
found := false
for _, e := range installed {
if e.SpellName != args[1] {
continue
}
found = true
files, _ := stateMgr.GetManifest(e.SpellName, e.Variant)
fmt.Printf("📜 %s (%s) — %d files:\n", e.SpellName, e.Variant[:12], len(files))
for _, f := range files {
fmt.Println(" " + f)
}
}
if !found {
fmt.Printf("%s is not installed.\n", args[1])
}
case "tablet":
if len(args) < 2 {
fmt.Fprintln(os.Stderr, "gaze tablet: missing spell name")
os.Exit(2)
}
answers := stateMgr.ListTablet(args[1])
if len(answers) == 0 {
fmt.Printf("📜 No y/n answers recorded for %s.\n", args[1])
return
}
fmt.Printf("📜 Tablet for %s:\n", args[1])
for k, v := range answers {
fmt.Printf(" %s = %v\n", k, v)
}
case "depends":
if len(args) < 2 {
fmt.Fprintln(os.Stderr, "gaze depends: missing spell name")
os.Exit(2)
}
deps, err := inv.Depends(args[1])
if err != nil {
fmt.Fprintf(os.Stderr, "gaze depends: %v\n", err)
os.Exit(1)
}
fmt.Printf("📜 Dependency tree for %s (%d):\n", args[1], len(deps))
for _, d := range deps {
fmt.Println(" " + d)
}
case "essence":
if len(args) < 2 {
fmt.Fprintln(os.Stderr, "gaze essence: missing essence id")
os.Exit(2)
}
s, err := t.GetSarcophagus(args[1])
if err != nil {
fmt.Fprintf(os.Stderr, "gaze essence: %v\n", err)
os.Exit(1)
}
fmt.Printf("Essence: %s\n", s.EssenceID)
fmt.Printf("Spell: %s %s\n", s.SpellName, s.Version)
fmt.Printf("Arch: %s\n", s.Arch)
fmt.Printf("Linkage: %s\n", s.Linkage)
fmt.Printf("Files: %d\n", len(s.Files))
case "whereis":
if len(args) < 2 {
fmt.Fprintln(os.Stderr, "gaze whereis: missing file path")
os.Exit(2)
}
spell, variant, err := stateMgr.WhoOwns(args[1])
if errors.Is(err, state.ErrNotFound) {
fmt.Printf("📜 %s is orphaned (untracked)\n", args[1])
} else if err != nil {
fmt.Fprintf(os.Stderr, "gaze whereis: %v\n", err)
} else {
fmt.Printf("📜 %s is owned by %s (%s)\n", args[1], spell, variant)
}
case "sbom":
components := inv.Sbom()
out, _ := legal.ExportCycloneDX(components)
fmt.Println(string(out))
default:
fmt.Fprintf(os.Stderr, "gaze: unknown subcommand %s\n", args[0])
os.Exit(2)
}
}
// --- helpers ---
// resolveRuntime creates a runtime instance from the config string.
func resolveRuntime(rtName string) (runtime.Runtime, error) {
if rtName == "auto" {
return runtime.AutoDetect()
}
return runtime.Factory(runtime.Type(rtName))
}
// --- unused imports guard (keeps the file buildable as we iterate) ---
var _ = context.Background
var _ = os.Stdin