Restores something the specimen said and the round that discarded the specimen lost with it: a changed external becomes a polad in the cart, and somebody decides. check now stages what moved into .loom/cart/current/polad/ with the ETag that was served alongside the bytes, and prints the .usages.md beside it, because reconciliation runs the other way — the facets usually survive and what moves is the code a usage named. It says so when there is no usages file, which is its own finding. With no cart open, check reports and stages nothing. The tool does not open a round: a cart is a bounded exchange between two presences and starting one is somebody's act, not a side effect of asking about freshness. add now adopts what is not here and refuses what is already adopted, superseding the entry that had it announce an overwrite — it no longer overwrites at all. The one exception is the only way out of a dead end: a document present but unlocked was fetched by hand, nothing records its origin, and the path does not round-trip, so check cannot ask about it and a refusal would strand it forever. add accepts it and the bytes decide — identical locks it without rewriting anything, which makes the lock's assertion verified rather than assumed, and different stages a polad and leaves the local copy alone because it is the only evidence anything moved. apply exists because the lock is the half a person forgets: moving a polad by hand leaves a lock describing the copy you just replaced. Recorded with its limit — for an external, discard does not mean the change goes away, so discarding is really knowingly stale and nothing yet records that choice. Measured end to end on this repository: eight hand-fetched documents, all eight locked, nothing rewritten. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018UTxuSizozEA8yDitPuris
119 lines
3.6 KiB
Go
119 lines
3.6 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 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)
|
|
}
|