A CLI for tangled operations.
11
fork

Configure Feed

Select the types of activity you want to include in your feed.

Align hosted knot workflows

onevcat 9ea8f9a3 7fd0ccdd

+65 -17
+25 -7
README.md
··· 155 155 tang repo clone onev.cat/tang-playground ./playground 156 156 ``` 157 157 158 + `repo clone` uses the SSH clone URL shown by the Tangled web UI, for example 159 + `git@tangled.org:onev.cat/tang-playground`. Make sure the matching public key is 160 + registered in Tangled before cloning private or writable repositories. 161 + 158 162 Create a repository: 159 163 160 164 ```sh ··· 201 205 Issue arguments may be a displayed number such as `1` or `#1`, a record rkey, or 202 206 a full AT URI. 203 207 208 + New records may need a short time before they appear in `issue list`, because 209 + Tangled discovers cross-record links through Constellation indexing. After 210 + creating an issue, keep the returned AT URI or rkey and use it directly for 211 + follow-up commands: 212 + 213 + ```sh 214 + tang issue view 3mkuteffbxa2b 215 + tang issue comment 3mkuteffbxa2b --body "More detail." 216 + ``` 217 + 204 218 ### Work With Pull Requests 205 219 206 220 List and inspect pull requests: ··· 241 255 tang pr merge 1 --subject "Merge pull request #1" 242 256 ``` 243 257 244 - `pr merge` applies the pull request patch locally, commits it, and records the 245 - pull request as merged on Tangled. Review the worktree before and after merging, 246 - especially when the pull request contains a large patch. 258 + `pr merge` sends the pull request patch to the repository's knot through 259 + Tangled's `sh.tangled.repo.merge` endpoint, then records the pull request as 260 + merged. It follows the same remote merge mechanism used by the Tangled web UI, 261 + with a narrower CLI surface: one pull request patch is merged at a time, and 262 + GitHub-style `--squash` / `--rebase` strategies are not part of the current 263 + Tangled endpoint. 247 264 248 265 Pull request arguments may be a displayed number such as `1` or `#1`, a record 249 266 rkey, or a full AT URI. ··· 272 289 ```sh 273 290 tang config list 274 291 tang config get knot.hosts 275 - tang config set knot.hosts tangled.org,knot1.tangled.sh 292 + tang config set knot.hosts knot1.tangled.sh,tangled.org 276 293 tang config set appview.url https://tangled.org 277 294 tang config set constellation.url https://constellation.microcosm.blue 278 295 tang config set remote origin ··· 339 356 writes. 340 357 - Some older pull request records do not contain patch rounds, so `pr diff` and 341 358 `pr checkout` cannot operate on them. 359 + - SSH clone depends on Tangled's SSH key authorization index. If a freshly added 360 + key is rejected, retry after the key appears in `tang ssh-key list`. 342 361 - `repo create` requires a create-capable knot. Pass `--knot` when the default 343 362 service route is not sufficient. 344 - - `pr merge` is intentionally local-git based. It applies the patch and creates 345 - a commit in the current worktree, so the caller remains responsible for 346 - reviewing and pushing the result. 363 + - `pr merge` depends on the repository knot's `sh.tangled.repo.merge` endpoint. 364 + It is a remote patch merge, not a local worktree merge.
+1
ai-docs/2026-05-02-PHASE_1_auth_basics.md
··· 46 46 47 47 - The original plan expected `onev.cat` to resolve to `https://tngl.sh`; live DID resolution on 2026-05-02 returned `https://discina.us-west.host.bsky.network`. The implementation does not hardcode either value and uses the DID document result, which is the required behavior for migrated or custom-PDS accounts. 48 48 - The SSH-key E2E initially exposed that this PDS rejects unknown lexicons when `validate=true`. `PDSClient.CreateRecord` now sets `validate=false` for Tangled records, which keeps the CLI usable across PDS implementations that do not know Tangled schemas locally. 49 + - Follow-up on 2026-05-03: default `knot.hosts` now puts the create-capable `knot1.tangled.sh` first while keeping `tangled.org` as a recognized host for existing remotes. 49 50 50 51 ## Completion 51 52
+2
ai-docs/2026-05-02-PHASE_3_repo_pr.md
··· 54 54 - `repo create` without `--knot` currently tries the configured default `tangled.org`; live validation returned a 404 HTML response for `sh.tangled.repo.create`. The real hosted create-capable knot for this account was `knot1.tangled.sh`, so the successful validation used `--knot knot1.tangled.sh`. This is a real deployment/config distinction, not a PDS hardcoding issue. 55 55 - Some old `tangled.org/core` PR records do not contain `rounds[]`; `tang pr diff` correctly reports `pull has no rounds` for those patchless/legacy records. 56 56 - `pr merge` does not expose `--squash`, `--rebase`, or `--merge`, matching the current Tangled merge endpoint. 57 + - Follow-up on 2026-05-03: `knot1.tangled.sh` is now the first default knot host, so `repo create` uses the create-capable knot by default. `tangled.org` remains in the default host list for AppView-style and existing remote URLs. 58 + - Follow-up on 2026-05-03: Tangled web maps repos whose record knot is `knot1.tangled.sh` to hosted clone URLs on `tangled.org`. After refreshing the account SSH key, `git clone git@tangled.org:onev.cat/tang-playground` succeeded, so `repo clone` now uses the web-equivalent SSH clone URL. 57 59 58 60 ## Completion 59 61
+5 -1
internal/cli/pr.go
··· 318 318 if message == "" { 319 319 message = pull.Title 320 320 } 321 + commitBody := body 322 + if commitBody == "" { 323 + commitBody = pull.Body 324 + } 321 325 author := session.Handle 322 326 if err := tangled.NewKnotClient(repoRecord.Knot, tangled.WithServiceAuthToken(token)).Merge(cmd.Context(), &core.RepoMerge_Input{ 323 327 Did: ownerDID, ··· 325 329 Branch: pull.Target, 326 330 Patch: patch, 327 331 CommitMessage: &message, 328 - CommitBody: optionalCLIString(body), 332 + CommitBody: optionalCLIString(commitBody), 329 333 AuthorName: &author, 330 334 }); err != nil { 331 335 return err
+4 -3
internal/config/store.go
··· 13 13 const ( 14 14 DefaultAppViewURL = "https://tangled.org" 15 15 DefaultConstellationURL = "https://constellation.microcosm.blue" 16 - DefaultKnotHost = "tangled.org" 16 + DefaultKnotHost = "knot1.tangled.sh" 17 + LegacyKnotHost = "tangled.org" 17 18 ) 18 19 19 20 var ErrUnsupportedKey = errors.New("unsupported config key") ··· 41 42 42 43 func Defaults() *Config { 43 44 return &Config{ 44 - Knot: KnotConfig{Hosts: []string{DefaultKnotHost}}, 45 + Knot: KnotConfig{Hosts: []string{DefaultKnotHost, LegacyKnotHost}}, 45 46 Constellation: ConstellationConfig{URL: DefaultConstellationURL}, 46 47 AppView: AppViewConfig{URL: DefaultAppViewURL}, 47 48 } ··· 157 158 158 159 func (c *Config) applyDefaults() { 159 160 if len(c.Knot.Hosts) == 0 { 160 - c.Knot.Hosts = []string{DefaultKnotHost} 161 + c.Knot.Hosts = []string{DefaultKnotHost, LegacyKnotHost} 161 162 } 162 163 if c.Constellation.URL == "" { 163 164 c.Constellation.URL = DefaultConstellationURL
+1 -1
internal/config/store_test.go
··· 12 12 if err != nil { 13 13 t.Fatalf("LoadAt returned error: %v", err) 14 14 } 15 - if !reflect.DeepEqual(cfg.Knot.Hosts, []string{"tangled.org"}) { 15 + if !reflect.DeepEqual(cfg.Knot.Hosts, []string{"knot1.tangled.sh", "tangled.org"}) { 16 16 t.Fatalf("default knot hosts = %#v", cfg.Knot.Hosts) 17 17 } 18 18 if cfg.Constellation.URL != DefaultConstellationURL {
+1 -1
internal/tangled/pulls.go
··· 22 22 "tangled.org/onev.cat/tang/internal/constellation" 23 23 ) 24 24 25 - var ErrPatchOnlyCheckout = errors.New("patch-only pull requests cannot be checked out") 25 + var ErrPatchOnlyCheckout = errors.New("pull request has no source branch; checkout only works for branch-based pull requests") 26 26 27 27 type Pull struct { 28 28 Number int `json:"number,omitempty"`
+11 -3
internal/tangled/repos.go
··· 130 130 if err != nil { 131 131 return err 132 132 } 133 - return git.Clone(ctx, repo.CloneHTTPS, dir) 133 + return git.Clone(ctx, repo.CloneSSH, dir) 134 134 } 135 135 136 136 func repoFromRecord(owner, uri, cid string, record *core.Repo) Repo { ··· 142 142 if record.RepoDid != nil { 143 143 repoDID = *record.RepoDid 144 144 } 145 + cloneHost := cloneHostForKnot(record.Knot) 145 146 return Repo{ 146 147 Owner: owner, 147 148 Name: record.Name, ··· 151 152 CreatedAt: record.CreatedAt, 152 153 URI: uri, 153 154 CID: cid, 154 - CloneSSH: fmt.Sprintf("git@%s:%s/%s.git", record.Knot, owner, record.Name), 155 - CloneHTTPS: fmt.Sprintf("https://%s/%s/%s", record.Knot, owner, record.Name), 155 + CloneSSH: fmt.Sprintf("git@%s:%s/%s", cloneHost, owner, record.Name), 156 + CloneHTTPS: fmt.Sprintf("https://%s/%s/%s", cloneHost, owner, record.Name), 156 157 } 158 + } 159 + 160 + func cloneHostForKnot(knot string) string { 161 + if knot == "knot1.tangled.sh" { 162 + return "tangled.org" 163 + } 164 + return knot 157 165 } 158 166 159 167 func resolveOwner(ctx context.Context, owner string) (did, pds string, err error) {
+15 -1
internal/tangled/repos_test.go
··· 12 12 Knot: "knot.example.com", 13 13 CreatedAt: "2026-05-02T00:00:00Z", 14 14 }) 15 - if repo.CloneSSH != "git@knot.example.com:onev.cat/tang.git" { 15 + if repo.CloneSSH != "git@knot.example.com:onev.cat/tang" { 16 16 t.Fatalf("CloneSSH = %q", repo.CloneSSH) 17 17 } 18 18 if repo.CloneHTTPS != "https://knot.example.com/onev.cat/tang" { 19 19 t.Fatalf("CloneHTTPS = %q", repo.CloneHTTPS) 20 20 } 21 21 } 22 + 23 + func TestRepoFromRecordUsesHostedCloneHostForDefaultHostedKnot(t *testing.T) { 24 + repo := repoFromRecord("onev.cat", "at://did/sh.tangled.repo/r", "cid", &core.Repo{ 25 + Name: "tang", 26 + Knot: "knot1.tangled.sh", 27 + CreatedAt: "2026-05-02T00:00:00Z", 28 + }) 29 + if repo.CloneSSH != "git@tangled.org:onev.cat/tang" { 30 + t.Fatalf("CloneSSH = %q", repo.CloneSSH) 31 + } 32 + if repo.CloneHTTPS != "https://tangled.org/onev.cat/tang" { 33 + t.Fatalf("CloneHTTPS = %q", repo.CloneHTTPS) 34 + } 35 + }