Gleam CLI client for Tangled (clone of aly.codes/tg)
0

Configure Feed

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

gtg / src / gtg / cli.gleam
28 kB 925 lines
1//// CLI command tree for gtg (Tangled client). 2 3import gleam/io 4import gleam/json 5import gleam/list 6import gleam/option.{type Option, None, Some} 7import gleam/result 8import gleam/string 9import gtg/app/service 10import gtg/app/types 11import gtg/config 12import gtg/knot/client as knot 13import gtg/output 14import gtg/sys 15import shellout 16import simplifile 17 18pub fn run(args: List(String)) -> Int { 19 let flags = config.parse_flags(args) 20 let settings = config.load(flags) 21 let cmd_args = config.strip_global_flags(args) 22 let svc = service.new(settings.appview, settings.account) 23 case dispatch(svc, settings, cmd_args) { 24 Ok(Nil) -> 0 25 Error(msg) -> { 26 io.println_error(msg) 27 1 28 } 29 } 30} 31 32fn dispatch( 33 svc: service.Service, 34 settings: config.Settings, 35 args: List(String), 36) -> Result(Nil, String) { 37 case args { 38 [] | ["-h"] | ["--help"] | ["help"] -> { 39 print_root_help() 40 Ok(Nil) 41 } 42 ["auth", ..rest] -> cmd_auth(svc, settings, rest) 43 ["repo", ..rest] -> cmd_repo(svc, settings, rest) 44 ["issue", ..rest] -> cmd_issue(svc, settings, rest) 45 ["pr", ..rest] -> cmd_pr(svc, settings, rest) 46 ["string", ..rest] -> cmd_string(svc, settings, rest) 47 ["ssh-key", ..rest] -> cmd_ssh_key(svc, settings, rest) 48 ["browse", ..rest] -> cmd_browse(svc, rest) 49 ["api", ..rest] -> cmd_api(svc, rest) 50 ["completion", ..] -> { 51 io.println("# gtg completion not yet implemented") 52 Ok(Nil) 53 } 54 [unknown, ..] -> Error("unknown command: " <> unknown) 55 } 56} 57 58fn map_err(r: Result(a, service.AppError)) -> Result(a, String) { 59 result.map_error(r, service.error_message) 60} 61 62// ---------- auth ---------- 63 64fn cmd_auth( 65 svc: service.Service, 66 settings: config.Settings, 67 args: List(String), 68) -> Result(Nil, String) { 69 case args { 70 [] | ["-h"] | ["--help"] -> { 71 io.println( 72 "Manage authentication\n\nUsage:\n gtg auth <command>\n\nCommands:\n login Log in via OAuth (browser) or app password\n logout Log out\n status Show auth status\n token Print access token\n list List accounts\n switch Switch active account\n\nLogin:\n gtg auth login <handle> OAuth in browser\n gtg auth login <handle> <app-password> headless app password", 73 ) 74 Ok(Nil) 75 } 76 ["login", handle] -> do_oauth_login(handle) 77 ["login", handle, "--password-stdin"] -> { 78 case sys.env("GTG_PASSWORD") { 79 Ok(pw) -> do_login(handle, pw) 80 Error(_) -> 81 Error( 82 "pass app password as argument, or set GTG_PASSWORD for --password-stdin in this build", 83 ) 84 } 85 } 86 ["login", handle, password] -> do_login(handle, password) 87 ["logout", ..rest] -> { 88 let all = list.contains(rest, "--all") 89 use r <- result.try(map_err(service.logout(svc, all))) 90 case settings.json { 91 True -> 92 output.print_json( 93 json.object([#("wasLoggedIn", json.bool(r.was_logged_in))]), 94 ) 95 False -> 96 case r.was_logged_in { 97 True -> io.println("Logged out.") 98 False -> io.println("Not logged in.") 99 } 100 } 101 Ok(Nil) 102 } 103 ["status"] -> { 104 use r <- result.try(map_err(service.auth_status(svc))) 105 case settings.json { 106 True -> output.print_json(types.auth_status_json(r)) 107 False -> 108 case r.authenticated { 109 False -> io.println("Not logged in.") 110 True -> 111 case r.status { 112 "active" -> io.println("Logged in as " <> r.handle) 113 "expired" -> 114 io.println( 115 "Session expired. Run \"gtg auth login\" to re-authenticate.", 116 ) 117 _ -> io.println("Unable to verify session (network error).") 118 } 119 } 120 } 121 Ok(Nil) 122 } 123 ["token"] -> { 124 use tok <- result.try(map_err(service.access_token(svc))) 125 io.println(tok) 126 Ok(Nil) 127 } 128 ["list"] -> { 129 use items <- result.try(map_err(service.auth_accounts(svc))) 130 case settings.json { 131 True -> 132 output.print_json( 133 json.preprocessed_array(list.map(items, types.auth_account_json)), 134 ) 135 False -> 136 case items { 137 [] -> io.println("No accounts.") 138 _ -> 139 list.each(items, fn(item) { 140 let marker = case item.active { 141 True -> "*" 142 False -> " " 143 } 144 io.println( 145 marker 146 <> " " 147 <> item.handle 148 <> " " 149 <> item.did 150 <> " " 151 <> item.method, 152 ) 153 }) 154 } 155 } 156 Ok(Nil) 157 } 158 ["switch", selector] -> { 159 use r <- result.try(map_err(service.switch_account(selector))) 160 case settings.json { 161 True -> output.print_json(types.auth_account_json(r)) 162 False -> io.println("Switched to " <> r.handle <> " (" <> r.did <> ")") 163 } 164 Ok(Nil) 165 } 166 _ -> Error("usage: gtg auth <login|logout|status|token|list|switch>") 167 } 168} 169 170fn do_login(handle: String, password: String) -> Result(Nil, String) { 171 use did <- result.try(map_err(service.login_with_password(handle, password))) 172 io.println("Logged in as " <> did) 173 Ok(Nil) 174} 175 176fn do_oauth_login(handle: String) -> Result(Nil, String) { 177 use did <- result.try(map_err(service.login_with_oauth(handle))) 178 io.println("Logged in as " <> did) 179 Ok(Nil) 180} 181 182// ---------- repo ---------- 183 184fn cmd_repo( 185 svc: service.Service, 186 settings: config.Settings, 187 args: List(String), 188) -> Result(Nil, String) { 189 case args { 190 [] | ["-h"] | ["--help"] -> { 191 io.println( 192 "Manage repositories\n\nCommands:\n list [handle]\n view <handle/repo>\n clone <repo|handle/repo> [dir]\n create <name> [--description ...] [--knot ...] [--clone] [--push path]\n delete <handle/repo>\n set-default-branch <branch> [--repo handle/repo]\n edit [--description ...] [--repo handle/repo]", 193 ) 194 Ok(Nil) 195 } 196 ["list"] -> repo_list(svc, settings, "") 197 ["list", handle] -> repo_list(svc, settings, handle) 198 ["view", target] -> { 199 use t <- result.try(map_err(service.parse_target(target))) 200 use item <- result.try(map_err(service.view_repo(svc, t))) 201 case settings.json { 202 True -> output.print_json(types.repo_item_json(item)) 203 False -> { 204 io.println("Name: " <> item.name) 205 io.println("Description: " <> item.description) 206 io.println("URI: " <> item.uri) 207 io.println("Knot: " <> item.knot) 208 io.println("Created: " <> item.created_at) 209 case item.repo_did { 210 "" -> Nil 211 d -> io.println("Repo DID: " <> d) 212 } 213 } 214 } 215 Ok(Nil) 216 } 217 ["clone", arg] -> repo_clone(svc, settings, arg, None) 218 ["clone", arg, dest] -> repo_clone(svc, settings, arg, Some(dest)) 219 ["create", name, ..flags] -> repo_create(svc, settings, name, flags) 220 ["delete", target] -> { 221 use t <- result.try(map_err(service.parse_target(target))) 222 use r <- result.try(map_err(service.delete_repo(svc, t))) 223 case settings.json { 224 True -> output.print_json(json.object([#("uri", json.string(r.uri))])) 225 False -> io.println("Deleted " <> r.uri) 226 } 227 Ok(Nil) 228 } 229 ["set-default-branch", branch, ..rest] -> { 230 let repo_flag = flag_value(rest, "--repo", "-R") 231 use t <- result.try(map_err(service.resolve_target_flag(repo_flag))) 232 use r <- result.try(map_err(service.set_default_branch(svc, t, branch))) 233 case settings.json { 234 True -> 235 output.print_json( 236 json.object([ 237 #("uri", json.string(r.uri)), 238 #("branch", json.string(r.branch)), 239 ]), 240 ) 241 False -> io.println("Set default branch to " <> r.branch) 242 } 243 Ok(Nil) 244 } 245 ["edit", ..rest] -> { 246 let repo_flag = flag_value(rest, "--repo", "-R") 247 let desc = flag_value(rest, "--description", "") 248 use t <- result.try(map_err(service.resolve_target_flag(repo_flag))) 249 let description = case desc { 250 "" -> None 251 d -> Some(d) 252 } 253 use r <- result.try(map_err(service.edit_repo(svc, t, description))) 254 case settings.json { 255 True -> 256 output.print_json( 257 json.object([ 258 #("uri", json.string(r.uri)), 259 #("description", json.string(r.description)), 260 ]), 261 ) 262 False -> io.println("Updated " <> r.uri) 263 } 264 Ok(Nil) 265 } 266 _ -> Error("usage: gtg repo <list|view|clone|create|delete|…>") 267 } 268} 269 270fn repo_list( 271 svc: service.Service, 272 settings: config.Settings, 273 handle: String, 274) -> Result(Nil, String) { 275 use handle <- result.try(map_err(service.handle_or_self(svc, handle))) 276 use items <- result.try(map_err(service.list_repos(svc, handle))) 277 case settings.json { 278 True -> 279 output.print_json( 280 json.preprocessed_array(list.map(items, types.repo_item_json)), 281 ) 282 False -> output.print_repos(items) 283 } 284 Ok(Nil) 285} 286 287fn repo_clone( 288 svc: service.Service, 289 settings: config.Settings, 290 arg: String, 291 dest: Option(String), 292) -> Result(Nil, String) { 293 use t <- result.try(case string.contains(arg, "/") { 294 True -> map_err(service.parse_target(arg)) 295 False -> { 296 use handle <- result.try(map_err(service.handle_or_self(svc, ""))) 297 Ok(types.Target(handle:, repo: arg)) 298 } 299 }) 300 let destination = case dest { 301 Some(d) -> d 302 None -> t.repo 303 } 304 use r <- result.try( 305 map_err(service.clone_repo(svc, t.handle, t.repo, destination)), 306 ) 307 case settings.json { 308 True -> 309 output.print_json( 310 json.object([ 311 #("handle", json.string(r.handle)), 312 #("repo", json.string(r.repo)), 313 #("destination", json.string(r.destination)), 314 ]), 315 ) 316 False -> 317 io.println( 318 "Cloned " <> r.handle <> "/" <> r.repo <> " into " <> r.destination, 319 ) 320 } 321 Ok(Nil) 322} 323 324fn repo_create( 325 svc: service.Service, 326 settings: config.Settings, 327 name: String, 328 flags: List(String), 329) -> Result(Nil, String) { 330 let description = flag_value(flags, "--description", "") 331 let knot_host = case flag_value(flags, "--knot", "") { 332 "" -> knot.default_knot 333 k -> k 334 } 335 let do_clone = list.contains(flags, "--clone") 336 let push_path = flag_value(flags, "--push", "") 337 let remote = case flag_value(flags, "--remote", "") { 338 "" -> "origin" 339 r -> r 340 } 341 use r <- result.try( 342 map_err(service.create_repo( 343 svc, 344 name, 345 description, 346 knot_host, 347 do_clone, 348 push_path, 349 remote, 350 )), 351 ) 352 case settings.json { 353 True -> output.print_json(types.repo_create_json(r)) 354 False -> { 355 io.println("Created repository " <> r.handle <> "/" <> r.name) 356 case r.cloned { 357 True -> io.println("Cloned into " <> r.name) 358 False -> Nil 359 } 360 case r.pushed { 361 True -> io.println("Pushed to " <> r.name) 362 False -> Nil 363 } 364 list.each(r.warnings, fn(w) { io.println_error("warning: " <> w) }) 365 } 366 } 367 Ok(Nil) 368} 369 370// ---------- issue ---------- 371 372fn cmd_issue( 373 svc: service.Service, 374 settings: config.Settings, 375 args: List(String), 376) -> Result(Nil, String) { 377 case args { 378 [] | ["-h"] | ["--help"] -> { 379 io.println( 380 "Manage issues\n\nCommands:\n list [handle/repo]\n view <rkey> [-R handle/repo]\n create <title> [-b body] [-R handle/repo]\n comment <rkey> [-b body]\n close <rkey>\n reopen <rkey>", 381 ) 382 Ok(Nil) 383 } 384 ["list", ..rest] -> { 385 let arg = case rest { 386 [a] -> [a] 387 _ -> [] 388 } 389 use t <- result.try(map_err(service.resolve_target(arg))) 390 use items <- result.try(map_err(service.list_issues(svc, t))) 391 case settings.json { 392 True -> 393 output.print_json( 394 json.preprocessed_array(list.map(items, types.item_json)), 395 ) 396 False -> output.print_items(items, "No issues found.") 397 } 398 Ok(Nil) 399 } 400 ["view", rkey, ..rest] -> { 401 let repo = flag_value(rest, "--repo", "-R") 402 use t <- result.try(map_err(service.resolve_target_flag(repo))) 403 use v <- result.try(map_err(service.view_issue(svc, t, rkey))) 404 case settings.json { 405 True -> output.print_json(types.view_json(v)) 406 False -> { 407 io.println("Title: " <> v.title) 408 io.println("Author: " <> v.author.handle) 409 io.println("Created: " <> v.created_at) 410 case v.body { 411 "" -> Nil 412 b -> { 413 io.println("") 414 io.println(b) 415 } 416 } 417 } 418 } 419 Ok(Nil) 420 } 421 ["create", title, ..rest] -> { 422 let body = flag_value(rest, "--body", "-b") 423 let body = case flag_value(rest, "--body-file", "-F") { 424 "" -> body 425 path -> 426 case simplifile.read(path) { 427 Ok(c) -> c 428 Error(_) -> body 429 } 430 } 431 let repo = flag_value(rest, "--repo", "-R") 432 use t <- result.try(map_err(service.resolve_target_flag(repo))) 433 use r <- result.try(map_err(service.create_issue(svc, t, title, body))) 434 case settings.json { 435 True -> output.print_json(types.created_json(r)) 436 False -> io.println("Created issue " <> r.uri) 437 } 438 Ok(Nil) 439 } 440 ["comment", rkey, ..rest] -> { 441 let body = flag_value(rest, "--body", "-b") 442 let repo = flag_value(rest, "--repo", "-R") 443 use t <- result.try(map_err(service.resolve_target_flag(repo))) 444 use r <- result.try(map_err(service.comment_issue(svc, t, rkey, body))) 445 case settings.json { 446 True -> output.print_json(types.created_json(r)) 447 False -> io.println("Created comment " <> r.uri) 448 } 449 Ok(Nil) 450 } 451 ["close", rkey, ..rest] -> issue_state(svc, settings, rkey, rest, "closed") 452 ["reopen", rkey, ..rest] -> issue_state(svc, settings, rkey, rest, "open") 453 _ -> Error("usage: gtg issue <list|view|create|comment|close|reopen>") 454 } 455} 456 457fn issue_state( 458 svc: service.Service, 459 settings: config.Settings, 460 rkey: String, 461 rest: List(String), 462 state: String, 463) -> Result(Nil, String) { 464 let repo = flag_value(rest, "--repo", "-R") 465 use t <- result.try(map_err(service.resolve_target_flag(repo))) 466 use r <- result.try(map_err(service.set_issue_state(svc, t, rkey, state))) 467 case settings.json { 468 True -> output.print_json(types.state_json(r)) 469 False -> io.println("Issue " <> r.rkey <> " is now " <> r.state) 470 } 471 Ok(Nil) 472} 473 474// ---------- pr ---------- 475 476fn cmd_pr( 477 svc: service.Service, 478 settings: config.Settings, 479 args: List(String), 480) -> Result(Nil, String) { 481 case args { 482 [] | ["-h"] | ["--help"] -> { 483 io.println( 484 "Manage pull requests\n\nCommands:\n list [handle/repo]\n view <rkey>\n create --title …\n comment <rkey> -b body\n diff <rkey>\n checkout <rkey>\n close|reopen|merge <rkey>", 485 ) 486 Ok(Nil) 487 } 488 ["list", ..rest] -> { 489 let arg = case rest { 490 [a] -> [a] 491 _ -> [] 492 } 493 use t <- result.try(map_err(service.resolve_target(arg))) 494 use items <- result.try(map_err(service.list_pulls(svc, t))) 495 case settings.json { 496 True -> 497 output.print_json( 498 json.preprocessed_array(list.map(items, types.item_json)), 499 ) 500 False -> output.print_items(items, "No pull requests found.") 501 } 502 Ok(Nil) 503 } 504 ["view", rkey, ..rest] -> { 505 let repo = flag_value(rest, "--repo", "-R") 506 use t <- result.try(map_err(service.resolve_target_flag(repo))) 507 use v <- result.try(map_err(service.view_pull(svc, t, rkey))) 508 case settings.json { 509 True -> output.print_json(types.view_json(v)) 510 False -> { 511 io.println("Title: " <> v.title) 512 io.println("Author: " <> v.author.handle) 513 io.println("Created: " <> v.created_at) 514 io.println( 515 "Branch: " <> v.source_branch <> "" <> v.target_branch, 516 ) 517 case v.body { 518 "" -> Nil 519 b -> { 520 io.println("") 521 io.println(b) 522 } 523 } 524 } 525 } 526 Ok(Nil) 527 } 528 ["create", ..rest] -> { 529 let title = flag_value(rest, "--title", "-t") 530 case title { 531 "" -> Error("--title is required") 532 title -> { 533 let body = flag_value(rest, "--body", "-b") 534 let base = flag_value(rest, "--base", "-B") 535 let head = flag_value(rest, "--head", "-H") 536 let repo = flag_value(rest, "--repo", "-R") 537 let source = flag_value(rest, "--source-repo", "") 538 use t <- result.try(map_err(service.resolve_target_flag(repo))) 539 use source_t <- result.try(case source { 540 "" -> Ok(None) 541 s -> 542 map_err(service.parse_target(s)) 543 |> result.map(Some) 544 }) 545 use dir <- result.try( 546 sys.get_cwd() |> result.map_error(fn(e) { e }), 547 ) 548 use r <- result.try( 549 map_err(service.create_pull( 550 svc, 551 t, 552 title, 553 body, 554 base, 555 head, 556 dir, 557 source_t, 558 )), 559 ) 560 case settings.json { 561 True -> output.print_json(types.pr_create_json(r)) 562 False -> 563 io.println( 564 "Created pull request " 565 <> r.uri 566 <> " (" 567 <> r.head 568 <> " -> " 569 <> r.base 570 <> ")", 571 ) 572 } 573 Ok(Nil) 574 } 575 } 576 } 577 ["comment", rkey, ..rest] -> { 578 let body = flag_value(rest, "--body", "-b") 579 let repo = flag_value(rest, "--repo", "-R") 580 use t <- result.try(map_err(service.resolve_target_flag(repo))) 581 use r <- result.try(map_err(service.comment_pull(svc, t, rkey, body))) 582 case settings.json { 583 True -> output.print_json(types.created_json(r)) 584 False -> io.println("Created comment " <> r.uri) 585 } 586 Ok(Nil) 587 } 588 ["diff", rkey, ..rest] -> { 589 let repo = flag_value(rest, "--repo", "-R") 590 use t <- result.try(map_err(service.resolve_target_flag(repo))) 591 use patch <- result.try(map_err(service.pull_diff(svc, t, rkey))) 592 io.println(patch) 593 Ok(Nil) 594 } 595 ["checkout", rkey, ..rest] -> { 596 let repo = flag_value(rest, "--repo", "-R") 597 use t <- result.try(map_err(service.resolve_target_flag(repo))) 598 use r <- result.try(map_err(service.checkout_pull(svc, t, rkey))) 599 case settings.json { 600 True -> 601 output.print_json( 602 json.object([ 603 #("rkey", json.string(r.rkey)), 604 #("branch", json.string(r.branch)), 605 ]), 606 ) 607 False -> io.println("Checked out " <> r.branch) 608 } 609 Ok(Nil) 610 } 611 ["close", rkey, ..rest] -> pr_state(svc, settings, rkey, rest, "closed") 612 ["reopen", rkey, ..rest] -> pr_state(svc, settings, rkey, rest, "open") 613 ["merge", rkey, ..rest] -> { 614 let repo = flag_value(rest, "--repo", "-R") 615 use t <- result.try(map_err(service.resolve_target_flag(repo))) 616 use r <- result.try(map_err(service.merge_pull(svc, t, rkey))) 617 case settings.json { 618 True -> output.print_json(types.state_json(r)) 619 False -> io.println("Merged pull request " <> r.rkey) 620 } 621 Ok(Nil) 622 } 623 _ -> Error("usage: gtg pr <list|view|create|…>") 624 } 625} 626 627fn pr_state( 628 svc: service.Service, 629 settings: config.Settings, 630 rkey: String, 631 rest: List(String), 632 state: String, 633) -> Result(Nil, String) { 634 let repo = flag_value(rest, "--repo", "-R") 635 use t <- result.try(map_err(service.resolve_target_flag(repo))) 636 use r <- result.try(map_err(service.set_pull_state(svc, t, rkey, state))) 637 case settings.json { 638 True -> output.print_json(types.state_json(r)) 639 False -> io.println("Pull request " <> r.rkey <> " is now " <> r.state) 640 } 641 Ok(Nil) 642} 643 644// ---------- string ---------- 645 646fn cmd_string( 647 svc: service.Service, 648 settings: config.Settings, 649 args: List(String), 650) -> Result(Nil, String) { 651 case args { 652 [] | ["-h"] | ["--help"] -> { 653 io.println( 654 "Manage strings\n\nCommands:\n list [handle]\n view <rkey> [handle]\n create --filename … --contents …\n delete <rkey>", 655 ) 656 Ok(Nil) 657 } 658 ["list"] -> string_list(svc, settings, "") 659 ["list", handle] -> string_list(svc, settings, handle) 660 ["view", rkey] -> string_view(svc, settings, "", rkey) 661 ["view", rkey, handle] -> string_view(svc, settings, handle, rkey) 662 ["create", ..rest] -> { 663 let filename = flag_value(rest, "--filename", "") 664 let description = flag_value(rest, "--description", "") 665 let contents = flag_value(rest, "--contents", "") 666 let contents = case flag_value(rest, "--contents-file", "") { 667 "" -> contents 668 path -> 669 case simplifile.read(path) { 670 Ok(c) -> c 671 Error(_) -> contents 672 } 673 } 674 case filename { 675 "" -> Error("--filename is required") 676 filename -> { 677 use r <- result.try( 678 map_err(service.create_string(svc, filename, description, contents)), 679 ) 680 case settings.json { 681 True -> output.print_json(types.created_json(r)) 682 False -> io.println("Created string " <> r.uri) 683 } 684 Ok(Nil) 685 } 686 } 687 } 688 ["delete", rkey] -> { 689 use r <- result.try(map_err(service.delete_string(svc, rkey))) 690 case settings.json { 691 True -> output.print_json(types.deleted_json(r)) 692 False -> io.println("Deleted string " <> r.rkey) 693 } 694 Ok(Nil) 695 } 696 _ -> Error("usage: gtg string <list|view|create|delete>") 697 } 698} 699 700fn string_list( 701 svc: service.Service, 702 settings: config.Settings, 703 handle: String, 704) -> Result(Nil, String) { 705 use handle <- result.try(map_err(service.handle_or_self(svc, handle))) 706 use items <- result.try(map_err(service.list_strings(svc, handle))) 707 case settings.json { 708 True -> 709 output.print_json( 710 json.preprocessed_array(list.map(items, types.string_item_json)), 711 ) 712 False -> output.print_strings(items) 713 } 714 Ok(Nil) 715} 716 717fn string_view( 718 svc: service.Service, 719 settings: config.Settings, 720 handle: String, 721 rkey: String, 722) -> Result(Nil, String) { 723 use handle <- result.try(map_err(service.handle_or_self(svc, handle))) 724 use v <- result.try(map_err(service.view_string(svc, handle, rkey))) 725 case settings.json { 726 True -> output.print_json(types.string_view_json(v)) 727 False -> { 728 io.println("Filename: " <> v.filename) 729 io.println("Author: " <> v.author.handle) 730 io.println("Created: " <> v.created_at) 731 case v.description { 732 "" -> Nil 733 d -> io.println("Description: " <> d) 734 } 735 io.println("") 736 io.println(v.contents) 737 } 738 } 739 Ok(Nil) 740} 741 742// ---------- ssh-key ---------- 743 744fn cmd_ssh_key( 745 svc: service.Service, 746 settings: config.Settings, 747 args: List(String), 748) -> Result(Nil, String) { 749 case args { 750 [] | ["-h"] | ["--help"] -> { 751 io.println( 752 "Manage SSH keys\n\nCommands:\n list [handle]\n add --name … --key …\n delete <rkey>", 753 ) 754 Ok(Nil) 755 } 756 ["list"] -> ssh_list(svc, settings, "") 757 ["list", handle] -> ssh_list(svc, settings, handle) 758 ["add", ..rest] -> { 759 let name = flag_value(rest, "--name", "") 760 let key = flag_value(rest, "--key", "") 761 let key = case flag_value(rest, "--key-file", "") { 762 "" -> key 763 path -> 764 case simplifile.read(path) { 765 Ok(c) -> string.trim(c) 766 Error(_) -> key 767 } 768 } 769 case name == "" || key == "" { 770 True -> Error("--name and --key (or --key-file) are required") 771 False -> { 772 use r <- result.try(map_err(service.add_ssh_key(svc, name, key))) 773 case settings.json { 774 True -> 775 output.print_json( 776 json.object([ 777 #("name", json.string(r.name)), 778 #("uri", json.string(r.uri)), 779 ]), 780 ) 781 False -> io.println("Added SSH key " <> r.name <> " (" <> r.uri <> ")") 782 } 783 Ok(Nil) 784 } 785 } 786 } 787 ["delete", rkey] -> { 788 use r <- result.try(map_err(service.delete_ssh_key(svc, rkey))) 789 case settings.json { 790 True -> output.print_json(types.deleted_json(r)) 791 False -> io.println("Deleted SSH key " <> r.rkey) 792 } 793 Ok(Nil) 794 } 795 _ -> Error("usage: gtg ssh-key <list|add|delete>") 796 } 797} 798 799fn ssh_list( 800 svc: service.Service, 801 settings: config.Settings, 802 handle: String, 803) -> Result(Nil, String) { 804 use handle <- result.try(map_err(service.handle_or_self(svc, handle))) 805 use items <- result.try(map_err(service.list_ssh_keys(svc, handle))) 806 case settings.json { 807 True -> 808 output.print_json( 809 json.preprocessed_array(list.map(items, types.ssh_key_json)), 810 ) 811 False -> output.print_ssh_keys(items) 812 } 813 Ok(Nil) 814} 815 816// ---------- browse / api ---------- 817 818fn cmd_browse(_svc: service.Service, args: List(String)) -> Result(Nil, String) { 819 use t <- result.try(map_err(service.resolve_target(args))) 820 let url = "https://tangled.org/" <> t.handle <> "/" <> t.repo 821 case shellout.command(run: "xdg-open", with: [url], in: ".", opt: []) { 822 Ok(_) -> Ok(Nil) 823 Error(_) -> { 824 io.println(url) 825 Ok(Nil) 826 } 827 } 828} 829 830fn cmd_api(svc: service.Service, args: List(String)) -> Result(Nil, String) { 831 case args { 832 [nsid, ..rest] -> { 833 let method = case flag_value(rest, "--method", "-X") { 834 "" -> "GET" 835 m -> m 836 } 837 let fields = collect_fields(rest, []) 838 use #(status, body) <- result.try( 839 map_err(service.call_api(svc, nsid, method, fields)), 840 ) 841 case status >= 200 && status < 300 { 842 True -> { 843 io.println(body) 844 Ok(Nil) 845 } 846 False -> Error("HTTP " <> sys.int_to_string(status) <> ": " <> body) 847 } 848 } 849 [] -> Error("usage: gtg api <nsid> [-X GET|POST] [-f key=value]…") 850 } 851} 852 853fn collect_fields( 854 args: List(String), 855 acc: List(#(String, String)), 856) -> List(#(String, String)) { 857 case args { 858 [] -> list.reverse(acc) 859 ["-f", kv, ..rest] | ["--field", kv, ..rest] -> { 860 case string.split_once(kv, "=") { 861 Ok(pair) -> collect_fields(rest, [pair, ..acc]) 862 Error(_) -> collect_fields(rest, acc) 863 } 864 } 865 [_, ..rest] -> collect_fields(rest, acc) 866 } 867} 868 869// ---------- flag helpers ---------- 870 871/// Get flag value for long and optional short form. 872fn flag_value(args: List(String), long: String, short: String) -> String { 873 flag_value_loop(args, long, short) 874} 875 876fn flag_value_loop(args: List(String), long: String, short: String) -> String { 877 case args { 878 [] -> "" 879 [arg, ..rest] -> { 880 case arg == long || { short != "" && arg == short } { 881 True -> 882 case rest { 883 [v, ..] -> v 884 [] -> "" 885 } 886 False -> 887 case string.starts_with(arg, long <> "=") { 888 True -> string.drop_start(arg, string.length(long) + 1) 889 False -> flag_value_loop(rest, long, short) 890 } 891 } 892 } 893 } 894} 895 896fn print_root_help() -> Nil { 897 io.println( 898 string.join( 899 [ 900 "A CLI for Tangled (Gleam port of tg)", 901 "", 902 "Usage:", 903 " gtg [command]", 904 "", 905 "Available Commands:", 906 " api Call an authenticated XRPC endpoint", 907 " auth Manage authentication", 908 " browse Open a Tangled repository in a browser", 909 " issue Manage issues on a Tangled repository", 910 " pr Manage pull requests on a Tangled repository", 911 " repo Manage repositories on Tangled", 912 " ssh-key Manage SSH keys on Tangled", 913 " string Manage strings on Tangled", 914 "", 915 "Flags:", 916 " --appview string Appview host URL (default \"https://bobbin.klbr.net\")", 917 " --config string Path to config file", 918 " --account string Account handle or DID", 919 " --json Output in JSON format", 920 " -h, --help Help", 921 ], 922 "\n", 923 ), 924 ) 925}