From 22b2dbd40ee4e8ee8f4efe5ef3dd9c723454754e Mon Sep 17 00:00:00 2001 From: Graham Barber Date: Wed, 19 Aug 2026 05:13:56 +0000 Subject: [PATCH] feat(cli): project set — update optional project fields Dogfooding 0.3.0 surfaced the gap: the docs say "set your project source once" and no command could. Only flags passed change; explicit empty clears. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01PWXFcuaRLZycYVAF7senMK --- cmd/disttown/project.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ web/src/routes/docs/cli/+page.md | 13 +++++++++++++ 2 file(s) changed, 67 insertion(s)(+), 0 deletion(s)(-) diff --git a/cmd/disttown/project.go b/cmd/disttown/project.go --- a/cmd/disttown/project.go +++ b/cmd/disttown/project.go @@ -58,6 +58,60 @@ return nil }, }, { + Name: "set", + Usage: "update a project's optional fields (only the flags you pass change)", + ArgsUsage: "", + Flags: []cli.Flag{ + &cli.StringFlag{Name: "display-name", Usage: "human-facing name"}, + &cli.StringFlag{Name: "description", Usage: "brief description"}, + &cli.StringFlag{Name: "source", Usage: "source repository: an at:// record uri (e.g. your sh.tangled.repo — same-DID sources are self-authenticated) or an https:// forge URL"}, + &cli.StringFlag{Name: "website", Usage: "project homepage, distinct from source"}, + }, + Action: func(c *cli.Context) error { + if c.Args().Len() != 1 { + return fmt.Errorf("usage: disttown project set --source ...") + } + name := canon.NormalizeName(c.Args().First()) + if err := canon.ValidateName(name); err != nil { + return err + } + if !c.IsSet("display-name") && !c.IsSet("description") && !c.IsSet("source") && !c.IsSet("website") { + return fmt.Errorf("nothing to change: pass at least one of --display-name, --description, --source, --website") + } + s, err := atsession.Get(c.Context) + if err != nil { + return err + } + out, err := atproto.RepoGetRecord(c.Context, s.Client, "", canon.NSIDProject, s.DID, name) + if err != nil { + return fmt.Errorf("reading project %q: %w", name, err) + } + rec, ok := out.Value.Val.(*gen.Project) + if !ok { + return fmt.Errorf("record %q is not a project", name) + } + set := func(flag string, field **string) { + if c.IsSet(flag) { + if v := c.String(flag); v == "" { + *field = nil // explicit empty clears the field + } else { + *field = &v + } + } + } + set("display-name", &rec.DisplayName) + set("description", &rec.Description) + set("source", &rec.Source) + set("website", &rec.Website) + w := updateElem(canon.NSIDProject, name, rec) + if err := applyWrites(c.Context, s, []*atproto.RepoApplyWrites_Input_Writes_Elem{w}); err != nil { + return err + } + fmt.Printf("updated project %s\n %s\n", name, atURI(s.DID, canon.NSIDProject, name)) + return nil + }, + }, + { Name: "rename", Usage: "rename a project: a successor record is created and the old record persists as a permanent alias", ArgsUsage: " ", diff --git a/web/src/routes/docs/cli/+page.md b/web/src/routes/docs/cli/+page.md --- a/web/src/routes/docs/cli/+page.md +++ b/web/src/routes/docs/cli/+page.md @@ -131,6 +131,19 @@ - `--display-name ` — human-facing name - `--description ` — brief description +### project set + +```sh +disttown project set +``` + +Update a project's optional fields (only the flags you pass change) + +- `--display-name ` — human-facing name +- `--description ` — brief description +- `--source ` — source repository: an at:// record uri (e.g. your sh.tangled.repo — same-DID sources are self-authenticated) or an https:// forge URL +- `--website ` — project homepage, distinct from source + ### project rename ```sh -- tangled.sh