Files
loom-cli/main.go
T
jeffryandClaude Opus 5 6a5966ba99 quince: reconcile through the tool, write the facets, and adopt gitea.md
The whole loop ran on a real change with a cart open for the first time: check
staged externals.md as a polad, apply moved it and its lock, and the new
confidentiality rule arrived through the tool rather than through somebody saying
so.

Answers loom's completeness case. A 200 says the document moved and nothing about
whether the casting still covers it — but the mechanism exists and we had not
built the thing it needs. check prints the document's .usages.md when it stages a
polad, and for an agreement that is the file the roles are cast in. It failed here
because externals.md had no facet at all. So both are written: externals.usages.md
naming which Go file implements which rule, and a v1 section on cart.usages.md
recording the casting, that we got it wrong before it was written down, and that
osprey and marmalade stay in history unrewritten.

Implements the adopted confidentiality rule as far as it can be implemented. add
warns when a fetch needed a credential, and says plainly that it cannot see who may
read the repository the copy lands in. Reference-only adoption is recorded as not
yet implemented rather than as a gap, because the gap is ours.

Adopts gitea.md, now that homelab-cluster is public — the document that cost three
tool calls and a guess this morning, with a facet recording that its :2222 fact is
a fact for people and not for the tool, whose every transport is HTTPS on 443.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018UTxuSizozEA8yDitPuris
2026-09-07 15:26:27 -04:00

123 lines
3.8 KiB
Go

// loomctl fetches documents this repository depends on, and finds out when they
// change.
//
// It reports and never repairs. Everything it writes, it writes to the working
// tree — committing and pushing are yours, because the consequences of a push
// land on people a tool cannot experience.
package main
import (
"flag"
"fmt"
"os"
"git.hypertheory-labs.dev/loom/loom-cli/internal/external"
)
const usage = `loomctl — fetch what you depend on, and find out when it changed.
loomctl external list <repo-url> what a repository publishes
loomctl external add <url> [--path p] adopt one document and lock it
loomctl external check ask every publisher whether theirs moved
loomctl external apply [path...] move a staged polad into place, lock and all
check reports and does not fix. A document that moved is staged as a polad in
.loom/cart/current/polad/ — a candidate shaped exactly like what it would
become — and somebody decides. Its exits are apply or discard; nothing there
drifts into being kept. With no cart open, check says what moved and stages
nothing, because opening a round is somebody's act and not a side effect.
add warns when a document could only be fetched with a credential: adopting is
copying, and confidentiality does not travel with the copy. It cannot see who
may read the repository the copy lands in, so it reports the half it knows.
add adopts what is not here yet, and refuses what is already adopted. Given a
document that is present but unlocked — fetched by hand before this existed —
it supplies the missing origin: identical bytes lock it, differing bytes are
staged, and the local copy is never overwritten, because a copy that differs is
the only evidence that anything moved while nothing was watching.
Adopted documents live in .loom/externals/<host>/<owner>/<repo>/<name>.md, and
their origins in .loom/externals/.locks. The path is for a person to read; the
lock is what a machine uses, because the path does not round-trip to a URL.
Credentials are read-only and per host, in ~/.config/loomctl/config.json or in
LOOMCTL_TOKEN_<HOST>. loomctl never writes over the network, so a token it is
given should never carry write scope.
Requires git on PATH, for list only.
`
func main() {
if err := run(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, "loomctl: "+err.Error())
os.Exit(1)
}
}
func run(args []string) error {
if len(args) == 0 || args[0] == "-h" || args[0] == "--help" || args[0] == "help" {
fmt.Print(usage)
return nil
}
switch args[0] {
case "external":
return runExternal(args[1:])
default:
return fmt.Errorf("unknown command %q\n\n%s", args[0], usage)
}
}
func runExternal(args []string) error {
if len(args) == 0 {
return fmt.Errorf("external needs a subcommand: list, add, check, apply")
}
switch args[0] {
case "list":
if len(args) != 2 {
return fmt.Errorf("usage: loomctl external list <repo-url>")
}
return external.List(args[1], os.Stdout)
case "add":
fs := flag.NewFlagSet("add", flag.ContinueOnError)
path := fs.String("path", "", "where to keep it, relative to .loom/externals (default: derived from the URL)")
if err := fs.Parse(args[1:]); err != nil {
return err
}
if fs.NArg() != 1 {
return fmt.Errorf("usage: loomctl external add <url> [--path p]")
}
root, err := root()
if err != nil {
return err
}
return external.Add(root, fs.Arg(0), *path, os.Stdout)
case "check":
root, err := root()
if err != nil {
return err
}
return external.Check(root, os.Stdout)
case "apply":
root, err := root()
if err != nil {
return err
}
return external.Apply(root, args[1:], os.Stdout)
default:
return fmt.Errorf("unknown external subcommand %q", args[0])
}
}
func root() (string, error) {
wd, err := os.Getwd()
if err != nil {
return "", err
}
return external.FindRoot(wd)
}